From a91004ef1ba3bb09c3302cac3d6340e147ae7fe5 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 21 May 2016 11:16:55 -0700 Subject: [PATCH] Detect timezone discrepancies and prompt users to reconcile them Summary: Ref T3025. This adds a check for different client/server timezone offsets and gives users an option to fix them or ignore them. Test Plan: - Fiddled with timezone in Settings and System Preferences. - Got appropriate prompts and behavior after simulating various trips to and from exotic locales. In particular, this slightly tricky case seems to work correctly: - Travel to NY. - Ignore discrepancy (you're only there for a couple hours for an important meeting, and returning to SF on a later flight). - Return to SF for a few days. - Travel back to NY. - You should be prompted again, since you left the timezone after you ignored the discrepancy. {F1654528} {F1654529} {F1654530} Reviewers: chad Reviewed By: chad Maniphest Tasks: T3025 Differential Revision: https://secure.phabricator.com/D15961 --- resources/celerity/packages.php | 1 + src/__phutil_library_map__.php | 2 + .../people/storage/PhabricatorUser.php | 11 ++ .../PhabricatorSettingsApplication.php | 2 + .../PhabricatorSettingsTimezoneController.php | 108 ++++++++++++++++++ .../PhabricatorDateTimeSettingsPanel.php | 4 +- .../storage/PhabricatorUserPreferences.php | 1 + src/view/page/PhabricatorStandardPageView.php | 24 ++++ .../rsrc/js/core/behavior-detect-timezone.js | 53 +++++++++ 9 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 src/applications/settings/controller/PhabricatorSettingsTimezoneController.php create mode 100644 webroot/rsrc/js/core/behavior-detect-timezone.js diff --git a/resources/celerity/packages.php b/resources/celerity/packages.php index fa19f50095..b44707f407 100644 --- a/resources/celerity/packages.php +++ b/resources/celerity/packages.php @@ -81,6 +81,7 @@ return array( 'javelin-behavior-scrollbar', 'javelin-behavior-durable-column', 'conpherence-thread-manager', + 'javelin-behavior-detect-timezone', ), 'core.pkg.css' => array( 'phabricator-core-css', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 3749af3276..3dbd3f1b57 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3355,6 +3355,7 @@ phutil_register_library_map(array( 'PhabricatorSettingsMainController' => 'applications/settings/controller/PhabricatorSettingsMainController.php', 'PhabricatorSettingsMainMenuBarExtension' => 'applications/settings/extension/PhabricatorSettingsMainMenuBarExtension.php', 'PhabricatorSettingsPanel' => 'applications/settings/panel/PhabricatorSettingsPanel.php', + 'PhabricatorSettingsTimezoneController' => 'applications/settings/controller/PhabricatorSettingsTimezoneController.php', 'PhabricatorSetupCheck' => 'applications/config/check/PhabricatorSetupCheck.php', 'PhabricatorSetupCheckTestCase' => 'applications/config/check/__tests__/PhabricatorSetupCheckTestCase.php', 'PhabricatorSetupIssue' => 'applications/config/issue/PhabricatorSetupIssue.php', @@ -8065,6 +8066,7 @@ phutil_register_library_map(array( 'PhabricatorSettingsMainController' => 'PhabricatorController', 'PhabricatorSettingsMainMenuBarExtension' => 'PhabricatorMainMenuBarExtension', 'PhabricatorSettingsPanel' => 'Phobject', + 'PhabricatorSettingsTimezoneController' => 'PhabricatorController', 'PhabricatorSetupCheck' => 'Phobject', 'PhabricatorSetupCheckTestCase' => 'PhabricatorTestCase', 'PhabricatorSetupIssue' => 'Phobject', diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index bdb2415733..dc36f55bc0 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -755,6 +755,17 @@ final class PhabricatorUser return new DateTimeZone($this->getTimezoneIdentifier()); } + public function getTimeZoneOffset() { + $timezone = $this->getTimeZone(); + $now = new DateTime('@'.PhabricatorTime::getNow()); + $offset = $timezone->getOffset($now); + + // Javascript offsets are in minutes and have the opposite sign. + $offset = -(int)($offset / 60); + + return $offset; + } + public function formatShortDateTime($when, $now = null) { if ($now === null) { $now = PhabricatorTime::getNow(); diff --git a/src/applications/settings/application/PhabricatorSettingsApplication.php b/src/applications/settings/application/PhabricatorSettingsApplication.php index d0d6494c12..66bce7205f 100644 --- a/src/applications/settings/application/PhabricatorSettingsApplication.php +++ b/src/applications/settings/application/PhabricatorSettingsApplication.php @@ -32,6 +32,8 @@ final class PhabricatorSettingsApplication extends PhabricatorApplication { '(?:(?P\d+)/)?(?:panel/(?P[^/]+)/)?' => 'PhabricatorSettingsMainController', 'adjust/' => 'PhabricatorSettingsAdjustController', + 'timezone/(?P[^/]+)/' + => 'PhabricatorSettingsTimezoneController', ), ); } diff --git a/src/applications/settings/controller/PhabricatorSettingsTimezoneController.php b/src/applications/settings/controller/PhabricatorSettingsTimezoneController.php new file mode 100644 index 0000000000..6af4d88eb9 --- /dev/null +++ b/src/applications/settings/controller/PhabricatorSettingsTimezoneController.php @@ -0,0 +1,108 @@ +getViewer(); + + $client_offset = $request->getURIData('offset'); + $client_offset = (int)$client_offset; + + $timezones = DateTimeZone::listIdentifiers(); + $now = new DateTime('@'.PhabricatorTime::getNow()); + + $options = array( + 'ignore' => pht('Ignore Conflict'), + ); + + foreach ($timezones as $identifier) { + $zone = new DateTimeZone($identifier); + $offset = -($zone->getOffset($now) / 60); + if ($offset == $client_offset) { + $options[$identifier] = $identifier; + } + } + + $settings_help = pht( + 'You can change your date and time preferences in Settings.'); + + if ($request->isFormPost()) { + $timezone = $request->getStr('timezone'); + + $pref_ignore = PhabricatorUserPreferences::PREFERENCE_IGNORE_OFFSET; + + $preferences = $viewer->loadPreferences(); + + if ($timezone == 'ignore') { + $preferences + ->setPreference($pref_ignore, $client_offset) + ->save(); + + return $this->newDialog() + ->setTitle(pht('Conflict Ignored')) + ->appendParagraph( + pht( + 'The conflict between your browser and profile timezone '. + 'settings will be ignored.')) + ->appendParagraph($settings_help) + ->addCancelButton('/', pht('Done')); + } + + if (isset($options[$timezone])) { + $preferences + ->setPreference($pref_ignore, null) + ->save(); + + $viewer + ->setTimezoneIdentifier($timezone) + ->save(); + } + } + + $server_offset = $viewer->getTimeZoneOffset(); + + if ($client_offset == $server_offset) { + return $this->newDialog() + ->setTitle(pht('Timezone Calibrated')) + ->appendParagraph( + pht( + 'Your browser timezone and profile timezone are now '. + 'in agreement (%s).', + $this->formatOffset($client_offset))) + ->appendParagraph($settings_help) + ->addCancelButton('/', pht('Done')); + } + + $form = id(new AphrontFormView()) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setName('timezone') + ->setLabel(pht('Timezone')) + ->setOptions($options)); + + return $this->newDialog() + ->setTitle(pht('Adjust Timezone')) + ->appendParagraph( + pht( + 'Your browser timezone (%s) differs from your profile timezone '. + '(%s). You can ignore this conflict or adjust your profile setting '. + 'to match your client.', + $this->formatOffset($client_offset), + $this->formatOffset($server_offset))) + ->appendForm($form) + ->addCancelButton(pht('Cancel')) + ->addSubmitButton(pht('Submit')); + } + + private function formatOffset($offset) { + $offset = $offset / 60; + + if ($offset >= 0) { + return pht('GMT-%d', $offset); + } else { + return pht('GMT+%d', -$offset); + } + } + +} diff --git a/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php b/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php index 7c70089836..6628f21187 100644 --- a/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php @@ -21,6 +21,7 @@ final class PhabricatorDateTimeSettingsPanel extends PhabricatorSettingsPanel { $pref_time = PhabricatorUserPreferences::PREFERENCE_TIME_FORMAT; $pref_date = PhabricatorUserPreferences::PREFERENCE_DATE_FORMAT; $pref_week_start = PhabricatorUserPreferences::PREFERENCE_WEEK_START_DAY; + $pref_ignore = PhabricatorUserPreferences::PREFERENCE_IGNORE_OFFSET; $preferences = $user->loadPreferences(); $errors = array(); @@ -41,7 +42,8 @@ final class PhabricatorDateTimeSettingsPanel extends PhabricatorSettingsPanel { $request->getStr($pref_date)) ->setPreference( $pref_week_start, - $request->getStr($pref_week_start)); + $request->getStr($pref_week_start)) + ->setPreference($pref_ignore, null); if (!$errors) { $preferences->save(); diff --git a/src/applications/settings/storage/PhabricatorUserPreferences.php b/src/applications/settings/storage/PhabricatorUserPreferences.php index d8c4982ccc..18f0dfe980 100644 --- a/src/applications/settings/storage/PhabricatorUserPreferences.php +++ b/src/applications/settings/storage/PhabricatorUserPreferences.php @@ -43,6 +43,7 @@ final class PhabricatorUserPreferences extends PhabricatorUserDAO { const PREFERENCE_PROFILE_MENU_COLLAPSED = 'profile-menu.collapsed'; const PREFERENCE_FAVORITE_POLICIES = 'policy.favorites'; + const PREFERENCE_IGNORE_OFFSET = 'time.offset.ignore'; // These are in an unusual order for historic reasons. const MAILTAG_PREFERENCE_NOTIFY = 0; diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php index 08f0c0b3c4..03c83f267a 100644 --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -223,6 +223,30 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView } if ($user) { + if ($user->isLoggedIn()) { + $offset = $user->getTimeZoneOffset(); + + $preferences = $user->loadPreferences(); + $ignore_key = PhabricatorUserPreferences::PREFERENCE_IGNORE_OFFSET; + + $ignore = $preferences->getPreference($ignore_key); + if (!strlen($ignore)) { + $ignore = null; + } + + Javelin::initBehavior( + 'detect-timezone', + array( + 'offset' => $offset, + 'uri' => '/settings/timezone/', + 'message' => pht( + 'Your browser timezone setting differs from the timezone '. + 'setting in your profile.'), + 'ignoreKey' => $ignore_key, + 'ignore' => $ignore, + )); + } + $default_img_uri = celerity_get_resource_uri( 'rsrc/image/icon/fatcow/document_black.png'); diff --git a/webroot/rsrc/js/core/behavior-detect-timezone.js b/webroot/rsrc/js/core/behavior-detect-timezone.js new file mode 100644 index 0000000000..fa0a8e96a7 --- /dev/null +++ b/webroot/rsrc/js/core/behavior-detect-timezone.js @@ -0,0 +1,53 @@ +/** + * @provides javelin-behavior-detect-timezone + * @requires javelin-behavior + * javelin-uri + * phabricator-notification + */ + +JX.behavior('detect-timezone', function(config) { + + var offset = new Date().getTimezoneOffset(); + var ignore = config.ignore; + + if (ignore !== null) { + // If we're ignoring a client offset and it's the current offset, just + // bail. This means the user has chosen to ignore the clock difference + // between the current client setting and their server setting. + if (offset == ignore) { + return; + } + + // If we're ignoring a client offset but the current offset is different, + // wipe the offset. If you go from SF to NY, ignore the difference, return + // to SF, then travel back to NY a few months later, we want to prompt you + // again. This code will clear the ignored setting upon your return to SF. + new JX.Request('/settings/adjust/', JX.bag) + .setData({key: config.ignoreKey, value: ''}) + .send(); + + ignore = null; + } + + // If the client and server clocks are in sync, we're all set. + if (offset == config.offset) { + return; + } + + var notification = new JX.Notification() + .alterClassName('jx-notification-alert', true) + .setContent(config.message) + .setDuration(0); + + notification.listen('activate', function() { + JX.Stratcom.context().kill(); + notification.hide(); + + var uri = config.uri + offset + '/'; + + new JX.Workflow(uri) + .start(); + }); + + notification.show(); +});