Cronjob calculation now uses "local" time instead of GMT
authorAlexander Ebert <ebert@woltlab.com>
Wed, 5 Apr 2017 16:53:26 +0000 (18:53 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Wed, 5 Apr 2017 16:53:32 +0000 (18:53 +0200)
Initially \DateTime was used, but dropped again due to some serious bugs
in \DateTime::setDate() which would have forced me to write some really
nasty work-around, including spawning new \DateTime objects left and
right.

Also \DateTime works slightly different, which would require a lot of
significant changes to the code. From my experience, this is simply not
worth it given the high potential of writing erroneous code. Also bugs
in the cronjob calculation tend to surface after years, turning such a
change into a future nightmare.

Fixes #2164

wcfsetup/install/files/lib/util/CronjobUtil.class.php

index 327c49de6977dceb5a33c70369fe7455afbcdb00..5089c82df39c9cca3e9f84d1dfb48d03b6f0bcdb 100644 (file)
@@ -7,6 +7,10 @@ use wcf\system\exception\SystemException;
  * As against the official cron-documentation, this implementation
  * does not support using nicknames (prefixed by the '@' character).
  * 
+ * Notice: This class used `gmdate()`/`gmmktime()` in previous versions,
+ * but now utilized the `date()`/`mktime()` counter-parts, but with the
+ * timezone set to the value of the `TIMEZONE` option.
+ * 
  * @author     Alexander Ebert
  * @copyright  2001-2017 WoltLab GmbH
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
@@ -62,6 +66,13 @@ final class CronjobUtil {
         * @return      integer
         */
        public static function calculateNextExec($minute, $hour, $dom, $month, $dow, $timeBase = TIME_NOW) {
+               // using the native `date()` and `mktime()` functions is dangerous
+               // unless we explicitly set the correct timezone
+               $originalTimezone = date_default_timezone_get();
+               if ($originalTimezone !== TIMEZONE) {
+                       date_default_timezone_set(TIMEZONE);
+               }
+               
                // initialize fields
                self::$timeBase = $timeBase;
                self::$result = [
@@ -122,7 +133,7 @@ final class CronjobUtil {
                self::calculateTime($values);
                
                // return timestamp
-               return gmmktime(
+               $timestamp = mktime(
                        self::$result['hour'],
                        self::$result['minute'],
                        1,
@@ -130,6 +141,13 @@ final class CronjobUtil {
                        self::$result['day'],
                        self::$result['year']
                );
+               
+               // restore the original timezone
+               if ($originalTimezone !== TIMEZONE) {
+                       date_default_timezone_set($originalTimezone);
+               }
+               
+               return $timestamp;
        }
        
        /**
@@ -178,13 +196,13 @@ final class CronjobUtil {
                $timeBase = self::$timeBase;
                
                if ($addAnDay) {
-                       $date = explode('.', gmdate("d.m.Y", $timeBase));
-                       $timeBase = gmmktime(0, 0, 1, $date[1], $date[0] + 1, $date[2]);
+                       $date = explode('.', date("d.m.Y", $timeBase));
+                       $timeBase = mktime(0, 0, 1, $date[1], $date[0] + 1, $date[2]);
                }
                
-               $day = gmdate('j', $timeBase);
-               $month = gmdate('n', $timeBase);
-               $year = gmdate('Y', $timeBase);
+               $day = date('j', $timeBase);
+               $month = date('n', $timeBase);
+               $year = date('Y', $timeBase);
                
                // calculate month of next execution and if its not the current one reset previous calculations
                $dateMonth = self::calculateMonth($month, $year, $values);
@@ -193,7 +211,7 @@ final class CronjobUtil {
                        $month = $dateMonth['month'];
                        $year = $dateMonth['year'];
                        
-                       $timeBase = gmmktime(0, 0, 1, $month, $day, $year);
+                       $timeBase = mktime(0, 0, 1, $month, $day, $year);
                        
                        if (!$addAnDay) {
                                self::calculateHour($values, $timeBase);
@@ -202,11 +220,11 @@ final class CronjobUtil {
                
                // calculate date of next execution based upon day of week
                $dateDow = self::calculateDow($month, $year, $values, $day);
-               $dateDowTimestamp = gmmktime(0, 0, 1, $dateDow['month'], $dateDow['day'], $dateDow['year']);
+               $dateDowTimestamp = mktime(0, 0, 1, $dateDow['month'], $dateDow['day'], $dateDow['year']);
                
                // calculate date of next execution based upon day of month
                $dateDom = self::calculateDom($month, $year, $values, $day);
-               $dateDomTimestamp = gmmktime(0, 0, 1, $dateDom['month'], $dateDom['day'], $dateDom['year']);
+               $dateDomTimestamp = mktime(0, 0, 1, $dateDom['month'], $dateDom['day'], $dateDom['year']);
                
                // pick the earlier date if both dom and dow are restricted
                if (self::$domRestricted && self::$dowRestricted) {
@@ -244,7 +262,7 @@ final class CronjobUtil {
                // compare day, month and year whether we have to recalculate hour and minute
                if (($day != self::$result['day']) || ($month != self::$result['month']) || ($year != self::$result['year'])) {
                        // calculate new time base
-                       $timeBase = gmmktime(0, 0, 1, self::$result['month'], self::$result['day'], self::$result['year']);
+                       $timeBase = mktime(0, 0, 1, self::$result['month'], self::$result['day'], self::$result['year']);
                        
                        self::calculateHour($values, $timeBase);
                }
@@ -260,11 +278,11 @@ final class CronjobUtil {
         * @return      array
         */
        protected static function calculateDow($month, $year, array &$values, $day = 1) {
-               $days = gmdate('t', gmmktime(0, 0, 1, $month, $day, $year));
+               $days = date('t', mktime(0, 0, 1, $month, $day, $year));
                
                for ($i = $day; $i <= $days; $i++) {
                        // get dow
-                       $dow = gmdate('w', gmmktime(0, 0, 1, $month, $i, $year));
+                       $dow = date('w', mktime(0, 0, 1, $month, $i, $year));
                        
                        if (in_array($dow, $values['dow'])) {
                                return [
@@ -290,7 +308,7 @@ final class CronjobUtil {
         * @return      array
         */
        protected static function calculateDom($month, $year, array &$values, $day = 1) {
-               $days = gmdate('t', gmmktime(0, 0, 1, $month, $day, $year));
+               $days = date('t', mktime(0, 0, 1, $month, $day, $year));
                
                for ($i = $day; $i <= $days; $i++) {
                        if (in_array($i, $values['dom'])) {
@@ -319,7 +337,7 @@ final class CronjobUtil {
                $addAnDay = false;
                
                // compare hour
-               $hour = intval(gmdate('G', $timeBase));
+               $hour = intval(date('G', $timeBase));
                $index = self::findKey($hour, $values['hour'], false);
                if ($index === false) {
                        $index = self::findKey($hour, $values['hour']);
@@ -363,7 +381,7 @@ final class CronjobUtil {
                        $minute = 0;
                }
                else {
-                       $minute = gmdate('i', $timeBase);
+                       $minute = date('i', $timeBase);
                }
                
                $index = self::findKey($minute, $values['minute'], false);