[TikiWiki-commits] [Git][tikiwiki/tiki][master] [FIX] Datetimesync: Improve functional timezone synchronization detection
Benoit Grégoire (@benoitg) via TikiWiki-cvs <[email protected]>
| Newsgroups | gmane.comp.cms.tiki.cvs |
|---|---|
| Message-ID | <698502c767ae3_3b184980535d2@gitlab-sidekiq-low-urgency-cpu-bound-v2-79666dbc4f-wcpqw.mail> |
Benoit Grégoire pushed to branch master at Tiki Wiki CMS Groupware / Tiki
Commits:
cf2ed292 by Alain Cisirika at 2026-02-05T20:42:16+00:00
[FIX] Datetimesync: Improve functional timezone synchronization detection
---
* [FIX] Datetimesync: Improve functional timezone synchronization detection
See merge request tikiwiki/tiki!8516
- - - - -
4 changed files:
- lib/core/Feedback.php
- lib/core/Services/User/Controller.php
- lib/test/Core/Services/User/ControllerTest.php
- templates/user/localtimezonesync.tpl
Changes:
=====================================
lib/core/Feedback.php
=====================================
@@ -27,12 +27,16 @@ if (str_contains($_SERVER['SCRIPT_NAME'], basename(__FILE__))) {
class Feedback
{
/**
- * Add error feedback
+ * Add error feedback.
*
- * This is a specific application of the add function below for errors.
+ * This method stores the error message in the session-based feedback stack.
+ * As a result, the feedback will persist across HTTP redirects and will be
+ * displayed on the next page load via the {feedback} Smarty function.
*
- * @param $feedback
- * @param bool $sendHeaders
+ * This is a specific application of the add() method for errors.
+ *
+ * @param mixed $feedback Error message or feedback array
+ * @param bool $sendHeaders Whether to immediately send feedback headers (AJAX use)
* @throws Exception
*/
public static function error($feedback, $sendHeaders = false)
=====================================
lib/core/Services/User/Controller.php
=====================================
@@ -1133,6 +1133,7 @@ class Services_User_Controller
global $user, $tikilib;
$access = TikiLib::lib('access');
$clientTz = $input->client_timezone->text();
+ $userPreferenceTz = $tikilib->get_user_preference($user, 'display_timezone', '');
$action = $input->timezone_action->text();
if ($action === 'never') {
@@ -1143,25 +1144,85 @@ class Services_User_Controller
return [];
}
- if (! empty($_SESSION["temp_timezone"])) {
- $preferedTz = $_SESSION["temp_timezone"];
- } else {
- $preferedTz = $tikilib->get_user_preference($user, 'display_timezone', '');
+ if (! $this->isValidTimezone($clientTz)) {
+ Feedback::error(tr("Invalid detected timezone."));
+ $access->redirect($_SERVER['HTTP_REFERER']);
+ return [];
+ }
+
+ // Determine effective timezone deterministically
+ $effectiveTz = '';
+
+ // Temporary session timezone (highest priority)
+ if (! empty($_SESSION['temp_timezone']) && $this->isValidTimezone($_SESSION['temp_timezone'])) {
+ $effectiveTz = $_SESSION['temp_timezone'];
+ } elseif (! empty($userPreferenceTz) && $this->isValidTimezone($userPreferenceTz)) { // User preference
+ $effectiveTz = $userPreferenceTz;
+ } else { // Fallback to detected/system timezone
+ $effectiveTz = $clientTz;
}
if ($action === 'switch') {
$tikilib->set_user_preference($user, 'display_timezone', $clientTz);
- $preferedTz = $tikilib->get_user_preference($user, 'display_timezone', '');
+
+ unset($_SESSION['temp_timezone']);
+
+ $userPreferenceTz = $clientTz;
+ $effectiveTz = $clientTz;
} elseif ($action === 'temporary') {
$_SESSION["temp_timezone"] = $clientTz;
- $preferedTz = $clientTz;
+ $effectiveTz = $clientTz;
+ }
+
+ // Determine if the client timezone and effective (user/system) timezone are different.
+ // If they differ by name, we check if they are functionally equivalent
+ // meaning their UTC offsets are the same throughout the year.
+ //
+ // This avoids showing misleading "timezone synchronization" notification when the
+ // timezones are actually the same in behavior but have different names (e.g. "America/Toronto" vs "EST").
+ //
+ // This logic improves UX by not bothering users with unnecessary alerts
+ // when the functional result is the same
+ $different = false;
+ // In detect mode, effective timezone always follows client
+ if (empty($userPreferenceTz)) {
+ $effectiveTz = $clientTz;
+ }
+ // $clientTz is already validated above
+ if (! empty($clientTz)) {
+ if ($clientTz !== $effectiveTz) {
+ $different = true;
+ try {
+ $tzClient = new DateTimeZone($clientTz);
+ $tzEffective = new DateTimeZone($effectiveTz);
+ $year = date('Y');
+ $functionallySame = true;
+
+ for ($month = 1; $month <= 12; $month++) {
+ $dateUTC = new DateTime("$year-$month-15 12:00:00", new DateTimeZone('UTC'));
+ $offsetClient = $tzClient->getOffset($dateUTC);
+ $offsetEffective = $tzEffective->getOffset($dateUTC);
+
+ if ($offsetClient !== $offsetEffective) {
+ $functionallySame = false;
+ break;
+ }
+ }
+ if ($functionallySame) {
+ $different = false;
+ }
+ } catch (Exception $e) {
+ Feedback::error("TimezoneSync Error: " . $e->getMessage());
+ $different = false;
+ }
+ }
}
- $different = $clientTz && $clientTz !== $preferedTz;
$result = [
- 'different' => $different ? true : false,
- 'preferedTimezone' => $preferedTz,
+ 'different' => $different,
+ 'preferedTimezone' => $userPreferenceTz,
'clientTimezone' => $clientTz,
+ 'effectiveTimezone' => $effectiveTz,
];
if ($action === 'switch' || $action === 'temporary') {
@@ -1171,6 +1232,11 @@ class Services_User_Controller
return $result;
}
+ private function isValidTimezone(string $timezone): bool
+ {
+ return in_array($timezone, DateTimeZone::listIdentifiers(), true);
+ }
+
private function redirectAndReturn($data = []): array
{
header("Location:" . $_SERVER['HTTP_REFERER']);
=====================================
lib/test/Core/Services/User/ControllerTest.php
=====================================
@@ -13,14 +13,22 @@ use Services_User_Controller as ServicesUserController;
class ServicesUserControllerTest extends TestCase
{
- protected $controller;
protected static $originalTimezone;
protected $originalUserSyncPref;
protected function setUp(): void
{
- global $prefs,$user;
+ global $prefs, $user;
+ // Ensure session exists (required for temporary timezone)
+ if (session_status() !== PHP_SESSION_ACTIVE) {
+ session_start();
+ }
+
+ // Simulate a logged-in user (required by get_display_timezone)
+ $user = 'testuser';
+
+ // Fake referer used by controller redirects
$_SERVER['HTTP_REFERER'] = 'http://example.com/some/page';
self::$originalTimezone = $prefs['display_timezone'];
@@ -36,10 +44,12 @@ class ServicesUserControllerTest extends TestCase
unset($_SESSION['temp_timezone']);
unset($_SERVER['HTTP_REFERER']);
+ unset($GLOBALS['user']);
}
public function testTimezoneSwitchAction(): void
{
+ global $prefs;
$input = new JitFilter([
'client_timezone' => 'Africa/Lubumbashi',
@@ -48,28 +58,64 @@ class ServicesUserControllerTest extends TestCase
$result = (new ServicesUserController())->actionLocalTimezoneSync($input);
- $this->assertEquals([
- 'different' => false,
- 'preferedTimezone' => 'Africa/Lubumbashi',
- 'clientTimezone' => 'Africa/Lubumbashi'
- ], $result);
+ // Actual behavior: preference must be updated
+ $this->assertEquals('Africa/Lubumbashi', $prefs['display_timezone']);
+
+ // Response consistency
+ $this->assertFalse($result['different']);
+ $this->assertEquals('Africa/Lubumbashi', $result['effectiveTimezone']);
}
public function testTimezoneTemporaryAction(): void
{
+ global $prefs;
+
+ $originalTz = $prefs['display_timezone'];
+
$input = new JitFilter([
'client_timezone' => 'Africa/Lubumbashi',
'timezone_action' => 'temporary',
]);
- $_SESSION['temp_timezone'] = $input->client_timezone;
+ $result = (new ServicesUserController())->actionLocalTimezoneSync($input);
+
+ // Actual behavior: session timezone only
+ $this->assertEquals('Africa/Lubumbashi', $_SESSION['temp_timezone']);
+
+ // Preference must remain unchanged
+ $this->assertEquals($originalTz, $prefs['display_timezone']);
+
+ // Response consistency
+ $this->assertEquals('Africa/Lubumbashi', $result['effectiveTimezone']);
+ }
+ public function testFunctionallyEquivalentTimezonesAreNotDifferent(): void
+ {
+ global $tikilib, $prefs;
+
+ // Deterministic initial state
+ $prefs['display_timezone'] = 'Etc/UTC';
+ date_default_timezone_set('Etc/UTC');
+
+ // Explicit user preference (not detect mode)
+ $tikilib->set_user_preference('testuser', 'display_timezone', 'Etc/UTC');
+
+ $input = new JitFilter([
+ 'client_timezone' => 'UTC',
+ 'timezone_action' => 'check',
+ ]);
$result = (new ServicesUserController())->actionLocalTimezoneSync($input);
- $this->assertEquals([
- 'different' => false,
- 'preferedTimezone' => 'Africa/Lubumbashi',
- 'clientTimezone' => 'Africa/Lubumbashi'
- ], $result);
+ // Functional equivalence must not trigger sync
+ $this->assertFalse(
+ $result['different'],
+ 'Equivalent timezones should not trigger sync notification'
+ );
+
+ // Effective timezone may be normalized
+ $this->assertContains(
+ $result['effectiveTimezone'],
+ ['UTC', 'Etc/UTC']
+ );
}
}
=====================================
templates/user/localtimezonesync.tpl
=====================================
@@ -7,7 +7,7 @@
{tr _0='<strong><span class="detected-tz-name"></span></strong>' _1='<strong><span class="current-tz-name"></span></strong>'}The detected timezone is %0, but your configured timezone is set to %1.{/tr}
</p>
<p id="tz_message_unconfigured" style="display: none;">
- {tr _0='<strong><span class="detected-tz-name"></span></strong>'}The detected timezone is %0, and you have not configured a preferred timezone yet.{/tr}
+ {tr _0='<strong><span class="detected-tz-name"></span></strong>' _1='<strong><span class="effective-tz-name"></span></strong>'}The detected timezone is %0, and you have not configured a preferred timezone. So the system uses %1 by default.{/tr}
</p>
<p class="mb-3">{tr}What would you like to do?{/tr}</p>
<input type="hidden" name="client_timezone" id="client-timezone" value=""/>
@@ -52,8 +52,12 @@
$('.detected-tz-name').text(response.clientTimezone);
$('.current-tz-name').text(response.preferedTimezone);
if (response.preferedTimezone) {
+ // If the preference string is not empty, show the "configured" message.
+ $('.current-tz-name').text(response.preferedTimezone);
$('#tz_message_configured').show();
} else {
+ // If the preference string is empty, show the "unconfigured" message.
+ $('.effective-tz-name').text(response.effectiveTimezone);
$('#tz_message_unconfigured').show();
}
}
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/cf2ed292d7ebb6386cee05abed6b46245b27db9c
--
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/cf2ed292d7ebb6386cee05abed6b46245b27db9c
You're receiving this email because of your account on gitlab.com.
_______________________________________________
TikiWiki-cvs mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/tikiwiki-cvs