Commit d6e7a63d authored by Petr Skoda's avatar Petr Skoda
Browse files

MDL-49684 timezones: rewrite timezone support

This patch replaces all homegrown timezone
stuff with standard PHP date/time code.

The main change is the introduction of core_date
class that returns normalised user and server
timezones. From now on nobody should be using
$CFG->timezone or $user->timezone directly!

Other new features and fixes:
* admins are prompted for timezone during install
* editing of other users is finally fixed
* timezones are displayed in user profile
* new $this->setTimezone() in phpunit
* time locale is now automatically reset in phpunit
* timezone is now automatically reset in phpunit
* phpunit has Australia/Perth as default timezone
parent e19b3710
......@@ -121,10 +121,11 @@ $olddir = getcwd();
chdir(dirname($_SERVER['argv'][0]));
// Servers should define a default timezone in php.ini, but if they don't then make sure something is defined.
// This is a quick hack. Ideally we should ask the admin for a value. See MDL-22625 for more on this.
if (function_exists('date_default_timezone_set') and function_exists('date_default_timezone_get')) {
@date_default_timezone_set(@date_default_timezone_get());
if (!function_exists('date_default_timezone_set') or !function_exists('date_default_timezone_get')) {
fwrite(STDERR, "Timezone functions are not available.\n");
exit(1);
}
date_default_timezone_set(@date_default_timezone_get());
// make sure PHP errors are displayed - helps with diagnosing of problems
@error_reporting(E_ALL);
......
......@@ -521,6 +521,7 @@ if (empty($site->shortname)) {
// probably new installation - lets return to frontpage after this step
// remove settings that we want uninitialised
unset_config('registerauth');
unset_config('timezone'); // Force admin to select timezone!
redirect('upgradesettings.php?return=site');
}
......
......@@ -4,11 +4,26 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
// "locations" settingpage
$temp = new admin_settingpage('locationsettings', new lang_string('locationsettings', 'admin'));
$options = get_list_of_timezones();
$options[99] = new lang_string('serverlocaltime');
$temp->add(new admin_setting_configselect('timezone', new lang_string('timezone','admin'), new lang_string('configtimezone', 'admin'), 99, $options));
$current = isset($CFG->timezone) ? $CFG->timezone : null;
$options = core_date::get_list_of_timezones($current, false);
$default = core_date::get_default_php_timezone();
if ($current == 99) {
// Do not show 99 unless it is current value, we want to get rid of it over time.
$options['99'] = new lang_string('timezonephpdefault', 'core_admin', $default);
}
if ($default === 'UTC') {
// Nobody really wants UTC, so instead default selection to the country that is confused by the UTC the most.
$default = 'Europe/London';
}
$temp->add(new admin_setting_configselect('timezone', new lang_string('timezone', 'admin'),
new lang_string('configtimezone', 'admin'), $default, $options));
$options = core_date::get_list_of_timezones(isset($CFG->forcetimezone) ? $CFG->forcetimezone : null, true);
$options[99] = new lang_string('timezonenotforced', 'admin');
$temp->add(new admin_setting_configselect('forcetimezone', new lang_string('forcetimezone', 'admin'), new lang_string('helpforcetimezone', 'admin'), 99, $options));
$temp->add(new admin_setting_configselect('forcetimezone', new lang_string('forcetimezone', 'admin'),
new lang_string('helpforcetimezone', 'admin'), 99, $options));
$temp->add(new admin_settings_country_select('country', new lang_string('country', 'admin'), new lang_string('configcountry', 'admin'), 0));
$temp->add(new admin_setting_configtext('defaultcity', new lang_string('defaultcity', 'admin'), new lang_string('defaultcity_help', 'admin'), ''));
......
......@@ -157,6 +157,7 @@ if ($hassiteconfig
array('description' => new lang_string('description'),
'city' => new lang_string('city'),
'country' => new lang_string('country'),
'timezone' => new lang_string('timezone'),
'webpage' => new lang_string('webpage'),
'icqnumber' => new lang_string('icqnumber'),
'skypeid' => new lang_string('skypeid'),
......
......@@ -41,11 +41,11 @@
}
require_once($CFG->dirroot.'/calendar/lib.php');
$timezones = get_list_of_timezones();
$timezones = core_date::get_list_of_timezones(null, true);
echo '<center><form action="timezone.php" method="post">';
echo html_writer::label($strusers . ' (' . $strall . '): ', 'menuzone');
echo html_writer::select($timezones, "zone", $current, array('99'=>get_string("serverlocaltime")));
echo html_writer::select($timezones, "zone", $current);
echo "<input type=\"hidden\" name=\"sesskey\" value=\"".sesskey()."\" />";
echo '<input type="submit" value="'.s($strsavechanges).'" />';
echo "</form></center>";
......
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Automatic update of Timezones from a new source
*
* @package tool
* @subpackage timezoneimport
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once('../../../config.php');
require_once($CFG->libdir.'/adminlib.php');
require_once($CFG->libdir.'/filelib.php');
require_once($CFG->libdir.'/olson.php');
admin_externalpage_setup('tooltimezoneimport');
$ok = optional_param('ok', 0, PARAM_BOOL);
/// Print headings
$strimporttimezones = get_string('importtimezones', 'tool_timezoneimport');
echo $OUTPUT->header();
echo $OUTPUT->heading($strimporttimezones);
if (!$ok or !confirm_sesskey()) {
$message = '<br /><br />';
$message .= $CFG->tempdir.'/olson.txt<br />';
$message .= $CFG->tempdir.'/timezone.txt<br />';
$message .= '<a href="https://download.moodle.org/timezone/">https://download.moodle.org/timezone/</a><br />';
$message .= '<a href="'.$CFG->wwwroot.'/lib/timezone.txt">'.$CFG->dirroot.'/lib/timezone.txt</a><br />';
$message .= '<br />';
$message = get_string("configintrotimezones", 'tool_timezoneimport', $message);
echo $OUTPUT->confirm($message, 'index.php?ok=1', new moodle_url('/admin/index.php'));
echo $OUTPUT->footer();
exit;
}
/// Try to find a source of timezones to import from
$importdone = false;
/// First, look for an Olson file locally
$source = $CFG->tempdir.'/olson.txt';
if (!$importdone and is_readable($source)) {
if ($timezones = olson_to_timezones($source)) {
update_timezone_records($timezones);
$importdone = $source;
}
}
/// Next, look for a CSV file locally
$source = $CFG->tempdir.'/timezone.txt';
if (!$importdone and is_readable($source)) {
if ($timezones = get_records_csv($source, 'timezone')) {
update_timezone_records($timezones);
$importdone = $source;
}
}
/// Otherwise, let's try moodle.org's copy
$source = 'https://download.moodle.org/timezone/';
if (!$importdone && ($content=download_file_content($source))) {
if ($file = fopen($CFG->tempdir.'/timezone.txt', 'w')) { // Make local copy
fwrite($file, $content);
fclose($file);
if ($timezones = get_records_csv($CFG->tempdir.'/timezone.txt', 'timezone')) { // Parse it
update_timezone_records($timezones);
$importdone = $source;
}
unlink($CFG->tempdir.'/timezone.txt');
}
}
/// Final resort, use the copy included in Moodle
$source = $CFG->dirroot.'/lib/timezone.txt';
if (!$importdone and is_readable($source)) { // Distribution file
if ($timezones = get_records_csv($source, 'timezone')) {
update_timezone_records($timezones);
$importdone = $source;
}
}
/// That's it!
if ($importdone) {
$a = new stdClass();
$a->count = count($timezones);
$a->source = $importdone;
echo $OUTPUT->notification(get_string('importtimezonescount', 'tool_timezoneimport', $a), 'notifysuccess');
echo $OUTPUT->continue_button(new moodle_url('/admin/index.php'));
$timezonelist = array();
foreach ($timezones as $timezone) {
if (is_array($timezone)) {
$timezone = (object)$timezone;
}
if (isset($timezonelist[$timezone->name])) {
$timezonelist[$timezone->name]++;
} else {
$timezonelist[$timezone->name] = 1;
}
}
ksort($timezonelist);
$timezonetable = new html_table();
$timezonetable->head = array(
get_string('timezone', 'moodle'),
get_string('entries', 'moodle')
);
$rows = array();
foreach ($timezonelist as $name => $count) {
$row = new html_table_row(
array(
new html_table_cell($name),
new html_table_cell($count)
)
);
$rows[] = $row;
}
$timezonetable->data = $rows;
echo html_writer::table($timezonetable);
} else {
echo $OUTPUT->notification(get_string('importtimezonesfailed', 'tool_timezoneimport'));
echo $OUTPUT->continue_button(new moodle_url('/admin/index.php'));
}
echo $OUTPUT->footer();
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Strings for component 'tool_timezoneimport', language 'en', branch 'MOODLE_22_STABLE'
*
* @package tool
* @subpackage timezoneimport
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['configintrotimezones'] = 'This page will search for new information about world timezones (including daylight savings time rules) and update your local database with this information. These locations will be checked, in order: {$a} Do you wish to update your timezones now?';
$string['importtimezones'] = 'Update complete list of timezones';
$string['importtimezonescount'] = '{$a->count} entries imported from {$a->source}';
$string['importtimezonesfailed'] = 'No sources found! (Bad news)';
$string['pluginname'] = 'Timezones updater';
$string['updatetimezones'] = 'Update timezones';
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Plugin version info
*
* @package tool
* @subpackage timezoneimport
* @copyright 2011 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
if ($hassiteconfig) {
$ADMIN->add('location', new admin_externalpage('tooltimezoneimport', get_string('updatetimezones', 'tool_timezoneimport'), "$CFG->wwwroot/$CFG->admin/tool/timezoneimport/index.php"));
}
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Plugin version info
*
* @package tool
* @subpackage timezoneimport
* @copyright 2011 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2014111000;
$plugin->requires = 2014110400;
$plugin->component = 'tool_timezoneimport'; // Full name of the plugin (used for diagnostics)
......@@ -255,8 +255,7 @@ class admin_uploaduser_form2 extends moodleform {
}
$mform->setAdvanced('country');
$choices = get_list_of_timezones();
$choices['99'] = get_string('serverlocaltime');
$choices = core_date::get_list_of_timezones($templateuser->timezone, true);
$mform->addElement('select', 'timezone', get_string('timezone'), $choices);
$mform->setDefault('timezone', $templateuser->timezone);
$mform->setAdvanced('timezone');
......
......@@ -47,13 +47,13 @@ class availability_date_condition_testcase extends advanced_testcase {
* Tests constructing and using date condition as part of tree.
*/
public function test_in_tree() {
global $SITE, $USER;
global $SITE, $USER, $CFG;
$this->resetAfterTest();
$this->setAdminUser();
// Set server timezone for test. (Important as otherwise the timezone
// could be anything - this is modified by other unit tests, too.)
date_default_timezone_set('UTC');
$this->setTimezone('UTC');
// SEt user to GMT+5.
$USER->timezone = 5;
......@@ -179,7 +179,10 @@ class availability_date_condition_testcase extends advanced_testcase {
* Tests the get_description and get_standalone_description functions.
*/
public function test_get_description() {
global $SITE;
global $SITE, $CFG;
$this->resetAfterTest();
$this->setTimezone('UTC');
$modinfo = get_fast_modinfo($SITE);
$info = new \core_availability\mock_info();
......
......@@ -114,7 +114,7 @@ abstract class backup_cron_automated_helper {
core_php_time_limit::raise();
raise_memory_limit(MEMORY_EXTRA);
$nextstarttime = backup_cron_automated_helper::calculate_next_automated_backup($admin->timezone, $now);
$nextstarttime = backup_cron_automated_helper::calculate_next_automated_backup(null, $now);
$showtime = "undefined";
if ($nextstarttime > 0) {
$showtime = date('r', $nextstarttime);
......@@ -315,24 +315,24 @@ abstract class backup_cron_automated_helper {
/**
* Works out the next time the automated backup should be run.
*
* @param mixed $timezone user timezone
* @param mixed $ignroedtimezone all settings are in server timezone!
* @param int $now timestamp, should not be in the past, most likely time()
* @return int timestamp of the next execution at server time
*/
public static function calculate_next_automated_backup($timezone, $now) {
public static function calculate_next_automated_backup($ignroedtimezone, $now) {
$result = 0;
$config = get_config('backup');
$autohour = $config->backup_auto_hour;
$automin = $config->backup_auto_minute;
// Gets the user time relatively to the server time.
$date = usergetdate($now, $timezone);
$usertime = mktime($date['hours'], $date['minutes'], $date['seconds'], $date['mon'], $date['mday'], $date['year']);
$diff = $now - $usertime;
$backuptime = new DateTime('@' . $now);
$backuptime->setTimezone(core_date::get_server_timezone_object());
$backuptime->setTime($config->backup_auto_hour, $config->backup_auto_minute);
// Get number of days (from user's today) to execute backups.
$automateddays = substr($config->backup_auto_weekdays, $date['wday']) . $config->backup_auto_weekdays;
while ($backuptime->getTimestamp() < $now) {
$backuptime->add(new DateInterval('P1D'));
}
// Get number of days from backup date to execute backups.
$automateddays = substr($config->backup_auto_weekdays, $backuptime->format('w')) . $config->backup_auto_weekdays;
$daysfromnow = strpos($automateddays, "1");
// Error, there are no days to schedule the backup for.
......@@ -340,25 +340,11 @@ abstract class backup_cron_automated_helper {
return 0;
}
// Checks if the date would happen in the future (of the user).
$userresult = mktime($autohour, $automin, 0, $date['mon'], $date['mday'] + $daysfromnow, $date['year']);
if ($userresult <= $usertime) {
// If not, we skip the first scheduled day, that should fix it.
$daysfromnow = strpos($automateddays, "1", 1);
$userresult = mktime($autohour, $automin, 0, $date['mon'], $date['mday'] + $daysfromnow, $date['year']);
}
// Now we generate the time relative to the server.
$result = $userresult + $diff;
// If that time is past, call the function recursively to obtain the next valid day.
if ($result <= $now) {
// Checking time() in here works, but makes PHPUnit Tests extremely hard to predict.
// $now should never be earlier than time() anyway...
$result = self::calculate_next_automated_backup($timezone, $now + DAYSECS);
if ($daysfromnow > 0) {
$backuptime->add(new DateInterval('P' . $daysfromnow . 'D'));
}
return $result;
return $backuptime->getTimestamp();
}
/**
......
......@@ -32,27 +32,17 @@ require_once($CFG->dirroot . '/backup/util/helper/backup_cron_helper.class.php')
* Unit tests for backup cron helper
*/
class backup_cron_helper_testcase extends advanced_testcase {
/**
* @var String keep system default timezone.
*/
protected $systemdefaulttimezone;
/**
* Setup.
*/
protected function setUp() {
parent::setUp();
$this->systemdefaulttimezone = date_default_timezone_get();
}
/**
* Test {@link backup_cron_automated_helper::calculate_next_automated_backup}.
*/
public function test_next_automated_backup() {
global $CFG;
$this->resetAfterTest();
set_config('backup_auto_active', '1', 'backup');
$this->setTimezone('Australia/Perth');
// Notes
// - backup_auto_weekdays starts on Sunday
// - Tests cannot be done in the past
......@@ -62,7 +52,8 @@ class backup_cron_helper_testcase extends advanced_testcase {
set_config('backup_auto_weekdays', '0010010', 'backup');
set_config('backup_auto_hour', '23', 'backup');
set_config('backup_auto_minute', '0', 'backup');
$timezone = 99;
$timezone = 99; // Ignored, everything is calculated in server timezone!!!
$now = strtotime('next Monday 17:00:00');
$next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
......@@ -96,7 +87,6 @@ class backup_cron_helper_testcase extends advanced_testcase {
set_config('backup_auto_weekdays', '1000001', 'backup');
set_config('backup_auto_hour', '0', 'backup');
set_config('backup_auto_minute', '0', 'backup');
$timezone = 99;
$now = strtotime('next Monday 17:00:00');
$next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
......@@ -130,7 +120,6 @@ class backup_cron_helper_testcase extends advanced_testcase {
set_config('backup_auto_weekdays', '1000000', 'backup');
set_config('backup_auto_hour', '4', 'backup');
set_config('backup_auto_minute', '0', 'backup');
$timezone = 99;
$now = strtotime('next Monday 17:00:00');
$next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
......@@ -164,7 +153,6 @@ class backup_cron_helper_testcase extends advanced_testcase {
set_config('backup_auto_weekdays', '1110111', 'backup');
set_config('backup_auto_hour', '20', 'backup');
set_config('backup_auto_minute', '30', 'backup');
$timezone = 99;
$now = strtotime('next Monday 17:00:00');
$next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
......@@ -198,7 +186,6 @@ class backup_cron_helper_testcase extends advanced_testcase {
set_config('backup_auto_weekdays', '1010101', 'backup');
set_config('backup_auto_hour', '0', 'backup');
set_config('backup_auto_minute', '0', 'backup');
$timezone = 99;
$now = strtotime('next Monday 13:00:00');
$next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
......@@ -232,7 +219,6 @@ class backup_cron_helper_testcase extends advanced_testcase {
set_config('backup_auto_weekdays', '0000000', 'backup');
set_config('backup_auto_hour', '15', 'backup');
set_config('backup_auto_minute', '30', 'backup');
$timezone = 99;
$now = strtotime('next Sunday 13:00:00');
$next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
......@@ -243,239 +229,19 @@ class backup_cron_helper_testcase extends advanced_testcase {
set_config('backup_auto_hour', '20', 'backup');
set_config('backup_auto_minute', '00', 'backup');
$timezone = 99;
date_default_timezone_set('Australia/Perth');
$now = strtotime('18:00:00');
$next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
$this->assertEquals(date('w-20:00'), date('w-H:i', $next));
$timezone = 99;
date_default_timezone_set('Europe/Brussels');
$this->setTimezone('Australia/Perth');
$now = strtotime('18:00:00');
$next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
$this->assertEquals(date('w-20:00'), date('w-H:i', $next));
$timezone = 99;
date_default_timezone_set('America/New_York');
$this->setTimezone('Europe/Brussels');
$now = strtotime('18:00:00');
$next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
$this->assertEquals(date('w-20:00'), date('w-H:i', $next));
// Viva Australia! (UTC+8).
date_default_timezone_set('Australia/Perth');
$this->setTimezone('America/New_York');
$now = strtotime('18:00:00');
$timezone = -10.0; // 12am for the user.
$next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
$this->assertEquals(date('w-14:00', strtotime('tomorrow')), date('w-H:i', $next));
$timezone = -5.0; // 5am for the user.
$next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
$this->assertEquals(date('w-09:00', strtotime('tomorrow')), date('w-H:i', $next));
$timezone = 0.0; // 10am for the user.
$next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
$this->assertEquals(date('w-04:00', strtotime('tomorrow')), date('w-H:i', $next));
$timezone = 3.0; // 1pm for the user.
$next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
$this->assertEquals(date('w-01:00', strtotime('tomorrow')), date('w-H:i', $next));
$timezone = 8.0; // 6pm for the user (same than the server).
$next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
$this->assertEquals(date('w-20:00'), date('w-H:i', $next));
$timezone = 9.0; // 7pm for the user.
$next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
$this->assertEquals(date('w-19:00'), date('w-H:i', $next));
$timezone = 13.0; // 12am for the user.
$next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
$this->assertEquals(date('w-15:00', strtotime('tomorrow')), date('w-H:i', $next));
// Let's have a Belgian beer! (UTC+1 / UTC+2 DST).
// Warning: Some of these tests will fail if executed "around"
// 'Europe/Brussels' DST changes (last Sunday in March and
// last Sunday in October right now - 2012). Once Moodle
// moves to PHP TZ support this could be fixed properly.
date_default_timezone_set('Europe/Brussels');
$now = strtotime('18:00:00');
$dst = date('I', $now);
$timezone = -10.0; // 7am for the user.
$next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
$expected = !$dst ? date('w-07:00', strtotime('tomorrow')) : date('w-08:00', strtotime('tomorrow'));
$this->assertEquals($expected, date('w-H:i', $next));
$timezone = -5.0; // 12pm for the user.