Skip to content

Commit 21c4bc8

Browse files
epeicherepeicherjsnajdr
authored andcommitted
fix(date): Recover WP timezone after third-party moment-timezone reload (#75831)
Fixes issue where plugins (e.g. WooCommerce) that load their own copy of moment-timezone destroy the custom 'WP' zone and cause post times to be offset by 2x the UTC difference. The fix adds ensureWPTimezone() guards to isInTheFuture(), getDate(), and humanTimeDiff(). The custom WP timezone zone is now cached in packed form on first creation, so it can be re-added without requiring moment-timezone-utils after a third-party reload. Includes tests that verify recovery after the zone is destroyed. Co-authored-by: epeicher <epeicher@git.wordpress.org> Co-authored-by: jsnajdr <jsnajdr@git.wordpress.org>
1 parent 3b79ece commit 21c4bc8

2 files changed

Lines changed: 110 additions & 16 deletions

File tree

‎packages/date/src/index.ts‎

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -184,34 +184,57 @@ export function __experimentalGetSettings() {
184184
return getSettings();
185185
}
186186

187+
// Cached packed zone string, used to re-add the WP zone without requiring
188+
// moment-timezone-utils (which provides .pack()) after a third-party plugin
189+
// reloads moment-timezone and destroys both the zone and the utils.
190+
let wpZonePacked: string | undefined;
191+
192+
/**
193+
* Ensures the custom WP timezone zone exists in moment-timezone's registry.
194+
*
195+
* Third-party plugins (e.g. WooCommerce) may load their own copy of
196+
* moment-timezone, which reinitializes the internal zone storage and
197+
* destroys the custom 'WP' zone. This function checks for the zone's
198+
* existence and re-creates it if necessary.
199+
*/
200+
function ensureWPTimezone() {
201+
if ( ! momentLib.tz.zone( WP_ZONE ) ) {
202+
if ( wpZonePacked ) {
203+
momentLib.tz.add( wpZonePacked );
204+
} else {
205+
setupWPTimezone();
206+
}
207+
}
208+
}
209+
187210
function setupWPTimezone() {
188211
// Get the current timezone settings from the WP timezone string.
189212
const currentTimezone = momentLib.tz.zone( settings.timezone.string );
190213

214+
let packed;
191215
// Check to see if we have a valid TZ data, if so, use it for the custom WP_ZONE timezone, otherwise just use the offset.
192216
if ( currentTimezone ) {
193217
// Create WP timezone based off settings.timezone.string. We need to include the additional data so that we
194218
// don't lose information about daylight savings time and other items.
195219
// See https://github.com/WordPress/gutenberg/pull/48083
196-
momentLib.tz.add(
197-
momentLib.tz.pack( {
198-
name: WP_ZONE,
199-
abbrs: currentTimezone.abbrs,
200-
untils: currentTimezone.untils,
201-
offsets: currentTimezone.offsets,
202-
} )
203-
);
220+
packed = momentLib.tz.pack( {
221+
name: WP_ZONE,
222+
abbrs: currentTimezone.abbrs,
223+
untils: currentTimezone.untils,
224+
offsets: currentTimezone.offsets,
225+
} );
204226
} else {
205227
// Create WP timezone based off dateSettings.
206-
momentLib.tz.add(
207-
momentLib.tz.pack( {
208-
name: WP_ZONE,
209-
abbrs: [ WP_ZONE ],
210-
untils: [ null ],
211-
offsets: [ -settings.timezone.offset * 60 || 0 ],
212-
} )
213-
);
228+
packed = momentLib.tz.pack( {
229+
name: WP_ZONE,
230+
abbrs: [ WP_ZONE ],
231+
untils: [ null ],
232+
offsets: [ -settings.timezone.offset * 60 || 0 ],
233+
} );
214234
}
235+
236+
wpZonePacked = packed;
237+
momentLib.tz.add( packed );
215238
}
216239

217240
// Date constants.
@@ -547,6 +570,7 @@ export function gmdateI18n(
547570
* @return Is in the future.
548571
*/
549572
export function isInTheFuture( dateValue: Date | string | number ) {
573+
ensureWPTimezone();
550574
const now = momentLib.tz( WP_ZONE );
551575
const momentObject = momentLib.tz( dateValue, WP_ZONE );
552576

@@ -561,6 +585,7 @@ export function isInTheFuture( dateValue: Date | string | number ) {
561585
* @return Date
562586
*/
563587
export function getDate( dateString?: string | null ) {
588+
ensureWPTimezone();
564589
if ( ! dateString ) {
565590
return momentLib.tz( WP_ZONE ).toDate();
566591
}
@@ -580,6 +605,7 @@ export function humanTimeDiff(
580605
from: Moment | Date | string | number,
581606
to?: Moment | Date | string | number
582607
) {
608+
ensureWPTimezone();
583609
const fromMoment = momentLib.tz( from, WP_ZONE );
584610
const toMoment = to ? momentLib.tz( to, WP_ZONE ) : momentLib.tz( WP_ZONE );
585611
return fromMoment.from( toMoment );

‎packages/date/src/test/index.js‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import momentLib from 'moment';
5+
16
/**
27
* Internal dependencies
38
*/
@@ -683,3 +688,66 @@ describe( 'Moment.js Localization', () => {
683688
} );
684689
} );
685690
} );
691+
692+
describe( 'WP timezone zone recovery', () => {
693+
// Simulate what happens when a third-party plugin (e.g. WooCommerce)
694+
// loads its own copy of moment-timezone, which reinitializes the
695+
// internal zone storage and destroys the custom 'WP' zone.
696+
function destroyWPZone() {
697+
// moment-timezone stores zones in an internal object.
698+
// Removing 'wp' simulates a third-party moment-timezone reload.
699+
const zones = momentLib.tz._zones;
700+
delete zones.wp;
701+
}
702+
703+
it( 'getDate should recover after WP zone is lost', () => {
704+
const settings = getSettings();
705+
setSettings( {
706+
...settings,
707+
timezone: { offset: 4, string: '' },
708+
} );
709+
710+
destroyWPZone();
711+
712+
// Should not throw and should return a valid date.
713+
const result = getDate( '2024-01-15T10:00:00' );
714+
expect( result ).toBeInstanceOf( Date );
715+
expect( result.getTime() ).not.toBeNaN();
716+
717+
setSettings( settings );
718+
} );
719+
720+
it( 'isInTheFuture should recover after WP zone is lost', () => {
721+
const settings = getSettings();
722+
setSettings( {
723+
...settings,
724+
timezone: { offset: 4, string: '' },
725+
} );
726+
727+
destroyWPZone();
728+
729+
// Create a date far in the future.
730+
const futureDate = new Date( Date.now() + 1000 * 60 * 60 * 24 * 365 );
731+
expect( isInTheFuture( futureDate ) ).toBe( true );
732+
733+
setSettings( settings );
734+
} );
735+
736+
it( 'humanTimeDiff should recover after WP zone is lost', () => {
737+
const settings = getSettings();
738+
setSettings( {
739+
...settings,
740+
timezone: { offset: 4, string: '' },
741+
} );
742+
743+
destroyWPZone();
744+
745+
const result = humanTimeDiff(
746+
'2023-04-28T11:00:00.000Z',
747+
'2023-04-28T12:00:00.000Z'
748+
);
749+
expect( result ).toBe( 'an hour ago' );
750+
751+
setSettings( settings );
752+
} );
753+
} );

0 commit comments

Comments
 (0)