[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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.