weekly release 3.1dev
[moodle.git] / lib / classes / date.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Core date and time related code.
19  *
20  * @package   core
21  * @copyright 2015 Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
22  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  * @author    Petr Skoda <petr.skoda@totaralms.com>
24  */
26 /**
27  * Core date and time related code.
28  *
29  * @since Moodle 2.9
30  * @package   core
31  * @copyright 2015 Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
32  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
33  * @author    Petr Skoda <petr.skoda@totaralms.com>
34  */
35 class core_date {
36     /** @var array list of recommended zones */
37     protected static $goodzones = null;
39     /** @var array list of BC zones supported by PHP */
40     protected static $bczones = null;
42     /** @var array mapping of timezones not supported by PHP */
43     protected static $badzones = null;
45     /** @var string the default PHP timezone right after config.php */
46     protected static $defaultphptimezone = null;
48     /**
49      * Returns a localised list of timezones.
50      * @param string $currentvalue
51      * @param bool $include99 should the server timezone info be included?
52      * @return array
53      */
54     public static function get_list_of_timezones($currentvalue = null, $include99 = false) {
55         self::init_zones();
57         // Localise first.
58         $timezones = array();
59         foreach (self::$goodzones as $tzkey => $ignored) {
60             $timezones[$tzkey] = self::get_localised_timezone($tzkey);
61         }
62         core_collator::asort($timezones);
64         // Add '99' if requested.
65         if ($include99 or $currentvalue == 99) {
66             $timezones['99'] = self::get_localised_timezone('99');
67         }
69         if (!isset($currentvalue) or isset($timezones[$currentvalue])) {
70             return $timezones;
71         }
73         if (is_numeric($currentvalue)) {
74             // UTC offset.
75             $modifier = ($currentvalue > 0) ? '+' : '';
76             $a = 'UTC' . $modifier . number_format($currentvalue, 1);
77             $timezones[$currentvalue] = get_string('timezoneinvalid', 'core_admin', $a);
78         } else {
79             // Some string we don't recognise.
80             $timezones[$currentvalue] = get_string('timezoneinvalid', 'core_admin', $currentvalue);
81         }
83         return $timezones;
84     }
86     /**
87      * Returns localised timezone name.
88      * @param string $tz
89      * @return string
90      */
91     public static function get_localised_timezone($tz) {
92         if ($tz == 99) {
93             $tz = self::get_server_timezone();
94             $tz = self::get_localised_timezone($tz);
95             return get_string('timezoneserver', 'core_admin', $tz);
96         }
98         if (get_string_manager()->string_exists(strtolower($tz), 'core_timezones')) {
99             $tz = get_string(strtolower($tz), 'core_timezones');
100         } else if ($tz === 'GMT' or $tz === 'Etc/GMT' or $tz === 'Etc/UTC') {
101             $tz = 'UTC';
102         } else if (preg_match('|^Etc/GMT([+-])([0-9]+)$|', $tz, $matches)) {
103             $sign = $matches[1] === '+' ? '-' : '+';
104             $tz = 'UTC' . $sign . $matches[2];
105         }
107         return $tz;
108     }
110     /**
111      * Normalise the timezone name. If timezone not supported
112      * this method falls back to server timezone (if valid)
113      * or default PHP timezone.
114      *
115      * @param int|string|float|DateTimeZone $tz
116      * @return string timezone compatible with PHP
117      */
118     public static function normalise_timezone($tz) {
119         global $CFG;
121         if ($tz instanceof DateTimeZone) {
122             return $tz->getName();
123         }
125         self::init_zones();
126         $tz = (string)$tz;
128         if (isset(self::$goodzones[$tz]) or isset(self::$bczones[$tz])) {
129             return $tz;
130         }
132         $fixed = false;
133         if (isset(self::$badzones[$tz])) {
134             // Convert to known zone.
135             $tz = self::$badzones[$tz];
136             $fixed = true;
137         } else if (is_number($tz)) {
138             // Half hour numeric offsets were already tested, try rounding to integers here.
139             $roundedtz = (string)(int)$tz;
140             if (isset(self::$badzones[$roundedtz])) {
141                 $tz = self::$badzones[$roundedtz];
142                 $fixed = true;
143             }
144         }
146         if ($fixed and isset(self::$goodzones[$tz]) or isset(self::$bczones[$tz])) {
147             return $tz;
148         }
150         // Is server timezone usable?
151         if (isset($CFG->timezone) and !is_numeric($CFG->timezone)) {
152             $result = @timezone_open($CFG->timezone); // Hide notices if invalid.
153             if ($result !== false) {
154                 return $result->getName();
155             }
156         }
158         // Bad luck, use the php.ini default or value set in config.php.
159         return self::get_default_php_timezone();
160     }
162     /**
163      * Returns server timezone.
164      * @return string normalised timezone name compatible with PHP
165      **/
166     public static function get_server_timezone() {
167         global $CFG;
169         if (!isset($CFG->timezone) or $CFG->timezone == 99 or $CFG->timezone === '') {
170             return self::get_default_php_timezone();
171         }
173         return self::normalise_timezone($CFG->timezone);
174     }
176     /**
177      * Returns server timezone.
178      * @return DateTimeZone
179      **/
180     public static function get_server_timezone_object() {
181         $tz = self::get_server_timezone();
182         return new DateTimeZone($tz);
183     }
185     /**
186      * Set PHP default timezone to $CFG->timezone.
187      */
188     public static function set_default_server_timezone() {
189         global $CFG;
191         if (!isset($CFG->timezone) or $CFG->timezone == 99 or $CFG->timezone === '') {
192             date_default_timezone_set(self::get_default_php_timezone());
193             return;
194         }
196         $current = date_default_timezone_get();
197         if ($current === $CFG->timezone) {
198             // Nothing to do.
199             return;
200         }
202         if (!isset(self::$goodzones)) {
203             // For better performance try do do this without full tz init,
204             // because this is called from lib/setup.php file on each page.
205             $result = @timezone_open($CFG->timezone); // Ignore error if setting invalid.
206             if ($result !== false) {
207                 date_default_timezone_set($result->getName());
208                 return;
209             }
210         }
212         // Slow way is the last option.
213         date_default_timezone_set(self::get_server_timezone());
214     }
216     /**
217      * Returns user timezone.
218      *
219      * Ideally the parameter should be a real user record,
220      * unfortunately the legacy code is using 99 for both server
221      * and default value.
222      *
223      * Example of using legacy API:
224      *    // Date for other user via legacy API.
225      *    $datestr = userdate($time, core_date::get_user_timezone($user));
226      *
227      * The coding style rules in Moodle are moronic,
228      * why cannot the parameter names have underscores in them?
229      *
230      * @param mixed $userorforcedtz user object or legacy forced timezone string or tz object
231      * @return string normalised timezone name compatible with PHP
232      */
233     public static function get_user_timezone($userorforcedtz = null) {
234         global $USER, $CFG;
236         if ($userorforcedtz instanceof DateTimeZone) {
237             return $userorforcedtz->getName();
238         }
240         if (isset($userorforcedtz) and !is_object($userorforcedtz) and $userorforcedtz != 99) {
241             // Legacy code is forcing timezone in legacy API.
242             return self::normalise_timezone($userorforcedtz);
243         }
245         if (isset($CFG->forcetimezone) and $CFG->forcetimezone != 99) {
246             // Override any user timezone.
247             return self::normalise_timezone($CFG->forcetimezone);
248         }
250         if ($userorforcedtz === null) {
251             $tz = isset($USER->timezone) ? $USER->timezone : 99;
253         } else if (is_object($userorforcedtz)) {
254             $tz = isset($userorforcedtz->timezone) ? $userorforcedtz->timezone : 99;
256         } else {
257             if ($userorforcedtz == 99) {
258                 $tz = isset($USER->timezone) ? $USER->timezone : 99;
259             } else {
260                 $tz = $userorforcedtz;
261             }
262         }
264         if ($tz == 99) {
265             return self::get_server_timezone();
266         }
268         return self::normalise_timezone($tz);
269     }
271     /**
272      * Return user timezone object.
273      *
274      * @param mixed $userorforcedtz
275      * @return DateTimeZone
276      */
277     public static function get_user_timezone_object($userorforcedtz = null) {
278         $tz = self::get_user_timezone($userorforcedtz);
279         return new DateTimeZone($tz);
280     }
282     /**
283      * Return default timezone set in php.ini or config.php.
284      * @return string normalised timezone compatible with PHP
285      */
286     public static function get_default_php_timezone() {
287         if (!isset(self::$defaultphptimezone)) {
288             // This should not happen.
289             self::store_default_php_timezone();
290         }
292         return self::$defaultphptimezone;
293     }
295     /**
296      * To be called from lib/setup.php only!
297      */
298     public static function store_default_php_timezone() {
299         if ((defined('PHPUNIT_TEST') and PHPUNIT_TEST)
300             or defined('BEHAT_SITE_RUNNING') or defined('BEHAT_TEST') or defined('BEHAT_UTIL')) {
301             // We want all test sites to be consistent by default.
302             self::$defaultphptimezone = 'Australia/Perth';
303             return;
304         }
305         if (!isset(self::$defaultphptimezone)) {
306             self::$defaultphptimezone = date_default_timezone_get();
307         }
308     }
310     /**
311      * Do not use directly - use $this->setTimezone('xx', $tz) instead in your test case.
312      * @param string $tz valid timezone name
313      */
314     public static function phpunit_override_default_php_timezone($tz) {
315         if (!defined('PHPUNIT_TEST')) {
316             throw new coding_exception('core_date::phpunit_override_default_php_timezone() must be used only from unit tests');
317         }
318         $result = timezone_open($tz); // This triggers error if $tz invalid.
319         if ($result !== false) {
320             self::$defaultphptimezone = $tz;
321         } else {
322             self::$defaultphptimezone = 'Australia/Perth';
323         }
324     }
326     /**
327      * To be called from phpunit reset only, after restoring $CFG.
328      */
329     public static function phpunit_reset() {
330         global $CFG;
331         if (!defined('PHPUNIT_TEST')) {
332             throw new coding_exception('core_date::phpunit_reset() must be used only from unit tests');
333         }
334         self::store_default_php_timezone();
335         date_default_timezone_set($CFG->timezone);
336     }
338     /**
339      * Initialise timezone arrays, call before use.
340      */
341     protected static function init_zones() {
342         if (isset(self::$goodzones)) {
343             return;
344         }
346         $zones = DateTimeZone::listIdentifiers();
347         self::$goodzones = array_fill_keys($zones, true);
349         $zones = DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC);
350         self::$bczones = array();
351         foreach ($zones as $zone) {
352             if (isset(self::$goodzones[$zone])) {
353                 continue;
354             }
355             self::$bczones[$zone] = true;
356         }
358         self::$badzones = array(
359             // Windows time zones.
360             'Dateline Standard Time' => 'Etc/GMT+12',
361             'Hawaiian Standard Time' => 'Pacific/Honolulu',
362             'Alaskan Standard Time' => 'America/Anchorage',
363             'Pacific Standard Time (Mexico)' => 'America/Santa_Isabel',
364             'Pacific Standard Time' => 'America/Los_Angeles',
365             'US Mountain Standard Time' => 'America/Phoenix',
366             'Mountain Standard Time (Mexico)' => 'America/Chihuahua',
367             'Mountain Standard Time' => 'America/Denver',
368             'Central America Standard Time' => 'America/Guatemala',
369             'Central Standard Time' => 'America/Chicago',
370             'Central Standard Time (Mexico)' => 'America/Mexico_City',
371             'Canada Central Standard Time' => 'America/Regina',
372             'SA Pacific Standard Time' => 'America/Bogota',
373             'Eastern Standard Time' => 'America/New_York',
374             'US Eastern Standard Time' => 'America/Indianapolis',
375             'Venezuela Standard Time' => 'America/Caracas',
376             'Paraguay Standard Time' => 'America/Asuncion',
377             'Atlantic Standard Time' => 'America/Halifax',
378             'Central Brazilian Standard Time' => 'America/Cuiaba',
379             'SA Western Standard Time' => 'America/La_Paz',
380             'Pacific SA Standard Time' => 'America/Santiago',
381             'Newfoundland Standard Time' => 'America/St_Johns',
382             'E. South America Standard Time' => 'America/Sao_Paulo',
383             'Argentina Standard Time' => 'America/Buenos_Aires',
384             'SA Eastern Standard Time' => 'America/Cayenne',
385             'Greenland Standard Time' => 'America/Godthab',
386             'Montevideo Standard Time' => 'America/Montevideo',
387             'Bahia Standard Time' => 'America/Bahia',
388             'Azores Standard Time' => 'Atlantic/Azores',
389             'Cape Verde Standard Time' => 'Atlantic/Cape_Verde',
390             'Morocco Standard Time' => 'Africa/Casablanca',
391             'GMT Standard Time' => 'Europe/London',
392             'Greenwich Standard Time' => 'Atlantic/Reykjavik',
393             'W. Europe Standard Time' => 'Europe/Berlin',
394             'Central Europe Standard Time' => 'Europe/Budapest',
395             'Romance Standard Time' => 'Europe/Paris',
396             'Central European Standard Time' => 'Europe/Warsaw',
397             'W. Central Africa Standard Time' => 'Africa/Lagos',
398             'Namibia Standard Time' => 'Africa/Windhoek',
399             'Jordan Standard Time' => 'Asia/Amman',
400             'GTB Standard Time' => 'Europe/Bucharest',
401             'Middle East Standard Time' => 'Asia/Beirut',
402             'Egypt Standard Time' => 'Africa/Cairo',
403             'Syria Standard Time' => 'Asia/Damascus',
404             'South Africa Standard Time' => 'Africa/Johannesburg',
405             'FLE Standard Time' => 'Europe/Kiev',
406             'Turkey Standard Time' => 'Europe/Istanbul',
407             'Israel Standard Time' => 'Asia/Jerusalem',
408             'Kaliningrad Standard Time' => 'Europe/Kaliningrad',
409             'Libya Standard Time' => 'Africa/Tripoli',
410             'Arabic Standard Time' => 'Asia/Baghdad',
411             'Arab Standard Time' => 'Asia/Riyadh',
412             'Belarus Standard Time' => 'Europe/Minsk',
413             'Russian Standard Time' => 'Europe/Moscow',
414             'E. Africa Standard Time' => 'Africa/Nairobi',
415             'Iran Standard Time' => 'Asia/Tehran',
416             'Arabian Standard Time' => 'Asia/Dubai',
417             'Azerbaijan Standard Time' => 'Asia/Baku',
418             'Russia Time Zone 3' => 'Europe/Samara',
419             'Mauritius Standard Time' => 'Indian/Mauritius',
420             'Georgian Standard Time' => 'Asia/Tbilisi',
421             'Caucasus Standard Time' => 'Asia/Yerevan',
422             'Afghanistan Standard Time' => 'Asia/Kabul',
423             'West Asia Standard Time' => 'Asia/Tashkent',
424             'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg',
425             'Pakistan Standard Time' => 'Asia/Karachi',
426             'India Standard Time' => 'Asia/Kolkata', // PHP and Windows differ in spelling.
427             'Sri Lanka Standard Time' => 'Asia/Colombo',
428             'Nepal Standard Time' => 'Asia/Katmandu',
429             'Central Asia Standard Time' => 'Asia/Almaty',
430             'Bangladesh Standard Time' => 'Asia/Dhaka',
431             'N. Central Asia Standard Time' => 'Asia/Novosibirsk',
432             'Myanmar Standard Time' => 'Asia/Rangoon',
433             'SE Asia Standard Time' => 'Asia/Bangkok',
434             'North Asia Standard Time' => 'Asia/Krasnoyarsk',
435             'China Standard Time' => 'Asia/Shanghai',
436             'North Asia East Standard Time' => 'Asia/Irkutsk',
437             'Singapore Standard Time' => 'Asia/Singapore',
438             'W. Australia Standard Time' => 'Australia/Perth',
439             'Taipei Standard Time' => 'Asia/Taipei',
440             'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar',
441             'Tokyo Standard Time' => 'Asia/Tokyo',
442             'Korea Standard Time' => 'Asia/Seoul',
443             'Yakutsk Standard Time' => 'Asia/Yakutsk',
444             'Cen. Australia Standard Time' => 'Australia/Adelaide',
445             'AUS Central Standard Time' => 'Australia/Darwin',
446             'E. Australia Standard Time' => 'Australia/Brisbane',
447             'AUS Eastern Standard Time' => 'Australia/Sydney',
448             'West Pacific Standard Time' => 'Pacific/Port_Moresby',
449             'Tasmania Standard Time' => 'Australia/Hobart',
450             'Magadan Standard Time' => 'Asia/Magadan',
451             'Vladivostok Standard Time' => 'Asia/Vladivostok',
452             'Russia Time Zone 10' => 'Asia/Srednekolymsk',
453             'Central Pacific Standard Time' => 'Pacific/Guadalcanal',
454             'Russia Time Zone 11' => 'Asia/Kamchatka',
455             'New Zealand Standard Time' => 'Pacific/Auckland',
456             'Fiji Standard Time' => 'Pacific/Fiji',
457             'Tonga Standard Time' => 'Pacific/Tongatapu',
458             'Samoa Standard Time' => 'Pacific/Apia',
459             'Line Islands Standard Time' => 'Pacific/Kiritimati',
461             // A lot more bad legacy time zones.
462             'CET' => 'Europe/Berlin',
463             'Central European Time' => 'Europe/Berlin',
464             'CST' => 'America/Chicago',
465             'Central Time' => 'America/Chicago',
466             'CST6CDT' => 'America/Chicago',
467             'CDT' => 'America/Chicago',
468             'China Time' => 'Asia/Shanghai',
469             'EDT' => 'America/New_York',
470             'EST' => 'America/New_York',
471             'EST5EDT' => 'America/New_York',
472             'Eastern Time' => 'America/New_York',
473             'IST' => 'Asia/Kolkata',
474             'India Time' => 'Asia/Kolkata',
475             'JST' => 'Asia/Tokyo',
476             'Japan Time' => 'Asia/Tokyo',
477             'Japan Standard Time' => 'Asia/Tokyo',
478             'MDT' => 'America/Denver',
479             'MST' => 'America/Denver',
480             'MST7MDT' => 'America/Denver',
481             'PDT' => 'America/Los_Angeles',
482             'PST' => 'America/Los_Angeles',
483             'Pacific Time' => 'America/Los_Angeles',
484             'PST8PDT' => 'America/Los_Angeles',
485             'HST' => 'Pacific/Honolulu',
486             'WET' => 'Europe/London',
487             'EET' => 'Europe/Kiev',
488             'FET' => 'Europe/Minsk',
490             // Some UTC variations.
491             'UTC-01' => 'Etc/GMT+1',
492             'UTC-02' => 'Etc/GMT+2',
493             'UTC-03' => 'Etc/GMT+3',
494             'UTC-04' => 'Etc/GMT+4',
495             'UTC-05' => 'Etc/GMT+5',
496             'UTC-06' => 'Etc/GMT+6',
497             'UTC-07' => 'Etc/GMT+7',
498             'UTC-08' => 'Etc/GMT+8',
499             'UTC-09' => 'Etc/GMT+9',
501             // Some weird GMTs.
502             'Etc/GMT+0' => 'Etc/GMT',
503             'Etc/GMT-0' => 'Etc/GMT',
504             'Etc/GMT0' => 'Etc/GMT',
506             // And lastly some alternative city spelling.
507             'Asia/Calcutta' => 'Asia/Kolkata',
508         );
510         // Legacy GMT fallback.
511         for ($i = -14; $i <= 13; $i++) {
512             $off = abs($i);
513             if ($i < 0) {
514                 $mapto = 'Etc/GMT+' . $off;
515                 $utc = 'UTC-' . $off;
516                 $gmt = 'GMT-' . $off;
517             } else if ($i > 0) {
518                 $mapto = 'Etc/GMT-' . $off;
519                 $utc = 'UTC+' . $off;
520                 $gmt = 'GMT+' . $off;
521             } else {
522                 $mapto = 'Etc/GMT';
523                 $utc = 'UTC';
524                 $gmt = 'GMT';
525             }
526             if (isset(self::$bczones[$mapto])) {
527                 self::$badzones[$i . ''] = $mapto;
528                 self::$badzones[$i . '.0'] = $mapto;
529                 self::$badzones[$utc] = $mapto;
530                 self::$badzones[$gmt] = $mapto;
531             }
532         }
534         // Legacy Moodle half an hour offsets - pick any city nearby, ideally without DST.
535         self::$badzones['-4.5'] = 'America/Caracas';
536         self::$badzones['4.5'] = 'Asia/Kabul';
537         self::$badzones['5.5'] = 'Asia/Kolkata';
538         self::$badzones['6.5'] = 'Asia/Rangoon';
539         self::$badzones['9.5'] = 'Australia/Darwin';
541         // Remove bad zones that are elsewhere.
542         foreach (self::$bczones as $zone => $unused) {
543             if (isset(self::$badzones[$zone])) {
544                 unset(self::$badzones[$zone]);
545             }
546         }
547         foreach (self::$goodzones as $zone => $unused) {
548             if (isset(self::$badzones[$zone])) {
549                 unset(self::$badzones[$zone]);
550             }
551         }
552     }