Merge branch 'MDL-49740-locking-bugs' of https://github.com/brendanheywood/moodle
authorDan Poltawski <dan@moodle.com>
Tue, 7 Apr 2015 12:08:41 +0000 (13:08 +0100)
committerDan Poltawski <dan@moodle.com>
Tue, 7 Apr 2015 12:08:41 +0000 (13:08 +0100)
211 files changed:
admin/cli/install.php
admin/index.php
admin/settings/location.php
admin/settings/plugins.php
admin/settings/users.php
admin/timezone.php
admin/tool/timezoneimport/index.php [deleted file]
admin/tool/timezoneimport/lang/en/tool_timezoneimport.php [deleted file]
admin/tool/timezoneimport/settings.php [deleted file]
admin/tool/uploaduser/user_form.php
availability/condition/date/tests/condition_test.php
backup/util/helper/backup_cron_helper.class.php
backup/util/helper/tests/cronhelper_test.php
calendar/classes/type_factory.php
calendar/lib.php
calendar/tests/calendartype_test.php
calendar/tests/rrule_manager_tests.php
calendar/type/gregorian/classes/structure.php
calendar/upgrade.txt
comment/comment.js
comment/tests/externallib_test.php
completion/classes/external.php
completion/tests/externallib_test.php
course/moodleform_mod.php
course/tests/behat/add_activities.feature
enrol/cohort/edit.php
enrol/cohort/edit_form.php
enrol/cohort/lang/en/enrol_cohort.php
enrol/cohort/lib.php
enrol/cohort/tests/cohortlib_test.php [new file with mode: 0644]
enrol/manual/db/services.php
enrol/manual/externallib.php
enrol/manual/lang/en/enrol_manual.php
enrol/manual/tests/externallib_test.php
enrol/self/lang/en/enrol_self.php
enrol/self/lib.php
install.php
lang/en/admin.php
lib/accesslib.php
lib/classes/date.php [new file with mode: 0644]
lib/classes/plugin_manager.php
lib/classes/task/scheduled_task.php
lib/datalib.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/deprecatedlib.php
lib/form/tests/dateselector_test.php
lib/form/tests/datetimeselector_test.php
lib/googleapi.php
lib/moodlelib.php
lib/olson.php [deleted file]
lib/phpunit/classes/advanced_testcase.php
lib/phpunit/classes/util.php
lib/phpunit/tests/advanced_test.php
lib/setup.php
lib/statslib.php
lib/tests/accesslib_test.php
lib/tests/behat/timezone.feature [new file with mode: 0644]
lib/tests/date_legacy_test.php [new file with mode: 0644]
lib/tests/date_test.php [new file with mode: 0644]
lib/tests/fixtures/timezonewindows.xml [new file with mode: 0644]
lib/tests/moodlelib_test.php
lib/tests/scheduled_task_test.php
lib/tests/statslib_test.php
lib/timezone.txt [deleted file]
lib/upgrade.txt
message/externallib.php
message/tests/externallib_test.php
mod/assign/mod_form.php
mod/book/mod_form.php
mod/book/settings.php
mod/chat/lib.php
mod/chat/mod_form.php
mod/choice/mod_form.php
mod/data/mod_form.php
mod/feedback/mod_form.php
mod/folder/mod_form.php
mod/folder/settings.php
mod/forum/lib.php
mod/forum/locallib.php
mod/forum/mod_form.php
mod/forum/tests/maildigest_test.php
mod/glossary/mod_form.php
mod/imscp/mod_form.php
mod/imscp/settings.php
mod/label/mod_form.php
mod/lesson/backup/moodle1/lib.php
mod/lesson/backup/moodle2/backup_lesson_stepslib.php
mod/lesson/backup/moodle2/restore_lesson_stepslib.php
mod/lesson/classes/event/group_override_created.php [new file with mode: 0644]
mod/lesson/classes/event/group_override_deleted.php [new file with mode: 0644]
mod/lesson/classes/event/group_override_updated.php [new file with mode: 0644]
mod/lesson/classes/event/user_override_created.php [new file with mode: 0644]
mod/lesson/classes/event/user_override_deleted.php [new file with mode: 0644]
mod/lesson/classes/event/user_override_updated.php [new file with mode: 0644]
mod/lesson/classes/group_observers.php [new file with mode: 0644]
mod/lesson/continue.php
mod/lesson/db/access.php
mod/lesson/db/events.php [moved from admin/tool/timezoneimport/version.php with 58% similarity]
mod/lesson/db/install.xml
mod/lesson/db/upgrade.php
mod/lesson/essay.php
mod/lesson/lang/en/lesson.php
mod/lesson/lib.php
mod/lesson/locallib.php
mod/lesson/mediafile.php
mod/lesson/mod_form.php
mod/lesson/override_form.php [new file with mode: 0644]
mod/lesson/overridedelete.php [new file with mode: 0644]
mod/lesson/overrideedit.php [new file with mode: 0644]
mod/lesson/overrides.php [new file with mode: 0644]
mod/lesson/report.php
mod/lesson/settings.php
mod/lesson/tests/behat/lesson_course_reset.feature [new file with mode: 0644]
mod/lesson/tests/behat/lesson_group_override.feature [new file with mode: 0644]
mod/lesson/tests/behat/lesson_user_override.feature [new file with mode: 0644]
mod/lesson/tests/events_test.php
mod/lesson/version.php
mod/lesson/view.php
mod/lti/mod_form.php
mod/page/mod_form.php
mod/page/settings.php
mod/quiz/attemptlib.php
mod/quiz/backup/moodle2/backup_quiz_stepslib.php
mod/quiz/backup/moodle2/restore_quiz_stepslib.php
mod/quiz/classes/output/edit_renderer.php
mod/quiz/classes/structure.php
mod/quiz/db/install.xml
mod/quiz/db/upgrade.php
mod/quiz/edit.php
mod/quiz/edit_rest.php
mod/quiz/lang/en/quiz.php
mod/quiz/lib.php
mod/quiz/locallib.php
mod/quiz/mod_form.php
mod/quiz/renderer.php
mod/quiz/repaginate.php
mod/quiz/settings.php
mod/quiz/styles.css
mod/quiz/tests/attempt_test.php
mod/quiz/tests/behat/attempt_basic.feature
mod/quiz/tests/behat/attempt_require_previous.feature
mod/quiz/tests/behat/backup.feature [new file with mode: 0644]
mod/quiz/tests/behat/behat_mod_quiz.php
mod/quiz/tests/behat/editing_click_delete_icon.feature [deleted file]
mod/quiz/tests/behat/editing_move_by_click.feature [moved from mod/quiz/tests/behat/editing_click_move_icon.feature with 85% similarity]
mod/quiz/tests/behat/editing_remove_question.feature [new file with mode: 0644]
mod/quiz/tests/behat/editing_require_previous.feature
mod/quiz/tests/behat/editing_section_headings.feature [new file with mode: 0644]
mod/quiz/tests/fixtures/moodle_28_quiz.mbz [new file with mode: 0644]
mod/quiz/tests/generator/lib.php
mod/quiz/tests/structure_test.php
mod/quiz/upgrade.txt
mod/quiz/version.php
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-debug.js
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-min.js
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop.js
mod/quiz/yui/build/moodle-mod_quiz-modform/moodle-mod_quiz-modform-debug.js
mod/quiz/yui/build/moodle-mod_quiz-modform/moodle-mod_quiz-modform-min.js
mod/quiz/yui/build/moodle-mod_quiz-modform/moodle-mod_quiz-modform.js
mod/quiz/yui/build/moodle-mod_quiz-quizbase/moodle-mod_quiz-quizbase-debug.js
mod/quiz/yui/build/moodle-mod_quiz-quizbase/moodle-mod_quiz-quizbase-min.js
mod/quiz/yui/build/moodle-mod_quiz-quizbase/moodle-mod_quiz-quizbase.js
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-debug.js
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-min.js
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes.js
mod/quiz/yui/build/moodle-mod_quiz-util-page/moodle-mod_quiz-util-page-debug.js
mod/quiz/yui/build/moodle-mod_quiz-util-page/moodle-mod_quiz-util-page-min.js
mod/quiz/yui/build/moodle-mod_quiz-util-page/moodle-mod_quiz-util-page.js
mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-debug.js
mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-min.js
mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot.js
mod/quiz/yui/src/dragdrop/js/dragdrop.js
mod/quiz/yui/src/dragdrop/js/resource.js
mod/quiz/yui/src/dragdrop/js/section.js
mod/quiz/yui/src/modform/js/modform.js
mod/quiz/yui/src/quizbase/js/quizbase.js
mod/quiz/yui/src/toolboxes/js/resource.js
mod/quiz/yui/src/toolboxes/js/section.js
mod/quiz/yui/src/toolboxes/js/toolbox.js
mod/quiz/yui/src/util/js/page.js
mod/quiz/yui/src/util/js/slot.js
mod/resource/mod_form.php
mod/resource/settings.php
mod/scorm/lib.php
mod/scorm/mod_form.php
mod/survey/mod_form.php
mod/url/locallib.php
mod/url/mod_form.php
mod/url/settings.php
mod/wiki/mod_form.php
mod/workshop/lib.php
mod/workshop/mod_form.php
notes/externallib.php
notes/index.php
notes/lib.php
notes/tests/externallib_test.php
portfolio/googledocs/lib.php
theme/base/style/course.css
user/edit.php
user/edit_form.php
user/editadvanced.php
user/editadvanced_form.php
user/editlib.php
user/externallib.php
user/index.php
user/lib.php
user/profile.php
user/tests/userlib_test.php
version.php

index 8ee0905..6b8e88f 100644 (file)
@@ -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);
index 8df4edf..b263903 100644 (file)
@@ -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');
 }
 
index 607090a..3666409 100644 (file)
@@ -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'), ''));
 
index f0455b7..248b0bc 100644 (file)
@@ -32,6 +32,13 @@ if ($hassiteconfig) {
 
     // activity modules
     $ADMIN->add('modules', new admin_category('modsettings', new lang_string('activitymodules')));
+
+    $temp = new admin_settingpage('managemodulescommon', new lang_string('commonsettings', 'admin'));
+
+    $temp->add(new admin_setting_configcheckbox('requiremodintro',
+        get_string('requiremodintro', 'admin'), get_string('requiremodintro_desc', 'admin'), 0));
+    $ADMIN->add('modsettings', $temp);
+
     $ADMIN->add('modsettings', new admin_page_managemods());
     foreach (core_plugin_manager::instance()->get_plugins_of_type('mod') as $plugin) {
         /** @var \core\plugininfo\mod $plugin */
index f8d5b74..b2c1d23 100644 (file)
@@ -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'),
index 546b021..8e603a6 100644 (file)
     }
 
     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>";
diff --git a/admin/tool/timezoneimport/index.php b/admin/tool/timezoneimport/index.php
deleted file mode 100644 (file)
index d6b6571..0000000
+++ /dev/null
@@ -1,157 +0,0 @@
-<?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();
-
-
diff --git a/admin/tool/timezoneimport/lang/en/tool_timezoneimport.php b/admin/tool/timezoneimport/lang/en/tool_timezoneimport.php
deleted file mode 100644 (file)
index bf3a5ad..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-<?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';
diff --git a/admin/tool/timezoneimport/settings.php b/admin/tool/timezoneimport/settings.php
deleted file mode 100644 (file)
index 2a2cfc2..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-<?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"));
-}
-
index 1ec16e0..2e919fd 100644 (file)
@@ -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');
index 425f145..34e4314 100644 (file)
@@ -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();
index 2834f91..18524bf 100644 (file)
@@ -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();
     }
 
     /**
index 915b2f8..cb88d53 100644 (file)
@@ -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.
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $expected = !$dst ? date('w-02:00', strtotime('tomorrow')) : date('w-03:00', strtotime('tomorrow'));
-        $this->assertEquals($expected, date('w-H:i', $next));
-
-        $timezone = 0.0;  // 5pm for the user.
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $expected = !$dst ? date('w-21:00') : date('w-22:00');
-        $this->assertEquals($expected, date('w-H:i', $next));
-
-        $timezone = 3.0; // 8pm for the user (note the expected time is today while in DST).
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $expected = !$dst ? date('w-18:00', strtotime('tomorrow')) : date('w-19:00');
-        $this->assertEquals($expected, date('w-H:i', $next));
-
-        $timezone = 8.0; // 1am for the user.
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $expected = !$dst ? date('w-13:00', strtotime('tomorrow')) : date('w-14:00', strtotime('tomorrow'));
-        $this->assertEquals($expected, date('w-H:i', $next));
-
-        $timezone = 9.0; // 2am for the user.
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $expected = !$dst ? date('w-12:00', strtotime('tomorrow')) : date('w-13:00', strtotime('tomorrow'));
-        $this->assertEquals($expected, date('w-H:i', $next));
-
-        $timezone = 13.0; // 6am for the user.
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $expected = !$dst ? date('w-08:00', strtotime('tomorrow')) : date('w-09:00', strtotime('tomorrow'));
-        $this->assertEquals($expected, date('w-H:i', $next));
-
-        // The big apple! (UTC-5 / UTC-4 DST).
-        // Warning: Some of these tests will fail if executed "around"
-        // 'America/New_York' DST changes (2nd Sunday in March and
-        // 1st Sunday in November right now - 2012). Once Moodle
-        // moves to PHP TZ support this could be fixed properly.
-        date_default_timezone_set('America/New_York');
-        $now = strtotime('18:00:00');
-        $dst = date('I', $now);
-
-        $timezone = -10.0; // 1pm for the user.
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $expected = !$dst ? date('w-01:00', strtotime('tomorrow')) : date('w-02:00', strtotime('tomorrow'));
-        $this->assertEquals($expected, date('w-H:i', $next));
-
-        $timezone = -5.0; // 6pm for the user (server time).
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $expected = !$dst ? date('w-20:00') : date('w-21:00');
-        $this->assertEquals($expected, date('w-H:i', $next));
-
-        $timezone = 0.0;  // 11pm for the user.
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $expected = !$dst ? date('w-15:00', strtotime('tomorrow')) : date('w-16:00', strtotime('tomorrow'));
-        $this->assertEquals($expected, date('w-H:i', $next));
-
-        $timezone = 3.0; // 2am for the user.
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $expected = !$dst ? date('w-12:00', strtotime('tomorrow')) : date('w-13:00', strtotime('tomorrow'));
-        $this->assertEquals($expected, date('w-H:i', $next));
-
-        $timezone = 8.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 = 9.0; // 8am for the user.
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $expected = !$dst ? date('w-06:00', strtotime('tomorrow')) : date('w-07:00', strtotime('tomorrow'));
-        $this->assertEquals($expected, date('w-H:i', $next));
-
-        $timezone = 13.0; // 6am for the user.
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $expected = !$dst ? date('w-02:00', strtotime('tomorrow')) : date('w-03:00', strtotime('tomorrow'));
-        $this->assertEquals($expected, date('w-H:i', $next));
-
-        // Some more timezone tests
-        set_config('backup_auto_weekdays', '0100001', 'backup');
-        set_config('backup_auto_hour', '20', 'backup');
-        set_config('backup_auto_minute', '00', 'backup');
-
-        // Note: These tests should not fail because they are "unnafected"
-        // by DST changes, as far as execution always happens on Monday and
-        // Saturday and those week days are not, right now, the ones rulez
-        // to peform the DST changes (Sunday is). This may change if rules
-        // are modified in the future.
-        date_default_timezone_set('Europe/Brussels');
-        $now = strtotime('next Monday 18:00:00');
-        $dst = date('I', $now);
-
-        $timezone = -12.0;  // 1pm for the user.
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $expected = !$dst ? '2-09:00' : '2-10:00';
-        $this->assertEquals($expected, date('w-H:i', $next));
-
-        $timezone = -4.0;  // 1pm for the user.
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $expected = !$dst ? '2-01:00' : '2-02:00';
-        $this->assertEquals($expected, date('w-H:i', $next));
-
-        $timezone = 0.0;  // 5pm for the user.
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $expected = !$dst ? '1-21:00' : '1-22:00';
-        $this->assertEquals($expected, date('w-H:i', $next));
-
-        $timezone = 2.0;  // 7pm for the user.
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $expected = !$dst ? '1-19:00' : '1-20:00';
-        $this->assertEquals($expected, date('w-H:i', $next));
-
-        $timezone = 4.0;  // 9pm for the user.
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $expected = !$dst ? '6-17:00' : '6-18:00';
-        $this->assertEquals($expected, date('w-H:i', $next));
-
-        $timezone = 12.0;  // 6am for the user.
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $expected = !$dst ? '6-09:00' : '6-10:00';
-        $this->assertEquals($expected, date('w-H:i', $next));
-
-        // Some more timezone tests
-        set_config('backup_auto_weekdays', '0100001', 'backup');
-        set_config('backup_auto_hour', '02', 'backup');
-        set_config('backup_auto_minute', '00', 'backup');
-
-        // Note: These tests should not fail because they are "unnafected"
-        // by DST changes, as far as execution always happens on Monday and
-        // Saturday and those week days are not, right now, the ones rulez
-        // to peform the DST changes (Sunday is). This may change if rules
-        // are modified in the future.
-        date_default_timezone_set('America/New_York');
-        $now = strtotime('next Monday 04:00:00');
-        $dst = date('I', $now);
-
-        $timezone = -12.0;  // 8pm for the user.
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $expected = !$dst ? '1-09:00' : '1-10:00';
-        $this->assertEquals($expected, date('w-H:i', $next));
-
-        $timezone = -4.0;  // 4am for the user.
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $expected = !$dst ? '6-01:00' : '6-02:00';
-        $this->assertEquals($expected, date('w-H:i', $next));
-
-        $timezone = 0.0;  // 8am for the user.
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $expected = !$dst ? '5-21:00' : '5-22:00';
-        $this->assertEquals($expected, date('w-H:i', $next));
-
-        $timezone = 2.0;  // 10am for the user.
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $expected = !$dst ? '5-19:00' : '5-20:00';
-        $this->assertEquals($expected, date('w-H:i', $next));
-
-        $timezone = 4.0;  // 12pm for the user.
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $expected = !$dst ? '5-17:00' : '5-18:00';
-        $this->assertEquals($expected, date('w-H:i', $next));
-
-        $timezone = 12.0;  // 8pm for the user.
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $expected = !$dst ? '5-09:00' : '5-10:00';
-        $this->assertEquals($expected, date('w-H:i', $next));
-
-    }
-
-    /**
-     * Set timezone back to default.
-     */
-    protected function tearDown() {
-        date_default_timezone_set($this->systemdefaulttimezone);
-        parent::tearDown();
     }
 }
index 8a47a84..3c92ede 100644 (file)
@@ -31,8 +31,8 @@ class type_factory {
      * Returns an instance of the currently used calendar type.
      *
      * @param string|null $type the calendar type to use, if none provided use logic to determine
-     * @return calendartype_* the created calendar_type class
-     * @throws coding_exception if the calendar type file could not be loaded
+     * @return \core_calendar\type_base the created calendar_type class
+     * @throws \coding_exception if the calendar type file could not be loaded
      */
     public static function get_calendar_instance($type = null) {
         if (is_null($type)) {
index 605c328..d0b5bf3 100644 (file)
@@ -1913,54 +1913,6 @@ function calendar_add_event_allowed($event) {
     }
 }
 
-/**
- * Convert region timezone to php supported timezone
- *
- * @param string $tz value from ical file
- * @return string $tz php supported timezone
- */
-function calendar_normalize_tz($tz) {
-    switch ($tz) {
-        case('CST'):
-        case('Central Time'):
-        case('Central Standard Time'):
-            $tz = 'America/Chicago';
-            break;
-        case('CET'):
-        case('Central European Time'):
-            $tz = 'Europe/Berlin';
-            break;
-        case('EST'):
-        case('Eastern Time'):
-        case('Eastern Standard Time'):
-            $tz = 'America/New_York';
-            break;
-        case('PST'):
-        case('Pacific Time'):
-        case('Pacific Standard Time'):
-            $tz = 'America/Los_Angeles';
-            break;
-        case('China Time'):
-        case('China Standard Time'):
-            $tz = 'Asia/Beijing';
-            break;
-        case('IST'):
-        case('India Time'):
-        case('India Standard Time'):
-            $tz = 'Asia/New_Delhi';
-            break;
-        case('JST');
-        case('Japan Time'):
-        case('Japan Standard Time'):
-            $tz = 'Asia/Tokyo';
-            break;
-        case('Romance Standard Time'):
-            $tz = 'Europe/Brussels';
-            break;
-    }
-    return $tz;
-}
-
 /**
  * Manage calendar events
  *
@@ -2329,9 +2281,13 @@ class calendar_event {
                 $eventcopy = clone($this->properties);
                 unset($eventcopy->id);
 
+                $timestart = new DateTime('@' . $eventcopy->timestart);
+                $timestart->setTimezone(core_date::get_user_timezone_object());
+
                 for($i = 1; $i < $eventcopy->repeats; $i++) {
 
-                    $eventcopy->timestart = ($eventcopy->timestart+WEEKSECS) + dst_offset_on($eventcopy->timestart) - dst_offset_on($eventcopy->timestart+WEEKSECS);
+                    $timestart->add(new DateInterval('P7D'));
+                    $eventcopy->timestart = $timestart->getTimestamp();
 
                     // Get the event id for the log record.
                     $eventcopyid = $DB->insert_record('event', $eventcopy);
@@ -2988,17 +2944,16 @@ function calendar_add_icalendar_event($event, $courseid, $subscriptionid, $timez
         return 0;
     }
 
-    $defaulttz = date_default_timezone_get();
     $tz = isset($event->properties['DTSTART'][0]->parameters['TZID']) ? $event->properties['DTSTART'][0]->parameters['TZID'] :
             $timezone;
-    $tz = calendar_normalize_tz($tz);
+    $tz = core_date::normalise_timezone($tz);
     $eventrecord->timestart = strtotime($event->properties['DTSTART'][0]->value . ' ' . $tz);
     if (empty($event->properties['DTEND'])) {
         $eventrecord->timeduration = 0; // no duration if no end time specified
     } else {
         $endtz = isset($event->properties['DTEND'][0]->parameters['TZID']) ? $event->properties['DTEND'][0]->parameters['TZID'] :
                 $timezone;
-        $endtz = calendar_normalize_tz($endtz);
+        $endtz = core_date::normalise_timezone($endtz);
         $eventrecord->timeduration = strtotime($event->properties['DTEND'][0]->value . ' ' . $endtz) - $eventrecord->timestart;
     }
 
@@ -3010,7 +2965,7 @@ function calendar_add_icalendar_event($event, $courseid, $subscriptionid, $timez
             // This event should be an all day event.
             $eventrecord->timeduration = 0;
         }
-        date_default_timezone_set($defaulttz);
+        core_date::set_default_server_timezone();
     }
 
     $eventrecord->uuid = $event->properties['UID'][0]->value;
index 96981f2..c124477 100644 (file)
@@ -102,9 +102,15 @@ class core_calendar_type_testcase extends advanced_testcase {
      * different calendar types.
      */
     public function test_calendar_type_dateselector_elements() {
+        global $CFG;
+
         // We want to reset the test data after this run.
         $this->resetAfterTest();
 
+        $this->setTimezone('UTC');
+
+        // Note: this test is pretty useless because it does not test current user timezones.
+
         // Check converting dates to Gregorian when submitting a date selector element works. Note: the test
         // calendar is 2 years, 2 months, 2 days, 2 hours and 2 minutes ahead of the Gregorian calendar.
         $date1 = array();
index aab10ae..5232ab7 100644 (file)
@@ -27,17 +27,14 @@ class core_calendar_rrule_manager_testcase extends advanced_testcase {
     /** @var stdClass a dummy event */
     protected $event;
 
-    /** @var string system timezone */
-    protected $tz;
-
     /**
      * Set up method.
      */
     protected function setUp() {
-        global $DB;
+        global $DB, $CFG;
         $this->resetAfterTest();
-        $this->tz = date_default_timezone_get();
-        date_default_timezone_set('Australia/Perth');
+
+        $this->setTimezone('Australia/Perth');
 
         $user = $this->getDataGenerator()->create_user();
         $sub = new stdClass();
@@ -65,13 +62,6 @@ class core_calendar_rrule_manager_testcase extends advanced_testcase {
         $this->event = $eventobj;
     }
 
-    /**
-     * Tear down method.
-     */
-    protected function tearDown() {
-        date_default_timezone_set($this->tz);
-    }
-
     /**
      * Test parse_rrule() method.
      */
@@ -542,4 +532,4 @@ class core_tests_calendar_rrule_manager extends \core_calendar\rrule_manager{
         }
         throw new coding_exception('invalidproperty');
     }
-}
\ No newline at end of file
+}
index 950d859..cfcabe9 100644 (file)
@@ -68,8 +68,11 @@ class structure extends type_base {
     public function get_months() {
         $months = array();
 
+        $date = new \DateTime('@1263556800');
+        $date->setTimezone(new \DateTimeZone('UTC'));
         for ($i = 1; $i <= 12; $i++) {
-            $months[$i] = userdate(gmmktime(12, 0, 0, $i, 15, 2000), '%B');
+            $date->setDate(2000, $i, 15);
+            $months[$i] = userdate($date->getTimestamp(), '%B', 'UTC');
         }
 
         return $months;
@@ -320,38 +323,21 @@ class structure extends type_base {
             $format = $formatnohour;
         }
 
-        // Add daylight saving offset for string timezones only, as we can't get dst for
-        // float values. if timezone is 99 (user default timezone), then try update dst.
-        if ((99 == $timezone) || !is_numeric($timezone)) {
-            $time += dst_offset_on($time, $timezone);
-        }
+        $time = (int)$time; // Moodle allows rubbish in input...
+        $datestring = date_format_string($time, $format, $timezone);
 
-        $timezone = get_user_timezone_offset($timezone);
-
-        // If we are running under Windows convert to windows encoding and then back to UTF-8
-        // (because it's impossible to specify UTF-8 to fetch locale info in Win32).
-        if (abs($timezone) > 13) { // Server time.
-            $datestring = date_format_string($time, $format, $timezone);
-            if ($fixday) {
-                $daystring  = ltrim(str_replace(array(' 0', ' '), '', strftime(' %d', $time)));
-                $datestring = str_replace('DD', $daystring, $datestring);
-            }
-            if ($fixhour) {
-                $hourstring = ltrim(str_replace(array(' 0', ' '), '', strftime(' %I', $time)));
-                $datestring = str_replace('HH', $hourstring, $datestring);
-            }
-        } else {
-            $time += (int)($timezone * 3600);
-            $datestring = date_format_string($time, $format, $timezone);
-            if ($fixday) {
-                $daystring  = ltrim(str_replace(array(' 0', ' '), '', gmstrftime(' %d', $time)));
-                $datestring = str_replace('DD', $daystring, $datestring);
-            }
-            if ($fixhour) {
-                $hourstring = ltrim(str_replace(array(' 0', ' '), '', gmstrftime(' %I', $time)));
-                $datestring = str_replace('HH', $hourstring, $datestring);
-            }
+        date_default_timezone_set(\core_date::get_user_timezone($timezone));
+
+        if ($fixday) {
+            $daystring  = ltrim(str_replace(array(' 0', ' '), '', strftime(' %d', $time)));
+            $datestring = str_replace('DD', $daystring, $datestring);
         }
+        if ($fixhour) {
+            $hourstring = ltrim(str_replace(array(' 0', ' '), '', strftime(' %I', $time)));
+            $datestring = str_replace('HH', $hourstring, $datestring);
+        }
+
+        \core_date::set_default_server_timezone();
 
         return $datestring;
     }
index a5d31af..55d272a 100644 (file)
@@ -5,6 +5,7 @@ information provided here is intended especially for developers.
 default values changes in code:
 * core_calendar_external::get_calendar_events_parameters() 'timeend' default option changed; now, by default,
   all events are returned, not only the past ones.
+* calendar types need to be updated to be compatible with standard PHP date/time code
 
 === 2.5 ===
 required changes in code:
index 90c767d..91f87a4 100644 (file)
@@ -65,6 +65,12 @@ M.core_comment = {
                 var scope = this;
                 var value = ta.get('value');
                 if (value && value != M.util.get_string('addcomment', 'moodle')) {
+                    ta.set('disabled', true);
+                    ta.setStyles({
+                        'backgroundImage': 'url(' + M.util.image_url('i/loading_small', 'core') + ')',
+                        'backgroundRepeat': 'no-repeat',
+                        'backgroundPosition': 'center center'
+                    });
                     var params = {'content': value};
                     this.request({
                         action: 'add',
@@ -75,6 +81,8 @@ M.core_comment = {
                             var cid = scope.client_id;
                             var ta = Y.one('#dlg-content-'+cid);
                             ta.set('value', '');
+                            ta.set('disabled', false);
+                            ta.setStyle('backgroundImage', 'none');
                             scope.toggle_textarea(false);
                             var container = Y.one('#comment-list-'+cid);
                             var result = scope.render([obj], true);
index d977eb5..c6a7433 100644 (file)
@@ -106,6 +106,7 @@ class core_comment_externallib_testcase extends externallib_advanced_testcase {
         $cmtid1 = $DB->insert_record('comments', $newcmt);
 
         $newcmt->content  = 'New comment 2';
+        $newcmt->timecreated  = time() + 1;
         $cmtid2 = $DB->insert_record('comments', $newcmt);
 
         $contextlevel = 'module';
@@ -126,7 +127,7 @@ class core_comment_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals($user->id, $result['comments'][0]['userid']);
         $this->assertEquals($user->id, $result['comments'][1]['userid']);
 
-        $this->assertEquals($cmtid1, $result['comments'][0]['id']);
-        $this->assertEquals($cmtid2, $result['comments'][1]['id']);
+        $this->assertEquals($cmtid2, $result['comments'][0]['id']);
+        $this->assertEquals($cmtid1, $result['comments'][1]['id']);
     }
 }
index 5881014..168d34d 100644 (file)
@@ -234,4 +234,147 @@ class core_completion_external extends external_api {
             )
         );
     }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 2.9
+     */
+    public static function get_course_completion_status_parameters() {
+        return new external_function_parameters(
+            array(
+                'courseid' => new external_value(PARAM_INT, 'Course ID'),
+                'userid'   => new external_value(PARAM_INT, 'User ID'),
+            )
+        );
+    }
+    /**
+     * Get Course completion status
+     *
+     * @param int $courseid ID of the Course
+     * @param int $userid ID of the User
+     * @return array of course completion status and warnings
+     * @since Moodle 2.9
+     * @throws moodle_exception
+     */
+    public static function get_course_completion_status($courseid, $userid) {
+        global $CFG, $USER;
+        require_once($CFG->libdir . '/grouplib.php');
+
+        $warnings = array();
+        $arrayparams = array(
+            'courseid' => $courseid,
+            'userid'   => $userid,
+        );
+        $params = self::validate_parameters(self::get_course_completion_status_parameters(), $arrayparams);
+
+        $course = get_course($params['courseid']);
+        $user = core_user::get_user($params['userid'], 'id', MUST_EXIST);
+        $context = context_course::instance($course->id);
+        self::validate_context($context);
+
+        // Can current user see user's course completion status?
+        // This check verifies if completion is enabled because $course is mandatory.
+        if (!completion_can_view_data($user->id, $course)) {
+            throw new moodle_exception('cannotviewreport');
+        }
+
+        // The previous function doesn't check groups.
+        if ($user->id != $USER->id) {
+            if (!groups_user_groups_visible($course, $user->id)) {
+                // We are not in the same group!
+                throw new moodle_exception('accessdenied', 'admin');
+            }
+        }
+
+        $info = new completion_info($course);
+
+        // Check this user is enroled.
+        if (!$info->is_tracked_user($user->id)) {
+            if ($USER->id == $user->id) {
+                throw new moodle_exception('notenroled', 'completion');
+            } else {
+                throw new moodle_exception('usernotenroled', 'completion');
+            }
+        }
+
+        $completions = $info->get_completions($user->id);
+        if (empty($completions)) {
+            throw new moodle_exception('err_nocriteria', 'completion');
+        }
+
+        // Load course completion.
+        $completionparams = array(
+            'userid' => $user->id,
+            'course' => $course->id,
+        );
+        $ccompletion = new completion_completion($completionparams);
+
+        $completionrows = array();
+        // Loop through course criteria.
+        foreach ($completions as $completion) {
+            $criteria = $completion->get_criteria();
+
+            $completionrow = array();
+            $completionrow['type'] = $criteria->criteriatype;
+            $completionrow['title'] = $criteria->get_title();
+            $completionrow['status'] = $completion->get_status();
+            $completionrow['complete'] = $completion->is_complete();
+            $completionrow['timecompleted'] = $completion->timecompleted;
+            $completionrow['details'] = $criteria->get_details($completion);
+            $completionrows[] = $completionrow;
+        }
+
+        $result = array(
+                  'completed'   => $info->is_course_complete($user->id),
+                  'aggregation' => $info->get_aggregation_method(),
+                  'completions' => $completionrows
+        );
+
+        $results = array(
+            'completionstatus' => $result,
+            'warnings' => $warnings
+        );
+        return $results;
+
+    }
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 2.9
+     */
+    public static function get_course_completion_status_returns() {
+        return new external_single_structure(
+            array(
+                'completionstatus' => new external_single_structure(
+                    array(
+                        'completed'     => new external_value(PARAM_BOOL, 'true if the course is complete, false otherwise'),
+                        'aggregation'   => new external_value(PARAM_INT, 'aggregation method 1 means all, 2 means any'),
+                        'completions'   => new external_multiple_structure(
+                            new external_single_structure(
+                            array(
+                                 'type'          => new external_value(PARAM_INT,   'Completion criteria type'),
+                                 'title'         => new external_value(PARAM_TEXT,  'Completion criteria Title'),
+                                 'status'        => new external_value(PARAM_NOTAGS, 'Completion status (Yes/No) a % or number'),
+                                 'complete'      => new external_value(PARAM_BOOL,   'Completion status (true/false)'),
+                                 'timecompleted' => new external_value(PARAM_INT,   'Timestamp for criteria completetion'),
+                                 'details' => new external_single_structure(
+                                     array(
+                                         'type' => new external_value(PARAM_TEXT, 'Type description'),
+                                         'criteria' => new external_value(PARAM_RAW, 'Criteria description'),
+                                         'requirement' => new external_value(PARAM_TEXT, 'Requirement description'),
+                                         'status' => new external_value(PARAM_TEXT, 'Status description'),
+                                         ), 'details'),
+                                 ), 'Completions'
+                            ), ''
+                         )
+                    ), 'Course status'
+                ),
+                'warnings' => new external_warnings()
+            ), 'Course completion status'
+        );
+    }
+
 }
index c71f315..c16b29c 100644 (file)
@@ -186,4 +186,135 @@ class core_completion_externallib_testcase extends externallib_advanced_testcase
         $this->assertCount(3, $result['statuses']);
     }
 
+    /**
+     * Test get_course_completion_status
+     */
+    public function test_get_course_completion_status() {
+        global $DB, $CFG, $COMPLETION_CRITERIA_TYPES;
+        require_once($CFG->dirroot.'/completion/criteria/completion_criteria_self.php');
+        require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php');
+        require_once($CFG->dirroot.'/completion/criteria/completion_criteria_unenrol.php');
+        require_once($CFG->dirroot.'/completion/criteria/completion_criteria_activity.php');
+        require_once($CFG->dirroot.'/completion/criteria/completion_criteria_duration.php');
+        require_once($CFG->dirroot.'/completion/criteria/completion_criteria_grade.php');
+        require_once($CFG->dirroot.'/completion/criteria/completion_criteria_role.php');
+        require_once($CFG->dirroot.'/completion/criteria/completion_criteria_course.php');
+
+        $this->resetAfterTest(true);
+
+        $CFG->enablecompletion = true;
+        $student = $this->getDataGenerator()->create_user();
+        $teacher = $this->getDataGenerator()->create_user();
+
+        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1,
+                                                                    'groupmode' => SEPARATEGROUPS,
+                                                                    'groupmodeforce' => 1));
+
+        $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id),
+                                                             array('completion' => 1));
+        $forum = $this->getDataGenerator()->create_module('forum',  array('course' => $course->id),
+                                                             array('completion' => 1));
+        $assign = $this->getDataGenerator()->create_module('assign',  array('course' => $course->id));
+
+        $cmdata = get_coursemodule_from_id('data', $data->cmid);
+        $cmforum = get_coursemodule_from_id('forum', $forum->cmid);
+
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
+        $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
+        $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
+
+        $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+        $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+        // Teacher and student in different groups initially.
+        groups_add_member($group1->id, $student->id);
+        groups_add_member($group2->id, $teacher->id);
+
+        // Set completion rules.
+        $completion = new completion_info($course);
+
+        // Loop through each criteria type and run its update_config() method.
+
+        $criteriadata = new stdClass();
+        $criteriadata->id = $course->id;
+        $criteriadata->criteria_activity = array();
+        // Some activities.
+        $criteriadata->criteria_activity[$cmdata->id] = 1;
+        $criteriadata->criteria_activity[$cmforum->id] = 1;
+
+        // In a week criteria date value.
+        $criteriadata->criteria_date_value = time() + WEEKSECS;
+
+        // Self completion.
+        $criteriadata->criteria_self = 1;
+
+        foreach ($COMPLETION_CRITERIA_TYPES as $type) {
+            $class = 'completion_criteria_'.$type;
+            $criterion = new $class();
+            $criterion->update_config($criteriadata);
+        }
+
+        // Handle overall aggregation.
+        $aggdata = array(
+            'course'        => $course->id,
+            'criteriatype'  => null
+        );
+        $aggregation = new completion_aggregation($aggdata);
+        $aggregation->setMethod(COMPLETION_AGGREGATION_ALL);
+        $aggregation->save();
+
+        $aggdata['criteriatype'] = COMPLETION_CRITERIA_TYPE_ACTIVITY;
+        $aggregation = new completion_aggregation($aggdata);
+        $aggregation->setMethod(COMPLETION_AGGREGATION_ALL);
+        $aggregation->save();
+
+        $this->setUser($student);
+
+        $result = core_completion_external::get_course_completion_status($course->id, $student->id);
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $studentresult = external_api::clean_returnvalue(
+            core_completion_external::get_course_completion_status_returns(), $result);
+
+        // 3 different criteria.
+        $this->assertCount(3, $studentresult['completionstatus']['completions']);
+
+        $this->assertEquals(COMPLETION_AGGREGATION_ALL, $studentresult['completionstatus']['aggregation']);
+        $this->assertFalse($studentresult['completionstatus']['completed']);
+
+        $this->assertEquals('No', $studentresult['completionstatus']['completions'][0]['status']);
+        $this->assertEquals('No', $studentresult['completionstatus']['completions'][1]['status']);
+        $this->assertEquals('No', $studentresult['completionstatus']['completions'][2]['status']);
+
+        // Teacher should see students status, they are in different groups but the teacher can access all groups.
+        $this->setUser($teacher);
+        $result = core_completion_external::get_course_completion_status($course->id, $student->id);
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $teacherresult = external_api::clean_returnvalue(
+            core_completion_external::get_course_completion_status_returns(), $result);
+
+        $this->assertEquals($studentresult, $teacherresult);
+
+        // Change teacher role capabilities (disable access al goups).
+        $context = context_course::instance($course->id);
+        assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $teacherrole->id, $context);
+        accesslib_clear_all_caches_for_unit_testing();
+
+        try {
+            $result = core_completion_external::get_course_completion_status($course->id, $student->id);
+            $this->fail('Exception expected due to groups permissions.');
+        } catch (moodle_exception $e) {
+            $this->assertEquals('accessdenied', $e->errorcode);
+        }
+
+        // Now add the teacher in the same group.
+        groups_add_member($group1->id, $teacher->id);
+        $result = core_completion_external::get_course_completion_status($course->id, $student->id);
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $teacherresult = external_api::clean_returnvalue(
+            core_completion_external::get_course_completion_status_returns(), $result);
+
+        $this->assertEquals($studentresult, $teacherresult);
+
+    }
+
 }
index 3d485ae..da113c3 100644 (file)
@@ -680,11 +680,31 @@ abstract class moodleform_mod extends moodleform {
         }
     }
 
-    function add_intro_editor($required=false, $customlabel=null) {
-        if (!$this->_features->introeditor) {
-            // intro editor not supported in this module
-            return;
-        }
+    /**
+     * Add an editor for an activity's introduction field.
+     * @deprecated since MDL-49101 - use moodleform_mod::standard_intro_elements() instead.
+     * @param null $required Override system default for requiremodintro
+     * @param null $customlabel Override default label for editor
+     * @throws coding_exception
+     */
+    protected function add_intro_editor($required=null, $customlabel=null) {
+        $str = "Function moodleform_mod::add_intro_editor() is deprecated, use moodleform_mod::standard_intro_elements() instead.";
+        debugging($str, DEBUG_DEVELOPER);
+
+        $this->standard_intro_elements($customlabel);
+    }
+
+
+    /**
+     * Add an editor for an activity's introduction field.
+     *
+     * @param null $customlabel Override default label for editor
+     * @throws coding_exception
+     */
+    protected function standard_intro_elements($customlabel=null) {
+        global $CFG;
+
+        $required = $CFG->requiremodintro;
 
         $mform = $this->_form;
         $label = is_null($customlabel) ? get_string('moduleintro') : $customlabel;
index 1bde753..478f5ef 100644 (file)
@@ -37,13 +37,20 @@ Feature: Add activities to courses
     And the field "Allow comments on entries" matches value "Yes"
 
   @javascript
-  Scenario: Add an activity without the required fields
+  Scenario: Add an activity supplying only the name
     When I add a "Database" to section "3" and I fill the form with:
       | Name | Test name |
-    And I press "Save and return to course"
-    Then I should see "Adding a new"
-    And I should see "Required"
-    And I press "Cancel"
+    Then I should see "Test name"
+
+  @javascript
+  Scenario: Set activity description to required then add an activity supplying only the name
+    Given I set the following administration settings values:
+      | requiremodintro | Yes |
+    When I follow "Home"
+    And I follow "Course 1"
+    And I add a "Database" to section "3" and I fill the form with:
+      | Name | Test name |
+    Then I should see "Required"
 
   Scenario: Add an activity to a course with Javascript disabled
     Then I should see "Add a resource to section 'Topic 1'"
index 02db3a1..b921f15 100644 (file)
@@ -89,9 +89,24 @@ if ($mform->is_cancelled()) {
         $instance->roleid       = $data->roleid;
         $instance->customint2   = $data->customint2;
         $instance->timemodified = time();
+        // Create a new group for the cohort if requested.
+        if ($data->customint2 == COHORT_CREATE_GROUP) {
+            require_capability('moodle/course:managegroups', $context);
+            $groupid = enrol_cohort_create_new_group($course->id, $data->customint1);
+            $instance->customint2 = $groupid;
+        }
         $DB->update_record('enrol', $instance);
     }  else {
-        $enrol->add_instance($course, array('name'=>$data->name, 'status'=>$data->status, 'customint1'=>$data->customint1, 'roleid'=>$data->roleid, 'customint2'=>$data->customint2));
+        // Create a new group for the cohort if requested.
+        if ($data->customint2 == COHORT_CREATE_GROUP) {
+            require_capability('moodle/course:managegroups', $context);
+            $groupid = enrol_cohort_create_new_group($course->id, $data->customint1);
+            $enrol->add_instance($course, array('name' => $data->name, 'status' => $data->status,
+                'customint1' => $data->customint1, 'roleid' => $data->roleid, 'customint2' => $groupid));
+        } else {
+            $enrol->add_instance($course, array('name' => $data->name, 'status' => $data->status,
+                'customint1' => $data->customint1, 'roleid' => $data->roleid, 'customint2' => $data->customint2));
+        }
         if (!empty($data->submitbuttonnext)) {
             $returnurl = new moodle_url($PAGE->url);
             $returnurl->param('message', 'added');
index bb5ee8a..2a02a29 100644 (file)
@@ -38,8 +38,11 @@ class enrol_cohort_edit_form extends moodleform {
 
         $enrol = enrol_get_plugin('cohort');
 
-
         $groups = array(0 => get_string('none'));
+        if (has_capability('moodle/course:managegroups', $coursecontext)) {
+            $groups[COHORT_CREATE_GROUP] = get_string('creategroup', 'enrol_cohort');
+        }
+
         foreach (groups_get_all_groups($course->id) as $group) {
             $groups[$group->id] = format_string($group->name, true, array('context'=>$coursecontext));
         }
index 48550f2..446beae 100644 (file)
@@ -26,7 +26,9 @@ $string['addgroup'] = 'Add to group';
 $string['assignrole'] = 'Assign role';
 $string['cohort:config'] = 'Configure cohort instances';
 $string['cohort:unenrol'] = 'Unenrol suspended users';
+$string['defaultgroupnametext'] = '{$a->name} cohort{$a->increment}';
 $string['instanceexists'] = 'Cohort is already synchronised with selected role';
 $string['pluginname'] = 'Cohort sync';
 $string['pluginname_desc'] = 'Cohort enrolment plugin synchronises cohort members with course participants.';
 $string['status'] = 'Active';
+$string['creategroup'] = 'Create new group';
index 1f5a221..55e8a04 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+/**
+ * COHORT_CREATEGROUP constant for automatically creating a group for a cohort.
+ */
+define('COHORT_CREATE_GROUP', -1);
+
 /**
  * Cohort enrolment plugin implementation.
  * @author Petr Skoda
@@ -323,3 +328,38 @@ class enrol_cohort_plugin extends enrol_plugin {
 function enrol_cohort_allow_group_member_remove($itemid, $groupid, $userid) {
     return false;
 }
+
+/**
+ * Create a new group with the cohorts name.
+ *
+ * @param int $courseid
+ * @param int $cohortid
+ * @return int $groupid Group ID for this cohort.
+ */
+function enrol_cohort_create_new_group($courseid, $cohortid) {
+    global $DB;
+
+    $groupname = $DB->get_field('cohort', 'name', array('id' => $cohortid), MUST_EXIST);
+    $a = new stdClass();
+    $a->name = $groupname;
+    $a->increment = '';
+    $groupname = get_string('defaultgroupnametext', 'enrol_cohort', $a);
+    // Check to see if the cohort group name already exists. Add an incremented number if it does.
+    while ($DB->record_exists('groups', array('name' => $groupname))) {
+        $matches = array();
+        if (!preg_match('/(.*?)\(([0-9]+)\)$/', $groupname, $matches)) {
+            $a->increment = '(2)';
+        } else {
+            $a->increment = '(' . ($matches[2] + 1) . ')';
+        }
+        $newshortname = get_string('defaultgroupnametext', 'enrol_cohort', $a);
+        $groupname = $newshortname;
+    }
+    // Create a new group for the cohort.
+    $groupdata = new stdClass();
+    $groupdata->courseid = $courseid;
+    $groupdata->name = $groupname;
+    $groupid = groups_create_group($groupdata);
+
+    return $groupid;
+}
diff --git a/enrol/cohort/tests/cohortlib_test.php b/enrol/cohort/tests/cohortlib_test.php
new file mode 100644 (file)
index 0000000..781c5e6
--- /dev/null
@@ -0,0 +1,86 @@
+<?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/>.
+
+/**
+ * Cohort enrolment sync functional test.
+ *
+ * @package    enrol_cohort
+ * @category   test
+ * @copyright  2015 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot.'/cohort/lib.php');
+require_once($CFG->dirroot.'/group/lib.php');
+
+/**
+ * Contains tests for the cohort library.
+ *
+ * @package   enrol_cohort
+ * @copyright 2015 Adrian Greeve <adrian@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class enrol_cohort_lib_testcase extends advanced_testcase {
+
+    /**
+     * Test that a new group with the name of the cohort is created.
+     */
+    public function test_enrol_cohort_create_new_group() {
+        global $DB;
+        $this->resetAfterTest();
+        // Create a category.
+        $category = $this->getDataGenerator()->create_category();
+        // Create two courses.
+        $course = $this->getDataGenerator()->create_course(array('category' => $category->id));
+        $course2 = $this->getDataGenerator()->create_course(array('category' => $category->id));
+        // Create a cohort.
+        $cohort = $this->getDataGenerator()->create_cohort(array('context' => context_coursecat::instance($category->id)->id));
+        // Run the function.
+        $groupid = enrol_cohort_create_new_group($course->id, $cohort->id);
+        // Check the results.
+        $group = $DB->get_record('groups', array('id' => $groupid));
+        // The group name should match the cohort name.
+        $this->assertEquals($cohort->name . ' cohort', $group->name);
+        // Group course id should match the course id.
+        $this->assertEquals($course->id, $group->courseid);
+
+        // Create a group that will have the same name as the cohort.
+        $groupdata = new stdClass();
+        $groupdata->courseid = $course2->id;
+        $groupdata->name = $cohort->name . ' cohort';
+        groups_create_group($groupdata);
+        // Create a group for the cohort in course 2.
+        $groupid = enrol_cohort_create_new_group($course2->id, $cohort->id);
+        $groupinfo = $DB->get_record('groups', array('id' => $groupid));
+        // Check that the group name has been changed.
+        $this->assertEquals($cohort->name . ' cohort(2)', $groupinfo->name);
+
+        // Create another group that will have the same name as a generated cohort.
+        $groupdata = new stdClass();
+        $groupdata->courseid = $course2->id;
+        $groupdata->name = $cohort->name . ' cohort(2)';
+        groups_create_group($groupdata);
+        // Create a group for the cohort in course 2.
+        $groupid = enrol_cohort_create_new_group($course2->id, $cohort->id);
+        $groupinfo = $DB->get_record('groups', array('id' => $groupid));
+        // Check that the group name has been changed.
+        $this->assertEquals($cohort->name . ' cohort(3)', $groupinfo->name);
+
+    }
+}
index 87e4e30..8522446 100644 (file)
@@ -44,4 +44,13 @@ $functions = array(
         'type'        => 'write',
     ),
 
+    'enrol_manual_unenrol_users' => array(
+        'classname'   => 'enrol_manual_external',
+        'methodname'  => 'unenrol_users',
+        'classpath'   => 'enrol/manual/externallib.php',
+        'description' => 'Manual unenrol users',
+        'capabilities'=> 'enrol/manual:unenrol',
+        'type'        => 'write',
+    ),
+
 );
index d24cc1c..8a9871d 100644 (file)
@@ -156,6 +156,75 @@ class enrol_manual_external extends external_api {
         return null;
     }
 
+    /**
+     * Returns description of method parameters.
+     *
+     * @return external_function_parameters
+     */
+    public static function unenrol_users_parameters() {
+        return new external_function_parameters(array(
+            'enrolments' => new external_multiple_structure(
+                new external_single_structure(
+                    array(
+                        'userid' => new external_value(PARAM_INT, 'The user that is going to be unenrolled'),
+                        'courseid' => new external_value(PARAM_INT, 'The course to unenrol the user from'),
+                        'roleid' => new external_value(PARAM_INT, 'The user role', VALUE_OPTIONAL),
+                    )
+                )
+            )
+        ));
+    }
+
+    /**
+     * Unenrolment of users.
+     *
+     * @param array $enrolments an array of course user and role ids
+     * @throws coding_exception
+     * @throws dml_transaction_exception
+     * @throws invalid_parameter_exception
+     * @throws moodle_exception
+     * @throws required_capability_exception
+     * @throws restricted_context_exception
+     */
+    public static function unenrol_users($enrolments) {
+        global $CFG, $DB;
+        $params = self::validate_parameters(self::unenrol_users_parameters(), array('enrolments' => $enrolments));
+        require_once($CFG->libdir . '/enrollib.php');
+        $transaction = $DB->start_delegated_transaction(); // Rollback all enrolment if an error occurs.
+        $enrol = enrol_get_plugin('manual');
+        if (empty($enrol)) {
+            throw new moodle_exception('manualpluginnotinstalled', 'enrol_manual');
+        }
+
+        foreach ($params['enrolments'] as $enrolment) {
+            $context = context_course::instance($enrolment['courseid']);
+            self::validate_context($context);
+            require_capability('enrol/manual:unenrol', $context);
+            $instance = $DB->get_record('enrol', array('courseid' => $enrolment['courseid'], 'enrol' => 'manual'));
+            if (!$instance) {
+                throw new moodle_exception('wsnoinstance', 'enrol_manual', $enrolment);
+            }
+            $user = $DB->get_record('user', array('id' => $enrolment['userid']));
+            if (!$user) {
+                throw new invalid_parameter_exception('User id not exist: '.$enrolment['userid']);
+            }
+            if (!$enrol->allow_unenrol($instance)) {
+                throw new moodle_exception('wscannotunenrol', 'enrol_manual', '', $enrolment);
+            }
+            $enrol->unenrol_user($instance, $enrolment['userid']);
+        }
+        $transaction->allow_commit();
+    }
+
+    /**
+     * Returns description of method result value.
+     *
+     * @return null
+     */
+    public static function unenrol_users_returns() {
+        return null;
+    }
+
 }
 
 /**
index 6c29c66..7c5f941 100644 (file)
@@ -71,3 +71,4 @@ $string['unenrolusers'] = 'Unenrol users';
 $string['wscannotenrol'] = 'Plugin instance cannot manually enrol a user in the course id = {$a->courseid}';
 $string['wsnoinstance'] = 'Manual enrolment plugin instance doesn\'t exist or is disabled for the course (id = {$a->courseid})';
 $string['wsusercannotassign'] = 'You don\'t have the permission to assign this role ({$a->roleid}) to this user ({$a->userid}) in this course({$a->courseid}).';
+$string['manualpluginnotinstalled'] = 'The "Manual" plugin has not yet been installed';
\ No newline at end of file
index 04f9212..2eefd4a 100644 (file)
@@ -114,4 +114,170 @@ class enrol_manual_externallib_testcase extends externallib_advanced_testcase {
             $this->assertSame('wsnoinstance', $e->errorcode);
         }
     }
+
+    /**
+     * Test for unerolling a single user.
+     * @throws coding_exception
+     * @throws invalid_parameter_exception
+     * @throws moodle_exception
+     */
+    public function test_unenrol_user_single() {
+        global $CFG, $DB;
+        require_once($CFG->libdir . '/enrollib.php');
+        $this->resetAfterTest(true);
+        // The user who perform the action.
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user); // Log this user in.
+        $enrol = enrol_get_plugin('manual');
+        // Create a course.
+        $course = self::getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+        $enrolinstance = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'manual'), '*', MUST_EXIST);
+        // Set the capability for the user.
+        $roleid = $this->assignUserCapability('enrol/manual:enrol', $coursecontext);
+        $this->assignUserCapability('enrol/manual:unenrol', $coursecontext, $roleid);
+        $this->assignUserCapability('moodle/course:view', $coursecontext, $roleid);
+        $this->assignUserCapability('moodle/role:assign', $coursecontext, $roleid);
+        // Create a student and enrol them into the course.
+        $student = $this->getDataGenerator()->create_user();
+        $enrol->enrol_user($enrolinstance, $student->id);
+        $this->assertTrue(is_enrolled($coursecontext, $student));
+        // Call the web service to unenrol.
+        enrol_manual_external::unenrol_users(array(
+            array('userid' => $student->id, 'courseid' => $course->id),
+        ));
+        $this->assertFalse(is_enrolled($coursecontext, $student));
+    }
+
+    /**
+     * Test for unenrolling multiple users.
+     * @throws coding_exception
+     * @throws invalid_parameter_exception
+     * @throws moodle_exception
+     */
+    public function test_unenrol_user_multiple() {
+        global $CFG, $DB;
+        require_once($CFG->libdir . '/enrollib.php');
+        $this->resetAfterTest(true);
+        // The user who perform the action.
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user); // Log this user in.
+        // Create a course.
+        $course = self::getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+        $enrolinstance = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'manual'), '*', MUST_EXIST);
+        // Set the capability for the user.
+        $roleid = $this->assignUserCapability('enrol/manual:enrol', $coursecontext);
+        $this->assignUserCapability('enrol/manual:unenrol', $coursecontext, $roleid);
+        $this->assignUserCapability('moodle/course:view', $coursecontext, $roleid);
+        $this->assignUserCapability('moodle/role:assign', $coursecontext, $roleid);
+        $enrol = enrol_get_plugin('manual');
+        // Create a student and enrol them into the course.
+        $student1 = $this->getDataGenerator()->create_user();
+        $enrol->enrol_user($enrolinstance, $student1->id);
+        $this->assertTrue(is_enrolled($coursecontext, $student1));
+        $student2 = $this->getDataGenerator()->create_user();
+        $enrol->enrol_user($enrolinstance, $student2->id);
+        $this->assertTrue(is_enrolled($coursecontext, $student2));
+        // Call the web service to unenrol.
+        enrol_manual_external::unenrol_users(array(
+            array('userid' => $student1->id, 'courseid' => $course->id),
+            array('userid' => $student2->id, 'courseid' => $course->id),
+        ));
+        $this->assertFalse(is_enrolled($coursecontext, $student1));
+        $this->assertFalse(is_enrolled($coursecontext, $student2));
+    }
+
+    /**
+     * Test for unenrol capability.
+     * @throws coding_exception
+     * @throws invalid_parameter_exception
+     * @throws moodle_exception
+     */
+    public function test_unenrol_user_error_no_capability() {
+        global $CFG, $DB;
+        require_once($CFG->libdir . '/enrollib.php');
+        $this->resetAfterTest(true);
+        // The user who perform the action.
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user); // Log this user in.
+        // Create a course.
+        $course = self::getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+        $enrolinstance = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'manual'), '*', MUST_EXIST);
+        $enrol = enrol_get_plugin('manual');
+        // Create a student and enrol them into the course.
+        $student = $this->getDataGenerator()->create_user();
+        $enrol->enrol_user($enrolinstance, $student->id);
+        $this->assertTrue(is_enrolled($coursecontext, $student));
+        // Call the web service to unenrol.
+        try {
+            enrol_manual_external::unenrol_users(array(
+                array('userid' => $student->id, 'courseid' => $course->id),
+            ));
+            $this->fail('Exception expected: User cannot log in to the course');
+        } catch (Exception $ex) {
+            $this->assertTrue($ex instanceof require_login_exception);
+        }
+        // Set the capability for the course, then try again.
+        $roleid = $this->assignUserCapability('moodle/course:view', $coursecontext);
+        try {
+            enrol_manual_external::unenrol_users(array(
+                array('userid' => $student->id, 'courseid' => $course->id),
+            ));
+            $this->fail('Exception expected: User cannot log in to the course');
+        } catch (Exception $ex) {
+            $this->assertTrue($ex instanceof required_capability_exception);
+        }
+        // Assign unenrol capability.
+        $this->assignUserCapability('enrol/manual:unenrol', $coursecontext, $roleid);
+        enrol_manual_external::unenrol_users(array(
+            array('userid' => $student->id, 'courseid' => $course->id),
+        ));
+        $this->assertFalse(is_enrolled($coursecontext, $student));
+    }
+
+    /**
+     * Test for unenrol if user does not exist.
+     * @throws coding_exception
+     */
+    public function test_unenrol_user_error_not_exist() {
+        global $CFG, $DB;
+        require_once($CFG->libdir . '/enrollib.php');
+        $this->resetAfterTest(true);
+        // The user who perform the action.
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user); // Log this user in.
+        $enrol = enrol_get_plugin('manual');
+        // Create a course.
+        $course = self::getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+        $enrolinstance = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'manual'), '*', MUST_EXIST);
+        // Set the capability for the user.
+        $roleid = $this->assignUserCapability('enrol/manual:enrol', $coursecontext);
+        $this->assignUserCapability('enrol/manual:unenrol', $coursecontext, $roleid);
+        $this->assignUserCapability('moodle/course:view', $coursecontext, $roleid);
+        $this->assignUserCapability('moodle/role:assign', $coursecontext, $roleid);
+        // Create a student and enrol them into the course.
+        $student = $this->getDataGenerator()->create_user();
+        $enrol->enrol_user($enrolinstance, $student->id);
+        $this->assertTrue(is_enrolled($coursecontext, $student));
+        try {
+            enrol_manual_external::unenrol_users(array(
+                array('userid' => $student->id + 1, 'courseid' => $course->id),
+            ));
+            $this->fail('Exception expected: invalid student id');
+        } catch (Exception $ex) {
+            $this->assertTrue($ex instanceof invalid_parameter_exception);
+        }
+        $DB->delete_records('enrol', array('id' => $enrolinstance->id));
+        try {
+            enrol_manual_external::unenrol_users(array(
+                array('userid' => $student->id + 1, 'courseid' => $course->id),
+            ));
+            $this->fail('Exception expected: invalid student id');
+        } catch (Exception $ex) {
+            $this->assertTrue($ex instanceof moodle_exception);
+        }
+    }
 }
index 87a43f0..ab7cd1e 100644 (file)
@@ -32,7 +32,9 @@ $string['customwelcomemessage_help'] = 'A custom welcome message may be added as
 The following placeholders may be included in the message:
 
 * Course name {$a->coursename}
-* Link to user\'s profile page {$a->profileurl}';
+* Link to user\'s profile page {$a->profileurl}
+* User email {$a->email}
+* User fullname {$a->fullname}';
 $string['defaultrole'] = 'Default role assignment';
 $string['defaultrole_desc'] = 'Select role which should be assigned to users during self enrolment';
 $string['enrolenddate'] = 'End date';
index 98d29ef..c51e340 100644 (file)
@@ -412,8 +412,9 @@ class enrol_self_plugin extends enrol_plugin {
 
         if (trim($instance->customtext1) !== '') {
             $message = $instance->customtext1;
-            $message = str_replace('{$a->coursename}', $a->coursename, $message);
-            $message = str_replace('{$a->profileurl}', $a->profileurl, $message);
+            $key = array('{$a->coursename}', '{$a->profileurl}', '{$a->fullname}', '{$a->email}');
+            $value = array($a->coursename, $a->profileurl, fullname($user), $user->email);
+            $message = str_replace($key, $value, $message);
             if (strpos($message, '<') === false) {
                 // Plain text only.
                 $messagetext = $message;
index d23f400..0f65755 100644 (file)
@@ -51,10 +51,11 @@ define('IGNORE_COMPONENT_CACHE', true);
 define('MDL_PERF_TEST', false);
 
 // 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')) {
+    echo("Timezone functions are not available.");
+    die;
 }
+date_default_timezone_set(@date_default_timezone_get());
 
 // make sure PHP errors are displayed - helps with diagnosing of problems
 @error_reporting(E_ALL);
index 8b6f1da..dd15350 100644 (file)
@@ -300,7 +300,6 @@ $string['configrequestedstudentname'] = 'Word for student used in requested cour
 $string['configrequestedstudentsname'] = 'Word for students used in requested courses';
 $string['configrequestedteachername'] = 'Word for teacher used in requested courses';
 $string['configrequestedteachersname'] = 'Word for teachers used in requested courses';
-$string['configrequiremodintro'] = 'Disable this option if you do not want to force users to enter description of each activity.';
 $string['configrunclamavonupload'] = 'When enabled, clam AV will be used to scan all uploaded files.';
 $string['configrunclamonupload'] = 'Run clam AV on file upload? You will need a correct path in pathtoclam for this to work.  (Clam AV is a free virus scanner that you can get from http://www.clamav.net/)';
 $string['configuserquota'] = 'The maximum number of bytes that a user can store in their own private file area. {$a->bytes} bytes == {$a->displaysize}';
@@ -345,7 +344,7 @@ $string['configtempdatafoldercleanup'] = 'Remove temporary data files from the d
 $string['configthemedesignermode'] = 'Normally all theme images and style sheets are cached in browsers and on the server for a very long time, for performance. If you are designing themes or developing code then you probably want to turn this mode on so that you are not served cached versions.  Warning: this will make your site slower for all users!  Alternatively, you can also reset the theme caches manually from the Theme selection page.';
 $string['configthemelist'] = 'Leave this blank to allow any valid theme to be used.  If you want to shorten the theme menu, you can specify a comma-separated list of names here (Don\'t use spaces!).
 For example:  standard,orangewhite.';
-$string['configtimezone'] = 'This is the default timezone for displaying dates - each user can override this setting in their profile. "Server timezone" here will make Moodle default to the server setting in PHP, but "Server timezone" in the user profile will make the user default to this timezone setting. It is recommended that you update timezones then select a named timezone specific to your region.';
+$string['configtimezone'] = 'This is the default timezone for displaying dates - each user can override this setting in their profile. Cron tasks and other server settings are specified in this timezone. You should change the setting if it shows as "Invalid timezone"';
 $string['configuseblogassociations'] = 'Should users be able to organize their blog by associating entries with courses and course modules?';
 $string['configuseexternalyui'] = 'Instead of using local files, use online files available on Yahoo&#145;s servers. WARNING: This requires an internet connection, or no AJAX will work on your site. This setting is not compatible with sites using https.';
 $string['configusesitenameforsitepages'] = 'If enabled the site\'s shortname will be used for the site pages node in the navigation rather than the string \'Site pages\'';
@@ -909,6 +908,7 @@ $string['reportsmanage'] = 'Manage reports';
 $string['requiredentrieschanged'] = '<strong>IMPORTANT - PLEASE READ<br/>(This warning message will only be displayed during this upgrade)</strong><br/>Due to a bug fix, the behaviour of database activities using the \'Required entries\' and \'Required entries before viewing settings\' settings will change. A more detailed explanation of the changes can be read on <a href="http://moodle.org/mod/forum/discuss.php?d=110928" target="_blank">the database module forum</a>. The expected behavior of these settings can also be read on <a href="http://docs.moodle.org/en/Adding/editing_a_database#Required_entries" target="_blank">Moodle Docs</a>.
 <br/><br/>This change affects the following databases in your system: (Please save this list now, and after the upgrade, check that these activities still work the way that the teacher intends.)<br/><strong>{$a->text}</strong><br/>';
 $string['requiremodintro'] = 'Require activity description';
+$string['requiremodintro_desc'] = 'Enable this option if you want to force users to enter description of each activity.';
 $string['requires'] = 'Requires';
 $string['purgecaches']= 'Purge all caches';
 $string['purgecachesconfirm']= 'Moodle can cache themes, javascript, language strings, filtered text, rss feeds and many other pieces of calculated data.  Purging these caches will delete that data from the server and force browsers to refetch data, so that you can be sure you are seeing the most up-to-date values produced by the current code.  There is no danger in purging caches, but your site may appear slower for a while until the server and clients calculate new information and cache it.';
@@ -1054,8 +1054,11 @@ $string['thirdpartylibrarylocation'] = 'Location';
 $string['thirdpartylibs'] = 'Third party libraries';
 $string['timezone'] = 'Default timezone';
 $string['timezoneforced'] = 'This is forced by the site administrator';
+$string['timezoneinvalid'] = 'Invalid timezone "{$a}"';
 $string['timezoneisforcedto'] = 'Force all users to use';
 $string['timezonenotforced'] = 'Users can choose their own timezone';
+$string['timezonephpdefault'] = 'Default PHP timezone ({$a})';
+$string['timezoneserver'] = 'Server timezone ({$a})';
 $string['tokenizerrecommended'] = 'Installing the optional PHP Tokenizer extension is recommended -- it improves Moodle Networking functionality.';
 $string['tools'] = 'Admin tools';
 $string['toolsmanage'] = 'Manage admin tools';
index 1c0f8b0..51675e9 100644 (file)
@@ -2256,9 +2256,10 @@ function can_access_course(stdClass $course, $user = null, $withcapability = '',
  * @param string $withcapability
  * @param int $groupid 0 means ignore groups, any other value limits the result by group id
  * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
+ * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
  * @return array list($sql, $params)
  */
-function get_enrolled_sql(context $context, $withcapability = '', $groupid = 0, $onlyactive = false) {
+function get_enrolled_sql(context $context, $withcapability = '', $groupid = 0, $onlyactive = false, $onlysuspended = false) {
     global $DB, $CFG;
 
     // use unique prefix just in case somebody makes some SQL magic with the result
@@ -2271,6 +2272,13 @@ function get_enrolled_sql(context $context, $withcapability = '', $groupid = 0,
 
     $isfrontpage = ($coursecontext->instanceid == SITEID);
 
+    if ($onlyactive && $onlysuspended) {
+        throw new coding_exception("Both onlyactive and onlysuspended are set, this is probably not what you want!");
+    }
+    if ($isfrontpage && $onlysuspended) {
+        throw new coding_exception("onlysuspended is not supported on frontpage; please add your own early-exit!");
+    }
+
     $joins  = array();
     $wheres = array();
     $params = array();
@@ -2387,13 +2395,28 @@ function get_enrolled_sql(context $context, $withcapability = '', $groupid = 0,
     if ($isfrontpage) {
         // all users are "enrolled" on the frontpage
     } else {
-        $joins[] = "JOIN {user_enrolments} {$prefix}ue ON {$prefix}ue.userid = {$prefix}u.id";
-        $joins[] = "JOIN {enrol} {$prefix}e ON ({$prefix}e.id = {$prefix}ue.enrolid AND {$prefix}e.courseid = :{$prefix}courseid)";
+        $where1 = "{$prefix}ue.status = :{$prefix}active AND {$prefix}e.status = :{$prefix}enabled";
+        $where2 = "{$prefix}ue.timestart < :{$prefix}now1 AND ({$prefix}ue.timeend = 0 OR {$prefix}ue.timeend > :{$prefix}now2)";
+        $ejoin = "JOIN {enrol} {$prefix}e ON ({$prefix}e.id = {$prefix}ue.enrolid AND {$prefix}e.courseid = :{$prefix}courseid)";
         $params[$prefix.'courseid'] = $coursecontext->instanceid;
 
-        if ($onlyactive) {
-            $wheres[] = "{$prefix}ue.status = :{$prefix}active AND {$prefix}e.status = :{$prefix}enabled";
-            $wheres[] = "{$prefix}ue.timestart < :{$prefix}now1 AND ({$prefix}ue.timeend = 0 OR {$prefix}ue.timeend > :{$prefix}now2)";
+        if (!$onlysuspended) {
+            $joins[] = "JOIN {user_enrolments} {$prefix}ue ON {$prefix}ue.userid = {$prefix}u.id";
+            $joins[] = $ejoin;
+            if ($onlyactive) {
+                $wheres[] = "$where1 AND $where2";
+            }
+        } else {
+            // Suspended only where there is enrolment but ALL are suspended.
+            // Consider multiple enrols where one is not suspended or plain role_assign.
+            $enrolselect = "SELECT DISTINCT {$prefix}ue.userid FROM {user_enrolments} {$prefix}ue $ejoin WHERE $where1 AND $where2";
+            $joins[] = "JOIN {user_enrolments} {$prefix}ue1 ON {$prefix}ue1.userid = {$prefix}u.id";
+            $joins[] = "JOIN {enrol} {$prefix}e1 ON ({$prefix}e1.id = {$prefix}ue1.enrolid AND {$prefix}e1.courseid = :{$prefix}_e1_courseid)";
+            $params["{$prefix}_e1_courseid"] = $coursecontext->instanceid;
+            $wheres[] = "{$prefix}u.id NOT IN ($enrolselect)";
+        }
+
+        if ($onlyactive || $onlysuspended) {
             $now = round(time(), -2); // rounding helps caching in DB
             $params = array_merge($params, array($prefix.'enabled'=>ENROL_INSTANCE_ENABLED,
                                                  $prefix.'active'=>ENROL_USER_ACTIVE,
@@ -7506,7 +7529,6 @@ function extract_suspended_users($context, &$users, $ignoreusers=array()) {
 function get_suspended_userids(context $context, $usecache = false) {
     global $DB;
 
-    // Check the cache first for performance reasons if enabled.
     if ($usecache) {
         $cache = cache::make('core', 'suspended_userids');
         $susers = $cache->get($context->id);
@@ -7515,21 +7537,14 @@ function get_suspended_userids(context $context, $usecache = false) {
         }
     }
 
-    // Get all enrolled users.
-    list($sql, $params) = get_enrolled_sql($context);
-    $users = $DB->get_records_sql($sql, $params);
-
-    // Get active enrolled users.
-    list($sql, $params) = get_enrolled_sql($context, null, null, true);
-    $activeusers = $DB->get_records_sql($sql, $params);
-
+    $coursecontext = $context->get_course_context();
     $susers = array();
-    if (sizeof($activeusers) != sizeof($users)) {
-        foreach ($users as $userid => $user) {
-            if (!array_key_exists($userid, $activeusers)) {
-                $susers[$userid] = $userid;
-            }
-        }
+
+    // Front page users are always enrolled, so suspended list is empty.
+    if ($coursecontext->instanceid != SITEID) {
+        list($sql, $params) = get_enrolled_sql($context, null, null, false, true);
+        $susers = $DB->get_fieldset_sql($sql, $params);
+        $susers = array_combine($susers, $susers);
     }
 
     // Cache results for the remainder of this request.
@@ -7537,6 +7552,5 @@ function get_suspended_userids(context $context, $usecache = false) {
         $cache->set($context->id, $susers);
     }
 
-    // Return.
     return $susers;
 }
diff --git a/lib/classes/date.php b/lib/classes/date.php
new file mode 100644 (file)
index 0000000..3ced6b6
--- /dev/null
@@ -0,0 +1,553 @@
+<?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/>.
+
+/**
+ * Core date and time related code.
+ *
+ * @package   core
+ * @copyright 2015 Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author    Petr Skoda <petr.skoda@totaralms.com>
+ */
+
+/**
+ * Core date and time related code.
+ *
+ * @since Moodle 2.9
+ * @package   core
+ * @copyright 2015 Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author    Petr Skoda <petr.skoda@totaralms.com>
+ */
+class core_date {
+    /** @var array list of recommended zones */
+    protected static $goodzones = null;
+
+    /** @var array list of BC zones supported by PHP */
+    protected static $bczones = null;
+
+    /** @var array mapping of timezones not supported by PHP */
+    protected static $badzones = null;
+
+    /** @var string the default PHP timezone right after config.php */
+    protected static $defaultphptimezone = null;
+
+    /**
+     * Returns a localised list of timezones.
+     * @param string $currentvalue
+     * @param bool $include99 should the server timezone info be included?
+     * @return array
+     */
+    public static function get_list_of_timezones($currentvalue = null, $include99 = false) {
+        self::init_zones();
+
+        // Localise first.
+        $timezones = array();
+        foreach (self::$goodzones as $tzkey => $ignored) {
+            $timezones[$tzkey] = self::get_localised_timezone($tzkey);
+        }
+        core_collator::asort($timezones);
+
+        // Add '99' if requested.
+        if ($include99 or $currentvalue == 99) {
+            $timezones['99'] = self::get_localised_timezone('99');
+        }
+
+        if (!isset($currentvalue) or isset($timezones[$currentvalue])) {
+            return $timezones;
+        }
+
+        if (is_numeric($currentvalue)) {
+            // UTC offset.
+            $modifier = ($currentvalue > 0) ? '+' : '';
+            $a = 'UTC' . $modifier . number_format($currentvalue, 1);
+            $timezones[$currentvalue] = get_string('timezoneinvalid', 'core_admin', $a);
+        } else {
+            // Some string we don't recognise.
+            $timezones[$currentvalue] = get_string('timezoneinvalid', 'core_admin', $currentvalue);
+        }
+
+        return $timezones;
+    }
+
+    /**
+     * Returns localised timezone name.
+     * @param string $tz
+     * @return string
+     */
+    public static function get_localised_timezone($tz) {
+        if ($tz == 99) {
+            $tz = self::get_server_timezone();
+            $tz = self::get_localised_timezone($tz);
+            return get_string('timezoneserver', 'core_admin', $tz);
+        }
+
+        if (get_string_manager()->string_exists(strtolower($tz), 'core_timezones')) {
+            $tz = get_string(strtolower($tz), 'core_timezones');
+        } else if ($tz === 'GMT' or $tz === 'Etc/GMT' or $tz === 'Etc/UTC') {
+            $tz = 'UTC';
+        } else if (preg_match('|^Etc/GMT([+-])([0-9]+)$|', $tz, $matches)) {
+            $sign = $matches[1] === '+' ? '-' : '+';
+            $tz = 'UTC' . $sign . $matches[2];
+        }
+
+        return $tz;
+    }
+
+    /**
+     * Normalise the timezone name. If timezone not supported
+     * this method falls back to server timezone (if valid)
+     * or default PHP timezone.
+     *
+     * @param int|string|float|DateTimeZone $tz
+     * @return string timezone compatible with PHP
+     */
+    public static function normalise_timezone($tz) {
+        global $CFG;
+
+        if ($tz instanceof DateTimeZone) {
+            return $tz->getName();
+        }
+
+        self::init_zones();
+        $tz = (string)$tz;
+
+        if (isset(self::$goodzones[$tz]) or isset(self::$bczones[$tz])) {
+            return $tz;
+        }
+
+        $fixed = false;
+        if (isset(self::$badzones[$tz])) {
+            // Convert to known zone.
+            $tz = self::$badzones[$tz];
+            $fixed = true;
+        } else if (is_number($tz)) {
+            // Half hour numeric offsets were already tested, try rounding to integers here.
+            $roundedtz = (string)(int)$tz;
+            if (isset(self::$badzones[$roundedtz])) {
+                $tz = self::$badzones[$roundedtz];
+                $fixed = true;
+            }
+        }
+
+        if ($fixed and isset(self::$goodzones[$tz]) or isset(self::$bczones[$tz])) {
+            return $tz;
+        }
+
+        // Is server timezone usable?
+        if (isset($CFG->timezone) and !is_numeric($CFG->timezone)) {
+            $result = @timezone_open($CFG->timezone); // Hide notices if invalid.
+            if ($result !== false) {
+                return $result->getName();
+            }
+        }
+
+        // Bad luck, use the php.ini default or value set in config.php.
+        return self::get_default_php_timezone();
+    }
+
+    /**
+     * Returns server timezone.
+     * @return string normalised timezone name compatible with PHP
+     **/
+    public static function get_server_timezone() {
+        global $CFG;
+
+        if (!isset($CFG->timezone) or $CFG->timezone == 99 or $CFG->timezone === '') {
+            return self::get_default_php_timezone();
+        }
+
+        return self::normalise_timezone($CFG->timezone);
+    }
+
+    /**
+     * Returns server timezone.
+     * @return DateTimeZone
+     **/
+    public static function get_server_timezone_object() {
+        $tz = self::get_server_timezone();
+        return new DateTimeZone($tz);
+    }
+
+    /**
+     * Set PHP default timezone to $CFG->timezone.
+     */
+    public static function set_default_server_timezone() {
+        global $CFG;
+
+        if (!isset($CFG->timezone) or $CFG->timezone == 99 or $CFG->timezone === '') {
+            date_default_timezone_set(self::get_default_php_timezone());
+            return;
+        }
+
+        $current = date_default_timezone_get();
+        if ($current === $CFG->timezone) {
+            // Nothing to do.
+            return;
+        }
+
+        if (!isset(self::$goodzones)) {
+            // For better performance try do do this without full tz init,
+            // because this is called from lib/setup.php file on each page.
+            $result = @timezone_open($CFG->timezone); // Ignore error if setting invalid.
+            if ($result !== false) {
+                date_default_timezone_set($result->getName());
+                return;
+            }
+        }
+
+        // Slow way is the last option.
+        date_default_timezone_set(self::get_server_timezone());
+    }
+
+    /**
+     * Returns user timezone.
+     *
+     * Ideally the parameter should be a real user record,
+     * unfortunately the legacy code is using 99 for both server
+     * and default value.
+     *
+     * Example of using legacy API:
+     *    // Date for other user via legacy API.
+     *    $datestr = userdate($time, core_date::get_user_timezone($user));
+     *
+     * The coding style rules in Moodle are moronic,
+     * why cannot the parameter names have underscores in them?
+     *
+     * @param mixed $userorforcedtz user object or legacy forced timezone string or tz object
+     * @return string normalised timezone name compatible with PHP
+     */
+    public static function get_user_timezone($userorforcedtz = null) {
+        global $USER, $CFG;
+
+        if ($userorforcedtz instanceof DateTimeZone) {
+            return $userorforcedtz->getName();
+        }
+
+        if (isset($userorforcedtz) and !is_object($userorforcedtz) and $userorforcedtz != 99) {
+            // Legacy code is forcing timezone in legacy API.
+            return self::normalise_timezone($userorforcedtz);
+        }
+
+        if (isset($CFG->forcetimezone) and $CFG->forcetimezone != 99) {
+            // Override any user timezone.
+            return self::normalise_timezone($CFG->forcetimezone);
+        }
+
+        if ($userorforcedtz === null) {
+            $tz = isset($USER->timezone) ? $USER->timezone : 99;
+
+        } else if (is_object($userorforcedtz)) {
+            $tz = isset($userorforcedtz->timezone) ? $userorforcedtz->timezone : 99;
+
+        } else {
+            if ($userorforcedtz == 99) {
+                $tz = isset($USER->timezone) ? $USER->timezone : 99;
+            } else {
+                $tz = $userorforcedtz;
+            }
+        }
+
+        if ($tz == 99) {
+            return self::get_server_timezone();
+        }
+
+        return self::normalise_timezone($tz);
+    }
+
+    /**
+     * Return user timezone object.
+     *
+     * @param mixed $userorforcedtz
+     * @return DateTimeZone
+     */
+    public static function get_user_timezone_object($userorforcedtz = null) {
+        $tz = self::get_user_timezone($userorforcedtz);
+        return new DateTimeZone($tz);
+    }
+
+    /**
+     * Return default timezone set in php.ini or config.php.
+     * @return string normalised timezone compatible with PHP
+     */
+    public static function get_default_php_timezone() {
+        if (!isset(self::$defaultphptimezone)) {
+            // This should not happen.
+            self::store_default_php_timezone();
+        }
+
+        return self::$defaultphptimezone;
+    }
+
+    /**
+     * To be called from lib/setup.php only!
+     */
+    public static function store_default_php_timezone() {
+        if (defined('PHPUNIT_TEST') or defined('BEHAT_SITE_RUNNING') or defined('BEHAT_TEST') or defined('BEHAT_UTIL')) {
+            // We want all test sites to be consistent by default.
+            self::$defaultphptimezone = 'Australia/Perth';
+            return;
+        }
+        if (!isset(self::$defaultphptimezone)) {
+            self::$defaultphptimezone = date_default_timezone_get();
+        }
+    }
+
+    /**
+     * Do not use directly - use $this->setTimezone('xx', $tz) instead in your test case.
+     * @param string $tz valid timezone name
+     */
+    public static function phpunit_override_default_php_timezone($tz) {
+        if (!defined('PHPUNIT_TEST')) {
+            throw new coding_exception('core_date::phpunit_override_default_php_timezone() must be used only from unit tests');
+        }
+        $result = timezone_open($tz); // This triggers error if $tz invalid.
+        if ($result !== false) {
+            self::$defaultphptimezone = $tz;
+        } else {
+            self::$defaultphptimezone = 'Australia/Perth';
+        }
+    }
+
+    /**
+     * To be called from phpunit reset only, after restoring $CFG.
+     */
+    public static function phpunit_reset() {
+        global $CFG;
+        if (!defined('PHPUNIT_TEST')) {
+            throw new coding_exception('core_date::phpunit_reset() must be used only from unit tests');
+        }
+        self::store_default_php_timezone();
+        date_default_timezone_set($CFG->timezone);
+    }
+
+    /**
+     * Initialise timezone arrays, call before use.
+     */
+    protected static function init_zones() {
+        if (isset(self::$goodzones)) {
+            return;
+        }
+
+        $zones = DateTimeZone::listIdentifiers();
+        self::$goodzones = array_fill_keys($zones, true);
+
+        $zones = DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC);
+        self::$bczones = array();
+        foreach ($zones as $zone) {
+            if (isset(self::$goodzones[$zone])) {
+                continue;
+            }
+            self::$bczones[$zone] = true;
+        }
+
+        self::$badzones = array(
+            // Windows time zones.
+            'Dateline Standard Time' => 'Etc/GMT+12',
+            'Hawaiian Standard Time' => 'Pacific/Honolulu',
+            'Alaskan Standard Time' => 'America/Anchorage',
+            'Pacific Standard Time (Mexico)' => 'America/Santa_Isabel',
+            'Pacific Standard Time' => 'America/Los_Angeles',
+            'US Mountain Standard Time' => 'America/Phoenix',
+            'Mountain Standard Time (Mexico)' => 'America/Chihuahua',
+            'Mountain Standard Time' => 'America/Denver',
+            'Central America Standard Time' => 'America/Guatemala',
+            'Central Standard Time' => 'America/Chicago',
+            'Central Standard Time (Mexico)' => 'America/Mexico_City',
+            'Canada Central Standard Time' => 'America/Regina',
+            'SA Pacific Standard Time' => 'America/Bogota',
+            'Eastern Standard Time' => 'America/New_York',
+            'US Eastern Standard Time' => 'America/Indianapolis',
+            'Venezuela Standard Time' => 'America/Caracas',
+            'Paraguay Standard Time' => 'America/Asuncion',
+            'Atlantic Standard Time' => 'America/Halifax',
+            'Central Brazilian Standard Time' => 'America/Cuiaba',
+            'SA Western Standard Time' => 'America/La_Paz',
+            'Pacific SA Standard Time' => 'America/Santiago',
+            'Newfoundland Standard Time' => 'America/St_Johns',
+            'E. South America Standard Time' => 'America/Sao_Paulo',
+            'Argentina Standard Time' => 'America/Buenos_Aires',
+            'SA Eastern Standard Time' => 'America/Cayenne',
+            'Greenland Standard Time' => 'America/Godthab',
+            'Montevideo Standard Time' => 'America/Montevideo',
+            'Bahia Standard Time' => 'America/Bahia',
+            'Azores Standard Time' => 'Atlantic/Azores',
+            'Cape Verde Standard Time' => 'Atlantic/Cape_Verde',
+            'Morocco Standard Time' => 'Africa/Casablanca',
+            'GMT Standard Time' => 'Europe/London',
+            'Greenwich Standard Time' => 'Atlantic/Reykjavik',
+            'W. Europe Standard Time' => 'Europe/Berlin',
+            'Central Europe Standard Time' => 'Europe/Budapest',
+            'Romance Standard Time' => 'Europe/Paris',
+            'Central European Standard Time' => 'Europe/Warsaw',
+            'W. Central Africa Standard Time' => 'Africa/Lagos',
+            'Namibia Standard Time' => 'Africa/Windhoek',
+            'Jordan Standard Time' => 'Asia/Amman',
+            'GTB Standard Time' => 'Europe/Bucharest',
+            'Middle East Standard Time' => 'Asia/Beirut',
+            'Egypt Standard Time' => 'Africa/Cairo',
+            'Syria Standard Time' => 'Asia/Damascus',
+            'South Africa Standard Time' => 'Africa/Johannesburg',
+            'FLE Standard Time' => 'Europe/Kiev',
+            'Turkey Standard Time' => 'Europe/Istanbul',
+            'Israel Standard Time' => 'Asia/Jerusalem',
+            'Kaliningrad Standard Time' => 'Europe/Kaliningrad',
+            'Libya Standard Time' => 'Africa/Tripoli',
+            'Arabic Standard Time' => 'Asia/Baghdad',
+            'Arab Standard Time' => 'Asia/Riyadh',
+            'Belarus Standard Time' => 'Europe/Minsk',
+            'Russian Standard Time' => 'Europe/Moscow',
+            'E. Africa Standard Time' => 'Africa/Nairobi',
+            'Iran Standard Time' => 'Asia/Tehran',
+            'Arabian Standard Time' => 'Asia/Dubai',
+            'Azerbaijan Standard Time' => 'Asia/Baku',
+            'Russia Time Zone 3' => 'Europe/Samara',
+            'Mauritius Standard Time' => 'Indian/Mauritius',
+            'Georgian Standard Time' => 'Asia/Tbilisi',
+            'Caucasus Standard Time' => 'Asia/Yerevan',
+            'Afghanistan Standard Time' => 'Asia/Kabul',
+            'West Asia Standard Time' => 'Asia/Tashkent',
+            'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg',
+            'Pakistan Standard Time' => 'Asia/Karachi',
+            'India Standard Time' => 'Asia/Kolkata', // PHP and Windows differ in spelling.
+            'Sri Lanka Standard Time' => 'Asia/Colombo',
+            'Nepal Standard Time' => 'Asia/Katmandu',
+            'Central Asia Standard Time' => 'Asia/Almaty',
+            'Bangladesh Standard Time' => 'Asia/Dhaka',
+            'N. Central Asia Standard Time' => 'Asia/Novosibirsk',
+            'Myanmar Standard Time' => 'Asia/Rangoon',
+            'SE Asia Standard Time' => 'Asia/Bangkok',
+            'North Asia Standard Time' => 'Asia/Krasnoyarsk',
+            'China Standard Time' => 'Asia/Shanghai',
+            'North Asia East Standard Time' => 'Asia/Irkutsk',
+            'Singapore Standard Time' => 'Asia/Singapore',
+            'W. Australia Standard Time' => 'Australia/Perth',
+            'Taipei Standard Time' => 'Asia/Taipei',
+            'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar',
+            'Tokyo Standard Time' => 'Asia/Tokyo',
+            'Korea Standard Time' => 'Asia/Seoul',
+            'Yakutsk Standard Time' => 'Asia/Yakutsk',
+            'Cen. Australia Standard Time' => 'Australia/Adelaide',
+            'AUS Central Standard Time' => 'Australia/Darwin',
+            'E. Australia Standard Time' => 'Australia/Brisbane',
+            'AUS Eastern Standard Time' => 'Australia/Sydney',
+            'West Pacific Standard Time' => 'Pacific/Port_Moresby',
+            'Tasmania Standard Time' => 'Australia/Hobart',
+            'Magadan Standard Time' => 'Asia/Magadan',
+            'Vladivostok Standard Time' => 'Asia/Vladivostok',
+            'Russia Time Zone 10' => 'Asia/Srednekolymsk',
+            'Central Pacific Standard Time' => 'Pacific/Guadalcanal',
+            'Russia Time Zone 11' => 'Asia/Kamchatka',
+            'New Zealand Standard Time' => 'Pacific/Auckland',
+            'Fiji Standard Time' => 'Pacific/Fiji',
+            'Tonga Standard Time' => 'Pacific/Tongatapu',
+            'Samoa Standard Time' => 'Pacific/Apia',
+            'Line Islands Standard Time' => 'Pacific/Kiritimati',
+
+            // A lot more bad legacy time zones.
+            'CET' => 'Europe/Berlin',
+            'Central European Time' => 'Europe/Berlin',
+            'CST' => 'America/Chicago',
+            'Central Time' => 'America/Chicago',
+            'CST6CDT' => 'America/Chicago',
+            'CDT' => 'America/Chicago',
+            'China Time' => 'Asia/Shanghai',
+            'EDT' => 'America/New_York',
+            'EST' => 'America/New_York',
+            'EST5EDT' => 'America/New_York',
+            'Eastern Time' => 'America/New_York',
+            'IST' => 'Asia/Kolkata',
+            'India Time' => 'Asia/Kolkata',
+            'JST' => 'Asia/Tokyo',
+            'Japan Time' => 'Asia/Tokyo',
+            'Japan Standard Time' => 'Asia/Tokyo',
+            'MDT' => 'America/Denver',
+            'MST' => 'America/Denver',
+            'MST7MDT' => 'America/Denver',
+            'PDT' => 'America/Los_Angeles',
+            'PST' => 'America/Los_Angeles',
+            'Pacific Time' => 'America/Los_Angeles',
+            'PST8PDT' => 'America/Los_Angeles',
+            'HST' => 'Pacific/Honolulu',
+            'WET' => 'Europe/London',
+            'EET' => 'Europe/Kiev',
+            'FET' => 'Europe/Minsk',
+
+            // Some UTC variations.
+            'UTC-01' => 'Etc/GMT+1',
+            'UTC-02' => 'Etc/GMT+2',
+            'UTC-03' => 'Etc/GMT+3',
+            'UTC-04' => 'Etc/GMT+4',
+            'UTC-05' => 'Etc/GMT+5',
+            'UTC-06' => 'Etc/GMT+6',
+            'UTC-07' => 'Etc/GMT+7',
+            'UTC-08' => 'Etc/GMT+8',
+            'UTC-09' => 'Etc/GMT+9',
+
+            // Some weird GMTs.
+            'Etc/GMT+0' => 'Etc/GMT',
+            'Etc/GMT-0' => 'Etc/GMT',
+            'Etc/GMT0' => 'Etc/GMT',
+
+            // And lastly some alternative city spelling.
+            'Asia/Calcutta' => 'Asia/Kolkata',
+        );
+
+        // Legacy GMT fallback.
+        for ($i = -14; $i <= 13; $i++) {
+            $off = abs($i);
+            if ($i < 0) {
+                $mapto = 'Etc/GMT+' . $off;
+                $utc = 'UTC-' . $off;
+                $gmt = 'GMT-' . $off;
+            } else if ($i > 0) {
+                $mapto = 'Etc/GMT-' . $off;
+                $utc = 'UTC+' . $off;
+                $gmt = 'GMT+' . $off;
+            } else {
+                $mapto = 'Etc/GMT';
+                $utc = 'UTC';
+                $gmt = 'GMT';
+            }
+            if (isset(self::$bczones[$mapto])) {
+                self::$badzones[$i . ''] = $mapto;
+                self::$badzones[$i . '.0'] = $mapto;
+                self::$badzones[$utc] = $mapto;
+                self::$badzones[$gmt] = $mapto;
+            }
+        }
+
+        // Legacy Moodle half an hour offsets - pick any city nearby, ideally without DST.
+        self::$badzones['-4.5'] = 'America/Caracas';
+        self::$badzones['4.5'] = 'Asia/Kabul';
+        self::$badzones['5.5'] = 'Asia/Kolkata';
+        self::$badzones['6.5'] = 'Asia/Rangoon';
+        self::$badzones['9.5'] = 'Australia/Darwin';
+        self::$badzones['11.5'] = 'Pacific/Norfolk';
+
+        // Remove bad zones that are elsewhere.
+        foreach (self::$bczones as $zone => $unused) {
+            if (isset(self::$badzones[$zone])) {
+                unset(self::$badzones[$zone]);
+            }
+        }
+        foreach (self::$goodzones as $zone => $unused) {
+            if (isset(self::$badzones[$zone])) {
+                unset(self::$badzones[$zone]);
+            }
+        }
+    }
+}
index 82921e4..040b39c 100644 (file)
@@ -913,7 +913,7 @@ class core_plugin_manager {
             'qformat' => array('blackboard', 'learnwise'),
             'enrol' => array('authorize'),
             'tinymce' => array('dragmath'),
-            'tool' => array('bloglevelupgrade', 'qeupgradehelper'),
+            'tool' => array('bloglevelupgrade', 'qeupgradehelper', 'timezoneimport'),
             'theme' => array('mymobile', 'afterburner', 'anomaly', 'arialist', 'binarius', 'boxxie', 'brick', 'formal_white',
                 'formfactor', 'fusion', 'leatherbound', 'magazine', 'nimble', 'nonzero', 'overlay', 'serenity', 'sky_high',
                 'splash', 'standard', 'standardold'),
@@ -1143,7 +1143,7 @@ class core_plugin_manager {
                 'assignmentupgrade', 'availabilityconditions', 'behat', 'capability', 'customlang',
                 'dbtransfer', 'filetypes', 'generator', 'health', 'innodb', 'installaddon',
                 'langimport', 'log', 'messageinbound', 'multilangupgrade', 'monitor', 'phpunit', 'profiling',
-                'replace', 'spamcleaner', 'task', 'timezoneimport',
+                'replace', 'spamcleaner', 'task',
                 'unittest', 'uploadcourse', 'uploaduser', 'unsuproles', 'xmldb'
             ),
 
index aedfceb..42500ab 100644 (file)
@@ -332,10 +332,7 @@ abstract class scheduled_task extends task_base {
         $validhours = $this->eval_cron_field($this->hour, self::HOURMIN, self::HOURMAX);
 
         // We need to change to the server timezone before using php date() functions.
-        $origtz = date_default_timezone_get();
-        if (!empty($CFG->timezone) && $CFG->timezone != 99) {
-            date_default_timezone_set($CFG->timezone);
-        }
+        \core_date::set_default_server_timezone();
 
         $daysinmonth = date("t");
         $validdays = $this->eval_cron_field($this->day, 1, $daysinmonth);
@@ -404,11 +401,6 @@ abstract class scheduled_task extends task_base {
                            $nextvaliddayofmonth,
                            $nextvalidyear);
 
-        // We need to change the timezone back so other date functions in moodle do not get confused.
-        if (!empty($CFG->timezone) && $CFG->timezone != 99) {
-            date_default_timezone_set($origtz);
-        }
-
         return $nexttime;
     }
 
index 1775ef6..b27f628 100644 (file)
@@ -1212,30 +1212,6 @@ function get_scales_menu($courseid=0) {
     return $DB->get_records_sql_menu($sql, $params);
 }
 
-
-
-/**
- * Given a set of timezone records, put them in the database,  replacing what is there
- *
- * @global object
- * @param array $timezones An array of timezone records
- * @return void
- */
-function update_timezone_records($timezones) {
-    global $DB;
-
-/// Clear out all the old stuff
-    $DB->delete_records('timezone');
-
-/// Insert all the new stuff
-    foreach ($timezones as $timezone) {
-        if (is_array($timezone)) {
-            $timezone = (object)$timezone;
-        }
-        $DB->insert_record('timezone', $timezone);
-    }
-}
-
 /**
  * Increment standard revision field.
  *
index da0ef3b..193dad3 100644 (file)
         <INDEX NAME="timemodified" UNIQUE="false" FIELDS="timemodified"/>
       </INDEXES>
     </TABLE>
-    <TABLE NAME="timezone" COMMENT="Rules for calculating local wall clock time for users">
-      <FIELDS>
-        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
-        <FIELD NAME="name" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
-        <FIELD NAME="year" TYPE="int" LENGTH="11" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-        <FIELD NAME="tzrule" TYPE="char" LENGTH="20" NOTNULL="true" SEQUENCE="false"/>
-        <FIELD NAME="gmtoff" TYPE="int" LENGTH="11" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-        <FIELD NAME="dstoff" TYPE="int" LENGTH="11" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-        <FIELD NAME="dst_month" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-        <FIELD NAME="dst_startday" TYPE="int" LENGTH="3" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-        <FIELD NAME="dst_weekday" TYPE="int" LENGTH="3" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-        <FIELD NAME="dst_skipweeks" TYPE="int" LENGTH="3" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-        <FIELD NAME="dst_time" TYPE="char" LENGTH="6" NOTNULL="true" DEFAULT="00:00" SEQUENCE="false"/>
-        <FIELD NAME="std_month" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-        <FIELD NAME="std_startday" TYPE="int" LENGTH="3" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-        <FIELD NAME="std_weekday" TYPE="int" LENGTH="3" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-        <FIELD NAME="std_skipweeks" TYPE="int" LENGTH="3" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-        <FIELD NAME="std_time" TYPE="char" LENGTH="6" NOTNULL="true" DEFAULT="00:00" SEQUENCE="false"/>
-      </FIELDS>
-      <KEYS>
-        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
-      </KEYS>
-    </TABLE>
     <TABLE NAME="user" COMMENT="One record for each person">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
index 0383b29..43b640a 100644 (file)
@@ -110,16 +110,6 @@ $functions = array(
         'capabilities'  => 'moodle/comment:view',
     ),
 
-    // Completion related functions.
-
-    'core_completion_get_activities_completion_status' => array(
-        'classname'     => 'core_completion_external',
-        'methodname'    => 'get_activities_completion_status',
-        'description'   => 'Return the activities completion status for a user in a course.',
-        'type'          => 'read',
-        'capabilities'  => '',
-    ),
-
     // Grade related functions.
 
     'core_grades_get_grades' => array(
@@ -513,6 +503,15 @@ $functions = array(
         'capabilities'  => '',
     ),
 
+    'core_user_view_user_list' => array(
+        'classname'     => 'core_user_external',
+        'methodname'    => 'view_user_list',
+        'classpath'     => 'user/externallib.php',
+        'description'   => 'Simulates the web-interface view of user/index.php (triggering events).',
+        'type'          => 'write',
+        'capabilities'  => 'moodle/course:viewparticipants',
+    ),
+
     // === enrol related functions ===
 
     'core_enrol_get_enrolled_users_with_capability' => array(
@@ -838,6 +837,15 @@ $functions = array(
         'capabilities'  => '',
     ),
 
+    'core_message_mark_message_read' => array(
+        'classname'     => 'core_message_external',
+        'methodname'    => 'mark_message_read',
+        'classpath'     => 'message/externallib.php',
+        'description'   => 'Mark a single message as read, trigger message_viewed event.',
+        'type'          => 'write',
+        'capabilities'  => '',
+    ),
+
     // === notes related functions ===
 
     'moodle_notes_create_notes' => array(
@@ -885,6 +893,15 @@ $functions = array(
         'capabilities'=> 'moodle/notes:manage',
     ),
 
+    'core_notes_view_notes' => array(
+        'classname'     => 'core_notes_external',
+        'methodname'    => 'view_notes',
+        'classpath'     => 'notes/externallib.php',
+        'description'   => 'Simulates the web interface view of notes/index.php: trigger events.',
+        'type'          => 'write',
+        'capabilities'  => 'moodle/notes:view',
+    ),
+
     // === grading related functions ===
 
     'core_grading_get_definitions' => array(
@@ -1004,6 +1021,22 @@ $functions = array(
         'description' => 'Update completion status for the current user in an activity, only for activities with manual tracking.',
         'type'        => 'write',
     ),
+
+    'core_completion_get_activities_completion_status' => array(
+        'classname'     => 'core_completion_external',
+        'methodname'    => 'get_activities_completion_status',
+        'description'   => 'Return the activities completion status for a user in a course.',
+        'type'          => 'read',
+        'capabilities'  => '',
+    ),
+
+    'core_completion_get_course_completion_status' => array(
+        'classname'    => 'core_completion_external',
+        'methodname'   => 'get_course_completion_status',
+        'description'  => 'Returns course completion status.',
+        'type'         => 'read',
+        'capabilities' => 'report/completion:view',
+    ),
 );
 
 $services = array(
@@ -1068,6 +1101,10 @@ $services = array(
             'core_course_view_course',
             'core_completion_get_activities_completion_status',
             'core_notes_get_course_notes',
+            'core_completion_get_course_completion_status',
+            'core_user_view_user_list',
+            'core_message_mark_message_read',
+            'core_notes_view_notes',
             ),
         'enabled' => 0,
         'restrictedusers' => 0,
index f5461f7..5ee7f94 100644 (file)
@@ -4270,5 +4270,39 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2015032000.00);
     }
 
+    if ($oldversion < 2015040200.01) {
+        // Force uninstall of deleted tool.
+        if (!file_exists("$CFG->dirroot/$CFG->admin/tool/timezoneimport")) {
+            // Remove capabilities.
+            capabilities_cleanup('tool_timezoneimport');
+            // Remove all other associated config.
+            unset_all_config_for_plugin('tool_timezoneimport');
+        }
+        upgrade_main_savepoint(true, 2015040200.01);
+    }
+
+    if ($oldversion < 2015040200.02) {
+        // Define table timezone to be dropped.
+        $table = new xmldb_table('timezone');
+        // Conditionally launch drop table for timezone.
+        if ($dbman->table_exists($table)) {
+            $dbman->drop_table($table);
+        }
+        upgrade_main_savepoint(true, 2015040200.02);
+    }
+
+    if ($oldversion < 2015040200.03) {
+        if (isset($CFG->timezone) and $CFG->timezone == 99) {
+            // Migrate to real server timezone.
+            unset_config('timezone');
+        }
+        upgrade_main_savepoint(true, 2015040200.03);
+    }
+
+    if ($oldversion < 2015040700.01) {
+        $DB->delete_records('config_plugins', array('name' => 'requiremodintro'));
+        upgrade_main_savepoint(true, 2015040700.01);
+    }
+
     return true;
 }
index 7751186..840734e 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+/**
+ * Convert region timezone to php supported timezone
+ *
+ * @deprecated since Moodle 2.9
+ * @param string $tz value from ical file
+ * @return string $tz php supported timezone
+ */
+function calendar_normalize_tz($tz) {
+    debugging('calendar_normalize_tz() is deprecated, use core_date::normalise_timezone() instead', DEBUG_DEVELOPER);
+    return core_date::normalise_timezone($tz);
+}
+
+/**
+ * Returns a float which represents the user's timezone difference from GMT in hours
+ * Checks various settings and picks the most dominant of those which have a value
+ * @deprecated since Moodle 2.9
+ * @param float|int|string $tz timezone user timezone
+ * @return float
+ */
+function get_user_timezone_offset($tz = 99) {
+    debugging('get_user_timezone_offset() is deprecated, use PHP DateTimeZone instead', DEBUG_DEVELOPER);
+    $tz = core_date::get_user_timezone($tz);
+    $date = new DateTime('now', new DateTimeZone($tz));
+    return ($date->getOffset() - dst_offset_on(time(), $tz)) / (3600.0);
+}
+
+/**
+ * Returns an int which represents the systems's timezone difference from GMT in seconds
+ * @deprecated since Moodle 2.9
+ * @param float|int|string $tz timezone for which offset is required.
+ *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
+ * @return int|bool if found, false is timezone 99 or error
+ */
+function get_timezone_offset($tz) {
+    debugging('get_timezone_offset() is deprecated, use PHP DateTimeZone instead', DEBUG_DEVELOPER);
+    $date = new DateTime('now', new DateTimeZone(core_date::normalise_timezone($tz)));
+    return $date->getOffset() - dst_offset_on(time(), $tz);
+}
+
+/**
+ * Returns a list of timezones in the current language.
+ * @deprecated since Moodle 2.9
+ * @return array
+ */
+function get_list_of_timezones() {
+    debugging('update_timezone_records() is deprecated, use core_date::get_list_of_timezones() instead', DEBUG_DEVELOPER);
+    return core_date::get_list_of_timezones();
+}
+
+/**
+ * Previous internal API, it was not supposed to be used anywhere.
+ * @deprecated since Moodle 2.9
+ * @param array $timezones
+ */
+function update_timezone_records($timezones) {
+    debugging('update_timezone_records() is not available any more, use standard PHP date/time code', DEBUG_DEVELOPER);
+}
+
+/**
+ * Previous internal API, it was not supposed to be used anywhere.
+ * @deprecated since Moodle 2.9
+ * @param int $fromyear
+ * @param int $toyear
+ * @param mixed $strtimezone
+ * @return bool
+ */
+function calculate_user_dst_table($fromyear = null, $toyear = null, $strtimezone = null) {
+    debugging('calculate_user_dst_table() is not available any more, use standard PHP date/time code', DEBUG_DEVELOPER);
+    return false;
+}
+
+/**
+ * Previous internal API, it was not supposed to be used anywhere.
+ * @deprecated since Moodle 2.9
+ * @param int|string $year
+ * @param mixed $timezone
+ * @return null
+ */
+function dst_changes_for_year($year, $timezone) {
+    debugging('dst_changes_for_year() is not available any more, use standard PHP date/time code', DEBUG_DEVELOPER);
+    return null;
+}
+
+/**
+ * Previous internal API, it was not supposed to be used anywhere.
+ * @deprecated since Moodle 2.9
+ * @param string $timezonename
+ * @return array
+ */
+function get_timezone_record($timezonename) {
+    debugging('get_timezone_record() is not available any more, use standard PHP date/time code', DEBUG_DEVELOPER);
+    return array();
+}
+
 /**
  * Add an entry to the legacy log table.
  *
index 49ae208..f6e4e24 100644 (file)
@@ -41,19 +41,9 @@ require_once($CFG->libdir.'/formslib.php');
  * @copyright  2012 Rajesh Taneja
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class core_form_dateselector_testcase extends basic_testcase {
+class core_form_dateselector_testcase extends advanced_testcase {
     /** @var MoodleQuickForm Keeps reference of dummy form object */
     private $mform;
-    /** @var stdClass saves current user data */
-    private $olduser;
-    /** @var int|float|string saves forcetimezone config variable */
-    private $cfgforcetimezone;
-    /** @var int|float|string saves current user timezone */
-    private $userstimezone;
-    /** @var string saves system locale */
-    private $oldlocale;
-    /** @var string saves system timezone */
-    private $systemdefaulttimezone;
     /** @var array test fixtures */
     private $testvals;
 
@@ -61,7 +51,14 @@ class core_form_dateselector_testcase extends basic_testcase {
      * Initalize test wide variable, it is called in start of the testcase
      */
     protected function setUp() {
+        global $CFG;
         parent::setUp();
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $this->setTimezone('Australia/Perth');
+
         // Get form data.
         $form = new temp_form_date();
         $this->mform = $form->getform();
@@ -119,15 +116,6 @@ class core_form_dateselector_testcase extends basic_testcase {
         );
     }
 
-    /**
-     * Clears the data set in the setUp() method call.
-     * @see dateselector_form_element_testcase::setUp()
-     */
-    protected function tearDown() {
-        unset($this->testvals);
-        parent::tearDown();
-    }
-
     /**
      * Testcase to check exportvalue
      */
@@ -135,9 +123,6 @@ class core_form_dateselector_testcase extends basic_testcase {
         global $USER;
         $testvals = $this->testvals;
 
-        // Set timezone to Australia/Perth for testing.
-        $this->settimezone();
-
         foreach ($testvals as $vals) {
             // Set user timezone to test value.
             $USER->timezone = $vals['usertimezone'];
@@ -151,9 +136,6 @@ class core_form_dateselector_testcase extends basic_testcase {
             $this->assertSame(array('dateselector' => $vals['timestamp']), $el->exportValue($submitvalues),
                     "Please check if timezones are updated (Site adminstration -> location -> update timezone)");
         }
-
-        // Restore user original timezone.
-        $this->restoretimezone();
     }
 
     /**
@@ -164,8 +146,7 @@ class core_form_dateselector_testcase extends basic_testcase {
         $testvals = $this->testvals;
         // Get dummy form for data.
         $mform = $this->mform;
-        // Set timezone to Australia/Perth for testing.
-        $this->settimezone();
+
         foreach ($testvals as $vals) {
             // Set user timezone to test value.
             $USER->timezone = $vals['usertimezone'];
@@ -183,58 +164,6 @@ class core_form_dateselector_testcase extends basic_testcase {
             $el->onQuickFormEvent('updateValue', null, $mform);
             $this->assertSame($expectedvalues, $el->getValue());
         }
-
-        // Restore user original timezone.
-        $this->restoretimezone();
-    }
-
-    /**
-     * Set user timezone to Australia/Perth for testing.
-     */
-    private function settimezone() {
-        global $USER, $CFG, $DB;
-        $this->olduser = $USER;
-        $USER = $DB->get_record('user', array('id'=>2)); // Admin.
-
-        // Check if forcetimezone is set then save it and set it to use user timezone.
-        $this->cfgforcetimezone = null;
-        if (isset($CFG->forcetimezone)) {
-            $this->cfgforcetimezone = $CFG->forcetimezone;
-            $CFG->forcetimezone = 99; //get user default timezone.
-        }
-
-        // Store user default timezone to restore later.
-        $this->userstimezone = $USER->timezone;
-
-        // The string version of date comes from server locale setting and does
-        // not respect user language, so it is necessary to reset that.
-        $this->oldlocale = setlocale(LC_TIME, '0');
-        setlocale(LC_TIME, 'en_AU.UTF-8');
-
-        // Set default timezone to Australia/Perth, else time calculated
-        // will not match expected values. Before that save system defaults.
-        $this->systemdefaulttimezone = date_default_timezone_get();
-        date_default_timezone_set('Australia/Perth');
-    }
-
-    /**
-     * Restore user timezone to original state
-     */
-    private function restoretimezone() {
-        global $USER, $CFG;
-        // Restore user timezone back to what it was.
-        $USER->timezone = $this->userstimezone;
-
-        // Restore forcetimezone.
-        if (!is_null($this->cfgforcetimezone)) {
-            $CFG->forcetimezone = $this->cfgforcetimezone;
-        }
-
-        // Restore system default values.
-        date_default_timezone_set($this->systemdefaulttimezone);
-        setlocale(LC_TIME, $this->oldlocale);
-
-        $USER = $this->olduser;
     }
 }
 
index 9c85c79..ba188ee 100644 (file)
@@ -41,19 +41,9 @@ require_once($CFG->libdir.'/formslib.php');
  * @copyright  2012 Rajesh Taneja
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class core_form_datetimeselector_testcase extends basic_testcase {
+class core_form_datetimeselector_testcase extends advanced_testcase {
     /** @var MoodleQuickForm Keeps reference of dummy form object */
     private $mform;
-    /** @var stdClass saves current user data */
-    private $olduser;
-    /** @var int|float|string saves forcetimezone config variable */
-    private $cfgforcetimezone;
-    /** @var int|float|string saves current user timezone */
-    private $userstimezone;
-    /** @var string saves system locale */
-    private $oldlocale;
-    /** @var string saves system timezone */
-    private $systemdefaulttimezone;
     /** @var array test fixtures */
     private $testvals;
 
@@ -61,7 +51,14 @@ class core_form_datetimeselector_testcase extends basic_testcase {
      * Initalize test wide variable, it is called in start of the testcase
      */
     protected function setUp() {
+        global $CFG;
         parent::setUp();
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $this->setTimezone('Australia/Perth');
+
         // Get form data.
         $form = new temp_form_datetime();
         $this->mform = $form->getform();
@@ -131,15 +128,6 @@ class core_form_datetimeselector_testcase extends basic_testcase {
         );
     }
 
-    /**
-     * Clears the data set in the setUp() method call.
-     * @see datetimeselector_form_element_testcase::setUp()
-     */
-    protected function tearDown() {
-        unset($this->testvals);
-        parent::tearDown();
-    }
-
     /**
      * Testcase to check exportvalue
      */
@@ -147,9 +135,6 @@ class core_form_datetimeselector_testcase extends basic_testcase {
         global $USER;
         $testvals = $this->testvals;
 
-        // Set timezone to Australia/Perth for testing.
-        $this->settimezone();
-
         foreach ($testvals as $vals) {
             // Set user timezone to test value.
             $USER->timezone = $vals['usertimezone'];
@@ -163,9 +148,6 @@ class core_form_datetimeselector_testcase extends basic_testcase {
             $this->assertSame(array('dateselector' => $vals['timestamp']), $el->exportValue($submitvalues),
                     "Please check if timezones are updated (Site adminstration -> location -> update timezone)");
         }
-
-        // Restore user original timezone.
-        $this->restoretimezone();
     }
 
     /**
@@ -176,8 +158,7 @@ class core_form_datetimeselector_testcase extends basic_testcase {
         $testvals = $this->testvals;
         // Get dummy form for data.
         $mform = $this->mform;
-        // Set timezone to Australia/Perth for testing.
-        $this->settimezone();
+
         foreach ($testvals as $vals) {
             // Set user timezone to test value.
             $USER->timezone = $vals['usertimezone'];
@@ -197,58 +178,6 @@ class core_form_datetimeselector_testcase extends basic_testcase {
             $el->onQuickFormEvent('updateValue', null, $mform);
             $this->assertSame($expectedvalues, $el->getValue());
         }
-
-        // Restore user original timezone.
-        $this->restoretimezone();
-    }
-
-    /**
-     * Set user timezone to Australia/Perth for testing
-     */
-    private function settimezone() {
-        global $USER, $CFG, $DB;
-        $this->olduser = $USER;
-        $USER = $DB->get_record('user', array('id'=>2)); //admin
-
-        // Check if forcetimezone is set then save it and set it to use user timezone.
-        $this->cfgforcetimezone = null;
-        if (isset($CFG->forcetimezone)) {
-            $this->cfgforcetimezone = $CFG->forcetimezone;
-            $CFG->forcetimezone = 99; //get user default timezone.
-        }
-
-        // Store user default timezone to restore later.
-        $this->userstimezone = $USER->timezone;
-
-        // The string version of date comes from server locale setting and does
-        // not respect user language, so it is necessary to reset that.
-        $this->oldlocale = setlocale(LC_TIME, '0');
-        setlocale(LC_TIME, 'en_AU.UTF-8');
-
-        // Set default timezone to Australia/Perth, else time calculated
-        // will not match expected values. Before that save system defaults.
-        $this->systemdefaulttimezone = date_default_timezone_get();
-        date_default_timezone_set('Australia/Perth');
-    }
-
-    /**
-     * Restore user timezone to original state
-     */
-    private function restoretimezone() {
-        global $USER, $CFG;
-        // Restore user timezone back to what it was.
-        $USER->timezone = $this->userstimezone;
-
-        // Restore forcetimezone.
-        if (!is_null($this->cfgforcetimezone)) {
-            $CFG->forcetimezone = $this->cfgforcetimezone;
-        }
-
-        // Restore system default values.
-        date_default_timezone_set($this->systemdefaulttimezone);
-        setlocale(LC_TIME, $this->oldlocale);
-
-        $USER = $this->olduser;
     }
 }
 
index 4b1307e..1c2be1d 100644 (file)
@@ -94,6 +94,7 @@ class google_docs {
             // return a more specific Exception, that's why the global Exception class is caught here.
             return $files;
         }
+        date_default_timezone_set(core_date::get_user_timezone());
         foreach ($xml->entry as $gdoc) {
             $docid  = (string) $gdoc->children('http://schemas.google.com/g/2005')->resourceId;
             list($type, $docid) = explode(':', $docid);
@@ -129,10 +130,11 @@ class google_docs {
             $files[] =  array( 'title' => $title,
                 'url' => "{$gdoc->link[0]->attributes()->href}",
                 'source' => $source,
-                'date'   => usertime(strtotime($gdoc->updated)),
+                'date'   => strtotime($gdoc->updated),
                 'thumbnail' => (string) $OUTPUT->pix_url(file_extension_icon($title, 32))
             );
         }
+        core_date::set_default_server_timezone();
 
         return $files;
     }
index b80bb30..1cd4af5 100644 (file)
@@ -2036,23 +2036,15 @@ function get_user_preferences($name = null, $default = null, $user = null) {
  * @return int GMT timestamp
  */
 function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) {
+    $date = new DateTime('now', core_date::get_user_timezone_object($timezone));
+    $date->setDate((int)$year, (int)$month, (int)$day);
+    $date->setTime((int)$hour, (int)$minute, (int)$second);
 
-    // Save input timezone, required for dst offset check.
-    $passedtimezone = $timezone;
+    $time = $date->getTimestamp();
 
-    $timezone = get_user_timezone_offset($timezone);
-
-    if (abs($timezone) > 13) {
-        // Server time.
-        $time = mktime((int)$hour, (int)$minute, (int)$second, (int)$month, (int)$day, (int)$year);
-    } else {
-        $time = gmmktime((int)$hour, (int)$minute, (int)$second, (int)$month, (int)$day, (int)$year);
-        $time = usertime($time, $timezone);
-
-        // Apply dst for string timezones or if 99 then try dst offset with user's default timezone.
-        if ($applydst && ((99 == $passedtimezone) || !is_numeric($passedtimezone))) {
-            $time -= dst_offset_on($time, $passedtimezone);
-        }
+    // Moodle BC DST stuff.
+    if (!$applydst) {
+        $time += dst_offset_on($time, $timezone);
     }
 
     return $time;
@@ -2176,13 +2168,9 @@ function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour
  * If we are running under Windows convert to Windows encoding and then back to UTF-8
  * (because it's impossible to specify UTF-8 to fetch locale info in Win32).
  *
- * This function does not do any calculation regarding the user preferences and should
- * therefore receive the final date timestamp, format and timezone. Timezone being only used
- * to differentiate the use of server time or not (strftime() against gmstrftime()).
- *
- * @param int $date the timestamp.
+ * @param int $date the timestamp - since Moodle 2.9 this is a real UTC timestamp
  * @param string $format strftime format.
- * @param int|float $tz the numerical timezone, typically returned by {@link get_user_timezone_offset()}.
+ * @param int|float|string $tz the user timezone
  * @return string the formatted date/time.
  * @since Moodle 2.3.3
  */
@@ -2196,23 +2184,18 @@ function date_format_string($date, $format, $tz = 99) {
         $localewincharset = $calendartype->locale_win_charset();
     }
 
-    if (abs($tz) > 13) {
-        if ($localewincharset) {
-            $format = core_text::convert($format, 'utf-8', $localewincharset);
-            $datestring = strftime($format, $date);
-            $datestring = core_text::convert($datestring, $localewincharset, 'utf-8');
-        } else {
-            $datestring = strftime($format, $date);
-        }
-    } else {
-        if ($localewincharset) {
-            $format = core_text::convert($format, 'utf-8', $localewincharset);
-            $datestring = gmstrftime($format, $date);
-            $datestring = core_text::convert($datestring, $localewincharset, 'utf-8');
-        } else {
-            $datestring = gmstrftime($format, $date);
-        }
+    if ($localewincharset) {
+        $format = core_text::convert($format, 'utf-8', $localewincharset);
+    }
+
+    date_default_timezone_set(core_date::get_user_timezone($tz));
+    $datestring = strftime($format, $date);
+    core_date::set_default_server_timezone();
+
+    if ($localewincharset) {
+        $datestring = core_text::convert($datestring, $localewincharset, 'utf-8');
     }
+
     return $datestring;
 }
 
@@ -2222,81 +2205,37 @@ function date_format_string($date, $format, $tz = 99) {
  *
  * @package core
  * @category time
- * @uses HOURSECS
  * @param int $time Timestamp in GMT
- * @param float|int|string $timezone offset's time with timezone, if float and not 99, then no
- *        dst offset is applied {@link http://docs.moodle.org/dev/Time_API#Timezone}
+ * @param float|int|string $timezone user timezone
  * @return array An array that represents the date in user time
  */
 function usergetdate($time, $timezone=99) {
+    date_default_timezone_set(core_date::get_user_timezone($timezone));
+    $result = getdate($time);
+    core_date::set_default_server_timezone();
 
-    // Save input timezone, required for dst offset check.
-    $passedtimezone = $timezone;
-
-    $timezone = get_user_timezone_offset($timezone);
-
-    if (abs($timezone) > 13) {
-        // Server time.
-        return getdate($time);
-    }
-
-    // Add daylight saving offset for string timezones only, as we can't get dst for
-    // float values. if timezone is 99 (user default timezone), then try update dst.
-    if ($passedtimezone == 99 || !is_numeric($passedtimezone)) {
-        $time += dst_offset_on($time, $passedtimezone);
-    }
-
-    $time += intval((float)$timezone * HOURSECS);
-
-    $datestring = gmstrftime('%B_%A_%j_%Y_%m_%w_%d_%H_%M_%S', $time);
-
-    // Be careful to ensure the returned array matches that produced by getdate() above.
-    list(
-        $getdate['month'],
-        $getdate['weekday'],
-        $getdate['yday'],
-        $getdate['year'],
-        $getdate['mon'],
-        $getdate['wday'],
-        $getdate['mday'],
-        $getdate['hours'],
-        $getdate['minutes'],
-        $getdate['seconds']
-    ) = explode('_', $datestring);
-
-    // Set correct datatype to match with getdate().
-    $getdate['seconds'] = (int)$getdate['seconds'];
-    $getdate['yday'] = (int)$getdate['yday'] - 1; // The function gmstrftime returns 0 through 365.
-    $getdate['year'] = (int)$getdate['year'];
-    $getdate['mon'] = (int)$getdate['mon'];
-    $getdate['wday'] = (int)$getdate['wday'];
-    $getdate['mday'] = (int)$getdate['mday'];
-    $getdate['hours'] = (int)$getdate['hours'];
-    $getdate['minutes'] = (int)$getdate['minutes'];
-    return $getdate;
+    return $result;
 }
 
 /**
  * Given a GMT timestamp (seconds since epoch), offsets it by
  * the timezone.  eg 3pm in India is 3pm GMT - 7 * 3600 seconds
  *
+ * NOTE: this function does not include DST properly,
+ *       you should use the PHP date stuff instead!
+ *
  * @package core
  * @category time
- * @uses HOURSECS
  * @param int $date Timestamp in GMT
- * @param float|int|string $timezone timezone to calculate GMT time offset before
- *        calculating user time, 99 is default user timezone
- *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
+ * @param float|int|string $timezone user timezone
  * @return int
  */
 function usertime($date, $timezone=99) {
+    $userdate = new DateTime('@' . $date);
+    $userdate->setTimezone(core_date::get_user_timezone_object($timezone));
+    $dst = dst_offset_on($date, $timezone);
 
-    $timezone = get_user_timezone_offset($timezone);
-
-    if (abs($timezone) > 13) {
-        return $date;
-    }
-    return $date - (int)($timezone * HOURSECS);
+    return $date - $userdate->getOffset() + $dst;
 }
 
 /**
@@ -2306,9 +2245,7 @@ function usertime($date, $timezone=99) {
  * @package core
  * @category time
  * @param int $date Timestamp in GMT
- * @param float|int|string $timezone timezone to calculate GMT time offset before
- *        calculating user midnight time, 99 is default user timezone
- *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
+ * @param float|int|string $timezone user timezone
  * @return int Returns a GMT timestamp
  */
 function usergetmidnight($date, $timezone=99) {
@@ -2325,86 +2262,12 @@ function usergetmidnight($date, $timezone=99) {
  *
  * @package core
  * @category time
- * @param float|int|string $timezone timezone to calculate GMT time offset before
- *        calculating user timezone, 99 is default user timezone
- *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
+ * @param float|int|string $timezone user timezone
  * @return string
  */
 function usertimezone($timezone=99) {
-
-    $tz = get_user_timezone($timezone);
-
-    if (!is_float($tz)) {
-        return $tz;
-    }
-
-    if (abs($tz) > 13) {
-        // Server time.
-        return get_string('serverlocaltime');
-    }
-
-    if ($tz == intval($tz)) {
-        // Don't show .0 for whole hours.
-        $tz = intval($tz);
-    }
-
-    if ($tz == 0) {
-        return 'UTC';
-    } else if ($tz > 0) {
-        return 'UTC+'.$tz;
-    } else {
-        return 'UTC'.$tz;
-    }
-
-}
-
-/**
- * Returns a float which represents the user's timezone difference from GMT in hours
- * Checks various settings and picks the most dominant of those which have a value
- *
- * @package core
- * @category time
- * @param float|int|string $tz timezone to calculate GMT time offset for user,
- *        99 is default user timezone
- *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
- * @return float
- */
-function get_user_timezone_offset($tz = 99) {
-    $tz = get_user_timezone($tz);
-
-    if (is_float($tz)) {
-        return $tz;
-    } else {
-        $tzrecord = get_timezone_record($tz);
-        if (empty($tzrecord)) {
-            return 99.0;
-        }
-        return (float)$tzrecord->gmtoff / HOURMINS;
-    }
-}
-
-/**
- * Returns an int which represents the systems's timezone difference from GMT in seconds
- *
- * @package core
- * @category time
- * @param float|int|string $tz timezone for which offset is required.
- *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
- * @return int|bool if found, false is timezone 99 or error
- */
-function get_timezone_offset($tz) {
-    if ($tz == 99) {
-        return false;
-    }
-
-    if (is_numeric($tz)) {
-        return intval($tz * 60*60);
-    }
-
-    if (!$tzrecord = get_timezone_record($tz)) {
-        return false;
-    }
-    return intval($tzrecord->gmtoff * 60);
+    $tz = core_date::get_user_timezone($timezone);
+    return core_date::get_localised_timezone($tz);
 }
 
 /**
@@ -2439,198 +2302,6 @@ function get_user_timezone($tz = 99) {
     return is_numeric($tz) ? (float) $tz : $tz;
 }
 
-/**
- * Returns cached timezone record for given $timezonename
- *
- * @package core
- * @param string $timezonename name of the timezone
- * @return stdClass|bool timezonerecord or false
- */
-function get_timezone_record($timezonename) {
-    global $DB;
-    static $cache = null;
-
-    if ($cache === null) {
-        $cache = array();
-    }
-
-    if (isset($cache[$timezonename])) {
-        return $cache[$timezonename];
-    }
-
-    return $cache[$timezonename] = $DB->get_record_sql('SELECT * FROM {timezone}
-                                                        WHERE name = ? ORDER BY year DESC', array($timezonename), IGNORE_MULTIPLE);
-}
-
-/**
- * Build and store the users Daylight Saving Time (DST) table
- *
- * @package core
- * @param int $fromyear Start year for the table, defaults to 1971
- * @param int $toyear End year for the table, defaults to 2035
- * @param int|float|string $strtimezone timezone to check if dst should be applied.
- * @return bool
- */
-function calculate_user_dst_table($fromyear = null, $toyear = null, $strtimezone = null) {
-    global $SESSION, $DB;
-
-    $usertz = get_user_timezone($strtimezone);
-
-    if (is_float($usertz)) {
-        // Trivial timezone, no DST.
-        return false;
-    }
-
-    if (!empty($SESSION->dst_offsettz) && $SESSION->dst_offsettz != $usertz) {
-        // We have pre-calculated values, but the user's effective TZ has changed in the meantime, so reset.
-        unset($SESSION->dst_offsets);
-        unset($SESSION->dst_range);
-    }
-
-    if (!empty($SESSION->dst_offsets) && empty($fromyear) && empty($toyear)) {
-        // Repeat calls which do not request specific year ranges stop here, we have already calculated the table.
-        // This will be the return path most of the time, pretty light computationally.
-        return true;
-    }
-
-    // Reaching here means we either need to extend our table or create it from scratch.
-
-    // Remember which TZ we calculated these changes for.
-    $SESSION->dst_offsettz = $usertz;
-
-    if (empty($SESSION->dst_offsets)) {
-        // If we 're creating from scratch, put the two guard elements in there.
-        $SESSION->dst_offsets = array(1 => null, 0 => null);
-    }
-    if (empty($SESSION->dst_range)) {
-        // If creating from scratch.
-        $from = max((empty($fromyear) ? intval(date('Y')) - 3 : $fromyear), 1971);
-        $to   = min((empty($toyear)   ? intval(date('Y')) + 3 : $toyear),   2035);
-
-        // Fill in the array with the extra years we need to process.
-        $yearstoprocess = array();
-        for ($i = $from; $i <= $to; ++$i) {
-            $yearstoprocess[] = $i;
-        }
-
-        // Take note of which years we have processed for future calls.
-        $SESSION->dst_range = array($from, $to);
-    } else {
-        // If needing to extend the table, do the same.
-        $yearstoprocess = array();
-
-        $from = max((empty($fromyear) ? $SESSION->dst_range[0] : $fromyear), 1971);
-        $to   = min((empty($toyear)   ? $SESSION->dst_range[1] : $toyear),   2035);
-
-        if ($from < $SESSION->dst_range[0]) {
-            // Take note of which years we need to process and then note that we have processed them for future calls.
-            for ($i = $from; $i < $SESSION->dst_range[0]; ++$i) {
-                $yearstoprocess[] = $i;
-            }
-            $SESSION->dst_range[0] = $from;
-        }
-        if ($to > $SESSION->dst_range[1]) {
-            // Take note of which years we need to process and then note that we have processed them for future calls.
-            for ($i = $SESSION->dst_range[1] + 1; $i <= $to; ++$i) {
-                $yearstoprocess[] = $i;
-            }
-            $SESSION->dst_range[1] = $to;
-        }
-    }
-
-    if (empty($yearstoprocess)) {
-        // This means that there was a call requesting a SMALLER range than we have already calculated.
-        return true;
-    }
-
-    // From now on, we know that the array has at least the two guard elements, and $yearstoprocess has the years we need
-    // Also, the array is sorted in descending timestamp order!
-
-    // Get DB data.
-
-    static $presetscache = array();
-    if (!isset($presetscache[$usertz])) {
-        $presetscache[$usertz] = $DB->get_records('timezone', array('name' => $usertz),
-            'year DESC', 'year, gmtoff, dstoff, dst_month, dst_startday, dst_weekday, dst_skipweeks, dst_time, std_month, '.
-            'std_startday, std_weekday, std_skipweeks, std_time');
-    }
-    if (empty($presetscache[$usertz])) {
-        return false;
-    }
-
-    // Remove ending guard (first element of the array).
-    reset($SESSION->dst_offsets);
-    unset($SESSION->dst_offsets[key($SESSION->dst_offsets)]);
-
-    // Add all required change timestamps.
-    foreach ($yearstoprocess as $y) {
-        // Find the record which is in effect for the year $y.
-        foreach ($presetscache[$usertz] as $year => $preset) {
-            if ($year <= $y) {
-                break;
-            }
-        }
-
-        $changes = dst_changes_for_year($y, $preset);
-
-        if ($changes === null) {
-            continue;
-        }
-        if ($changes['dst'] != 0) {
-            $SESSION->dst_offsets[$changes['dst']] = $preset->dstoff * MINSECS;
-        }
-        if ($changes['std'] != 0) {
-            $SESSION->dst_offsets[$changes['std']] = 0;
-        }
-    }
-
-    // Put in a guard element at the top.
-    $maxtimestamp = max(array_keys($SESSION->dst_offsets));
-    $SESSION->dst_offsets[($maxtimestamp + DAYSECS)] = null; // DAYSECS is arbitrary, any "small" number will do.
-
-    // Sort again.
-    krsort($SESSION->dst_offsets);
-
-    return true;
-}
-
-/**
- * Calculates the required DST change and returns a Timestamp Array
- *
- * @package core
- * @category time
- * @uses HOURSECS
- * @uses MINSECS
- * @param int|string $year Int or String Year to focus on
- * @param object $timezone Instatiated Timezone object
- * @return array|null Array dst => xx, 0 => xx, std => yy, 1 => yy or null
- */
-function dst_changes_for_year($year, $timezone) {
-
-    if ($timezone->dst_startday == 0 && $timezone->dst_weekday == 0 &&
-        $timezone->std_startday == 0 && $timezone->std_weekday == 0) {
-        return null;
-    }
-
-    $monthdaydst = find_day_in_month($timezone->dst_startday, $timezone->dst_weekday, $timezone->dst_month, $year);
-    $monthdaystd = find_day_in_month($timezone->std_startday, $timezone->std_weekday, $timezone->std_month, $year);
-
-    list($dsthour, $dstmin) = explode(':', $timezone->dst_time);
-    list($stdhour, $stdmin) = explode(':', $timezone->std_time);
-
-    $timedst = make_timestamp($year, $timezone->dst_month, $monthdaydst, 0, 0, 0, 99, false);
-    $timestd = make_timestamp($year, $timezone->std_month, $monthdaystd, 0, 0, 0, 99, false);
-
-    // Instead of putting hour and minute in make_timestamp(), we add them afterwards.
-    // This has the advantage of being able to have negative values for hour, i.e. for timezones
-    // where GMT time would be in the PREVIOUS day than the local one on which DST changes.
-
-    $timedst += $dsthour * HOURSECS + $dstmin * MINSECS;
-    $timestd += $stdhour * HOURSECS + $stdmin * MINSECS;
-
-    return array('dst' => $timedst, 0 => $timedst, 'std' => $timestd, 1 => $timestd);
-}
-
 /**
  * Calculates the Daylight Saving Offset for a given date/time (timestamp)
  * - Note: Daylight saving only works for string timezones and not for float.
@@ -2638,48 +2309,20 @@ function dst_changes_for_year($year, $timezone) {
  * @package core
  * @category time
  * @param int $time must NOT be compensated at all, it has to be a pure timestamp
- * @param int|float|string $strtimezone timezone for which offset is expected, if 99 or null
- *        then user's default timezone is used. {@link http://docs.moodle.org/dev/Time_API#Timezone}
+ * @param int|float|string $strtimezone user timezone
  * @return int
  */
 function dst_offset_on($time, $strtimezone = null) {
-    global $SESSION;
-
-    if (!calculate_user_dst_table(null, null, $strtimezone) || empty($SESSION->dst_offsets)) {
-        return 0;
-    }
-
-    reset($SESSION->dst_offsets);
-    while (list($from, $offset) = each($SESSION->dst_offsets)) {
-        if ($from <= $time) {
-            break;
-        }
-    }
-
-    // This is the normal return path.
-    if ($offset !== null) {
-        return $offset;
-    }
-
-    // Reaching this point means we haven't calculated far enough, do it now:
-    // Calculate extra DST changes if needed and recurse. The recursion always
-    // moves toward the stopping condition, so will always end.
-
-    if ($from == 0) {
-        // We need a year smaller than $SESSION->dst_range[0].
-        if ($SESSION->dst_range[0] == 1971) {
-            return 0;
-        }
-        calculate_user_dst_table($SESSION->dst_range[0] - 5, null, $strtimezone);
-        return dst_offset_on($time, $strtimezone);
-    } else {
-        // We need a year larger than $SESSION->dst_range[1].
-        if ($SESSION->dst_range[1] == 2035) {
-            return 0;
+    $tz = core_date::get_user_timezone($strtimezone);
+    $date = new DateTime('@' . $time);
+    $date->setTimezone(new DateTimeZone($tz));
+    if ($date->format('I') == '1') {
+        if ($tz === 'Australia/Lord_Howe') {
+            return 1800;
         }
-        calculate_user_dst_table(null, $SESSION->dst_range[1] + 5, $strtimezone);
-        return dst_offset_on($time, $strtimezone);
+        return 3600;
     }
+    return 0;
 }
 
 /**
@@ -7106,53 +6749,6 @@ function get_list_of_themes() {
     return $themes;
 }
 
-/**
- * Returns a list of timezones in the current language
- *
- * @return array
- */
-function get_list_of_timezones() {
-    global $DB;
-
-    static $timezones;
-
-    if (!empty($timezones)) {    // This function has been called recently.
-        return $timezones;
-    }
-
-    $timezones = array();
-
-    if ($rawtimezones = $DB->get_records_sql("SELECT MAX(id), name FROM {timezone} GROUP BY name")) {
-        foreach ($rawtimezones as $timezone) {
-            if (!empty($timezone->name)) {
-                if (get_string_manager()->string_exists(strtolower($timezone->name), 'timezones')) {
-                    $timezones[$timezone->name] = get_string(strtolower($timezone->name), 'timezones');
-                } else {
-                    $timezones[$timezone->name] = $timezone->name;
-                }
-                if (substr($timezones[$timezone->name], 0, 1) == '[') {  // No translation found.
-                    $timezones[$timezone->name] = $timezone->name;
-                }
-            }
-        }
-    }
-
-    asort($timezones);
-
-    for ($i = -13; $i <= 13; $i += .5) {
-        $tzstring = 'UTC';
-        if ($i < 0) {
-            $timezones[sprintf("%.1f", $i)] = $tzstring . $i;
-        } else if ($i > 0) {
-            $timezones[sprintf("%.1f", $i)] = $tzstring . '+' . $i;
-        } else {
-            $timezones[sprintf("%.1f", $i)] = $tzstring;
-        }
-    }
-
-    return $timezones;
-}
-
 /**
  * Factory function for emoticon_manager
  *
diff --git a/lib/olson.php b/lib/olson.php
deleted file mode 100644 (file)
index 2150f86..0000000
+++ /dev/null
@@ -1,710 +0,0 @@
-<?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/>.
-
-/**
- * @package   moodlecore
- * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-/**
- * olson_to_timezones ($filename)
- *
- * Parses the olson files for Zones and DST rules.
- * It updates the Moodle database with the Zones/DST rules
- *
- * @param string $filename
- * @return bool true/false
- *
- */
-function olson_to_timezones ($filename) {
-
-    // Look for zone and rule information up to 10 years in the future.
-    $maxyear = localtime(time(), true);
-    $maxyear = $maxyear['tm_year'] + 1900 + 10;
-
-    $zones = olson_simple_zone_parser($filename, $maxyear);
-    $rules = olson_simple_rule_parser($filename, $maxyear);
-
-    $mdl_zones = array();
-
-    /**
-     *** To translate the combined Zone & Rule changes
-     *** in the Olson files to the Moodle single ruleset
-     *** format, we need to trasverse every year and see
-     *** if either the Zone or the relevant Rule has a
-     *** change. It's yuck but it yields a rationalized
-     *** set of data, which is arguably simpler.
-     ***
-     *** Also note that I am starting at the epoch (1970)
-     *** because I don't think we'll see many events scheduled
-     *** before that, anyway.
-     ***
-     **/
-
-    foreach ($zones as $zname => $zbyyear) { // loop over zones
-        /**
-         *** Loop over years, only adding a rule when zone or rule
-         *** have changed. All loops preserver the last seen vars
-         *** until there's an explicit decision to delete them
-         ***
-         **/
-
-        // clean the slate for a new zone
-        $zone = NULL;
-        $rule = NULL;
-
-        //
-        // Find the pre 1970 zone rule entries
-        //
-        for ($y = 1970 ; $y >= 0 ; $y--) {
-            if (array_key_exists((string)$y, $zbyyear )) { // we have a zone entry for the year
-                $zone = $zbyyear[$y];
-                //print_object("Zone $zname pre1970 is in  $y\n");
-                break; // Perl's last -- get outta here
-            }
-        }
-        if (!empty($zone['rule']) && array_key_exists($zone['rule'], $rules)) {
-            $rule = NULL;
-            for ($y = 1970 ; $y > 0 ; $y--) {
-                if (array_key_exists((string)$y, $rules[$zone['rule']] )) { // we have a rule entry for the year
-                    $rule  =  $rules[$zone['rule']][$y];
-                    //print_object("Rule $rule[name] pre1970 is $y\n");
-                    break; // Perl's last -- get outta here
-                }
-
-            }
-            if (empty($rule)) {
-                // Colombia and a few others refer to rules before they exist
-                // Perhaps we should comment out this warning...
-                // trigger_error("Cannot find rule in $zone[rule] <= 1970");
-                $rule  = array();
-            }
-        } else {
-            // no DST this year!
-            $rule  = array();
-        }
-
-        // Prepare to insert the base 1970 zone+rule
-        if (!empty($rule) && array_key_exists($zone['rule'], $rules)) {
-            // merge the two arrays into the moodle rule
-            unset($rule['name']); // warning: $rule must NOT be a reference!
-            unset($rule['year']);
-            $mdl_tz = array_merge($zone, $rule);
-
-            //fix (de)activate_time (AT) field to be GMT
-            $mdl_tz['dst_time'] = olson_parse_at($mdl_tz['dst_time'], 'set',   $mdl_tz['gmtoff']);
-            $mdl_tz['std_time'] = olson_parse_at($mdl_tz['std_time'], 'reset', $mdl_tz['gmtoff']);
-        } else {
-            // just a simple zone
-            $mdl_tz = $zone;
-            // TODO: Add other default values here!
-            $mdl_tz['dstoff'] = 0;
-        }
-
-        // Fix the from year to 1970
-        $mdl_tz['year'] = 1970;
-
-        // add to the array
-        $mdl_zones[] = $mdl_tz;
-        //print_object("Zero entry for $zone[name] added");
-
-        $lasttimezone = $mdl_tz;
-
-        ///
-        /// 1971 onwards
-        ///
-        for ($y = 1971; $y < $maxyear ; $y++) {
-            $changed = false;
-            ///
-            /// We create a "zonerule" entry if either zone or rule change...
-            ///
-            /// force $y to string to avoid PHP
-            /// thinking of a positional array
-            ///
-            if (array_key_exists((string)$y, $zbyyear)) { // we have a zone entry for the year
-                $changed = true;
-                $zone    = $zbyyear[(string)$y];
-            }
-            if (!empty($zone['rule']) && array_key_exists($zone['rule'], $rules)) {
-                if (array_key_exists((string)$y, $rules[$zone['rule']])) {
-                    $changed = true;
-                    $rule    = $rules[$zone['rule']][(string)$y];
-                }
-            } else {
-                $rule = array();
-            }
-
-            if ($changed) {
-                //print_object("CHANGE YEAR $y Zone $zone[name] Rule $zone[rule]\n");
-                if (!empty($rule)) {
-                    // merge the two arrays into the moodle rule
-                    unset($rule['name']);
-                    unset($rule['year']);
-                    $mdl_tz = array_merge($zone, $rule);
-
-                    // VERY IMPORTANT!!
-                    $mdl_tz['year'] = $y;
-
-                    //fix (de)activate_time (AT) field to be GMT
-                    $mdl_tz['dst_time'] = olson_parse_at($mdl_tz['dst_time'], 'set',   $mdl_tz['gmtoff']);
-                    $mdl_tz['std_time'] = olson_parse_at($mdl_tz['std_time'], 'reset', $mdl_tz['gmtoff']);
-                } else {
-                    // just a simple zone
-                    $mdl_tz = $zone;
-                }
-
-/*
-if(isset($mdl_tz['dst_time']) && !strpos($mdl_tz['dst_time'], ':') || isset($mdl_tz['std_time']) &&  !strpos($mdl_tz['std_time'], ':')) {
-    print_object($mdl_tz);
-    print_object('---');
-}
-*/
-                // This is the simplest way to make the != operator just below NOT take the year into account
-                $lasttimezone['year'] = $mdl_tz['year'];
-
-                // If not a duplicate, add and update $lasttimezone
-                if($lasttimezone != $mdl_tz) {
-                    $mdl_zones[] = $lasttimezone = $mdl_tz;
-                }
-            }
-        }
-
-    }
-
-    /*
-    if (function_exists('memory_get_usage')) {
-        trigger_error("We are consuming this much memory: " . get_memory_usage());
-    }
-    */
-
-/// Since Moodle 1.7, rule is tzrule in DB (reserved words problem), so change it here
-/// after everything is calculated to be properly loaded to the timezone table.
-/// Pre 1.7 users won't have the old rule if updating this from moodle.org but it
-/// seems that such field isn't used at all by the rest of Moodle (at least I haven't
-/// found any use when looking for it).
-
-    foreach($mdl_zones as $key=>$mdl_zone) {
-        $mdl_zones[$key]['tzrule'] = $mdl_zones[$key]['rule'];
-    }
-
-    return $mdl_zones;
-}
-
-
-/**
- * olson_simple_rule_parser($filename)
- *
- * Parses the olson files for DST rules.
- * It's a simple implementation that simplifies some fields
- *
- * @return array a multidimensional array, or false on error
- *
- */
-function olson_simple_rule_parser($filename, $maxyear) {
-
-    $file = fopen($filename, 'r', 0);
-
-    if (empty($file)) {
-        return false;
-    }
-
-    while ($line = fgets($file)) {
-        // only pay attention to rules lines
-        if(!preg_match('/^Rule\s/', $line)){
-            continue;
-        }
-        $line = preg_replace('/\n$/', '',$line); // chomp
-        $rule = preg_split('/\s+/', $line);
-        list($discard,
-             $name,
-             $from,
-             $to,
-             $type,
-             $in,
-             $on,
-             $at,
-             $save,
-             $letter) = $rule;
-    }
-
-    fseek($file, 0);
-
-    $rules = array();
-    while ($line = fgets($file)) {
-        // only pay attention to rules lines
-        if(!preg_match('/^Rule\s/', $line)){
-            continue;
-        }
-        $line = preg_replace('/\n$/', '',$line); // chomp
-        $rule = preg_split('/\s+/', $line);
-        list($discard,
-             $name,
-             $from,
-             $to,
-             $type,
-             $in,
-             $on,
-             $at,
-             $save,
-             $letter) = $rule;
-
-        $srs = ($save === '0') ? 'reset' : 'set';
-
-        if($to == 'only') {
-            $to = $from;
-        }
-        else if($to == 'max') {
-            $to = $maxyear;
-        }
-
-        for($i = $from; $i <= $to; ++$i) {
-            $rules[$name][$i][$srs] = $rule;
-        }
-
-    }
-
-    fclose($file);
-
-    $months = array('jan' =>  1, 'feb' =>  2,
-                    'mar' =>  3, 'apr' =>  4,
-                    'may' =>  5, 'jun' =>  6,
-                    'jul' =>  7, 'aug' =>  8,
-                    'sep' =>  9, 'oct' => 10,
-                    'nov' => 11, 'dec' => 12);
-
-
-    // now reformat it a bit to match Moodle's DST table
-    $moodle_rules = array();
-    foreach ($rules as $rule => $rulesbyyear) {
-        foreach ($rulesbyyear as $year => $rulesthisyear) {
-
-            if(!isset($rulesthisyear['reset'])) {
-                // No "reset" rule. We will assume that this is somewhere in the southern hemisphere
-                // after a period of not using DST, otherwise it doesn't make sense at all.
-                // With that assumption, we can put in a fake reset e.g. on Jan 1, 12:00.
-                /*
-                print_object("no reset");
-                print_object($rules);
-                die();
-                */
-                $rulesthisyear['reset'] = array(
-                    NULL, NULL, NULL, NULL, NULL, 'jan', 1, '12:00', '00:00', NULL
-                );
-            }
-
-            if(!isset($rulesthisyear['set'])) {
-                // No "set" rule. We will assume that this is somewhere in the southern hemisphere
-                // and that it begins a period of not using DST, otherwise it doesn't make sense at all.
-                // With that assumption, we can put in a fake set on Dec 31, 12:00, shifting time by 0 minutes.
-                $rulesthisyear['set'] = array(
-                    NULL, $rulesthisyear['reset'][1], NULL, NULL, NULL, 'dec', 31, '12:00', '00:00', NULL
-                );
-            }
-
-            list($discard,
-                 $name,
-                 $from,
-                 $to,
-                 $type,
-                 $in,
-                 $on,
-                 $at,
-                 $save,
-                 $letter) = $rulesthisyear['set'];
-
-            $moodle_rule = array();
-
-            // $save is sometimes just minutes
-            // and othertimes HH:MM -- only
-            // parse if relevant
-            if (!preg_match('/^\d+$/', $save)) {
-                list($hours, $mins) = explode(':', $save);
-                $save = $hours * 60 + $mins;
-            }
-
-            // we'll parse $at later
-            // $at = olson_parse_at($at);
-            $in = strtolower($in);
-            if(!isset($months[$in])) {
-                trigger_error('Unknown month: '.$in);
-            }
-
-            $moodle_rule['name']   = $name;
-            $moodle_rule['year']   = $year;
-            $moodle_rule['dstoff'] = $save; // time offset
-
-            $moodle_rule['dst_month'] = $months[$in]; // the month
-            $moodle_rule['dst_time']  = $at; // the time
-
-            // Encode index and day as per Moodle's specs
-            $on = olson_parse_on($on);
-            $moodle_rule['dst_startday']  = $on['startday'];
-            $moodle_rule['dst_weekday']   = $on['weekday'];
-            $moodle_rule['dst_skipweeks'] = $on['skipweeks'];
-
-            // and now the "deactivate" data
-            list($discard,
-                 $name,
-                 $from,
-                 $to,
-                 $type,
-                 $in,
-                 $on,
-                 $at,
-                 $save,
-                 $letter) = $rulesthisyear['reset'];
-
-            // we'll parse $at later
-            // $at = olson_parse_at($at);
-            $in = strtolower($in);
-            if(!isset($months[$in])) {
-                trigger_error('Unknown month: '.$in);
-            }
-
-            $moodle_rule['std_month'] = $months[$in]; // the month
-            $moodle_rule['std_time']  = $at; // the time
-
-            // Encode index and day as per Moodle's specs
-            $on = olson_parse_on($on);
-            $moodle_rule['std_startday']  = $on['startday'];
-            $moodle_rule['std_weekday']   = $on['weekday'];
-            $moodle_rule['std_skipweeks'] = $on['skipweeks'];
-
-            $moodle_rules[$moodle_rule['name']][$moodle_rule['year']] = $moodle_rule;
-            //print_object($moodle_rule);
-
-        } // end foreach year within a rule
-
-        // completed with all the entries for this rule
-        // if the last entry has a TO other than 'max'
-        // then we have to deal with closing the last rule
-        //trigger_error("Rule $name ending to $to");
-        if (!empty($to) && $to !== 'max') {
-            // We can handle two cases for TO:
-            // a year, or "only"
-            $reset_rule = $moodle_rule;
-            $reset_rule['dstoff'] = '00';
-            if (preg_match('/^\d+$/', $to)){
-                $reset_rule['year'] = $to;
-                $moodle_rules[$reset_rule['name']][$reset_rule['year']] = $reset_rule;
-            } elseif ($to === 'only') {
-                $reset_rule['year'] = $reset_rule['year'] + 1;
-                $moodle_rules[$reset_rule['name']][$reset_rule['year']] = $reset_rule;
-            } else {
-                trigger_error("Strange value in TO $to rule field for rule $name");
-            }
-
-        } // end if $to is interesting
-
-    } // end foreach rule
-
-    return $moodle_rules;
-}
-
-/**
- * olson_simple_zone_parser($filename)
- *
- * Parses the olson files for zone info
- *
- * @return array a multidimensional array, or false on error
- *
- */
-function olson_simple_zone_parser($filename, $maxyear) {
-
-    $file = fopen($filename, 'r', 0);
-
-    if (empty($file)) {
-        return false;
-    }
-
-    $zones = array();
-    $lastzone = NULL;
-
-    while ($line = fgets($file)) {
-        // skip obvious non-zone lines
-        if (preg_match('/^#/', $line)) {
-            continue;
-        }
-        if (preg_match('/^(?:Rule|Link|Leap)/',$line)) {
-            $lastzone = NULL; // reset lastzone
-            continue;
-        }
-
-        // If there are blanks in the start of the line but the first non-ws character is a #,
-        // assume it's an "inline comment". The funny thing is that this happens only during
-        // the definition of the Rule for Europe/Athens.
-        if(substr(trim($line), 0, 1) == '#') {
-            continue;
-        }
-
-        /*** Notes
-         ***
-         *** By splitting on space, we are only keeping the
-         *** year of the UNTIL field -- that's on purpose.
-         ***
-         *** The Zone lines are followed by continuation lines
-         *** were we reuse the info from the last one seen.
-         ***
-         *** We are transforming "until" fields into "from" fields
-         *** which make more sense from the Moodle perspective, so
-         *** each initial Zone entry is "from" the year 0, and for the
-         *** continuation lines, we shift the "until" from the previous field
-         *** into this line's "from".
-         ***
-         *** If a RULES field contains a time instead of a rule we discard it
-         *** I have no idea of how to create a DST rule out of that
-         *** (what are the start/end times?)
-         ***
-         *** We remove "until" from the data we keep, but preserve
-         *** it in $lastzone.
-         */
-        if (preg_match('/^Zone/', $line)) { // a new zone
-            $line = trim($line);
-            $line = preg_split('/\s+/', $line);
-            $zone = array();
-            list( $discard, // 'Zone'
-                  $zone['name'],
-                  $zone['gmtoff'],
-                  $zone['rule'],
-                  $discard // format
-                  ) = $line;
-            // the things we do to avoid warnings
-            if (!empty($line[5])) {
-                $zone['until'] = $line[5];
-            }
-            $zone['year'] = '0';
-
-            $zones[$zone['name']] = array();
-
-        } else if (!empty($lastzone) && preg_match('/^\s+/', $line)){
-            // looks like a credible continuation line
-            $line = trim($line);
-            $line = preg_split('/\s+/', $line);
-            if (count($line) < 3) {
-                $lastzone = NULL;
-                continue;
-            }
-            // retrieve info from the lastzone
-            $zone = $lastzone;
-            $zone['year'] = $zone['until'];
-            // overwrite with current data
-            list(
-                  $zone['gmtoff'],
-                  $zone['rule'],
-                  $discard // format
-                  ) = $line;
-            // the things we do to avoid warnings
-            if (!empty($line[3])) {
-                $zone['until'] = $line[3];
-            }
-
-        } else {
-            $lastzone = NULL;
-            continue;
-        }
-
-        // tidy up, we're done
-        // perhaps we should insert in the DB at this stage?
-        $lastzone = $zone;
-        unset($zone['until']);
-        $zone['gmtoff'] = olson_parse_offset($zone['gmtoff']);
-        if ($zone['rule'] === '-') { // cleanup empty rules
-            $zone['rule'] = '';
-        }
-        if (preg_match('/:/',$zone['rule'])) {
-            // we are not handling direct SAVE rules here
-            // discard it
-            $zone['rule'] = '';
-        }
-
-        $zones[$zone['name']][(string)$zone['year']] = $zone;
-    }
-
-    return $zones;
-}
-
-/**
- * olson_parse_offset($offset)
- *
- * parses time offsets from the GMTOFF and SAVE
- * fields into +/-MINUTES
- *
- * @return int
- */
-function olson_parse_offset ($offset) {
-    $offset = trim($offset);
-
-    // perhaps it's just minutes
-    if (preg_match('/^(-?)(\d*)$/', $offset)) {
-        return intval($offset);
-    }
-    // (-)hours:minutes(:seconds)
-    if (preg_match('/^(-?)(\d*):(\d+)/', $offset, $matches)) {
-        // we are happy to discard the seconds
-        $sign    = $matches[1];
-        $hours   = intval($matches[2]);
-        $seconds = intval($matches[3]);
-        $offset  = $sign . ($hours*60 + $seconds);
-        return intval($offset);
-    }
-
-    trigger_error('Strange time format in olson_parse_offset() ' .$offset);
-    return 0;
-
-}
-
-
-/**
- * olson_parse_on_($on)
- *
- * see `man zic`. This function translates the following formats
- * 5        the fifth of the month
- * lastSun  the last Sunday in the month
- * lastMon  the last Monday in the month
- * Sun>=8   first Sunday on or after the eighth
- * Sun<=25  last Sunday on or before the 25th
- *
- * to a moodle friendly format. Returns an array with:
- *
- * startday: the day of the month that we start counting from.
- *           if negative, it means we start from that day and
- *           count backwards. since -1 would be meaningless,
- *           it means "end of month and backwards".
- * weekday:  the day of the week that we must find. we will
- *           scan days from the startday until we find the
- *           first such weekday. 0...6 = Sun...Sat.
- *           -1 means that any day of the week will do,
- *           effectively ending the search on startday.
- * skipweeks:after finding our end day as outlined above,
- *           skip this many weeks. this enables us to find
- *           "the second sunday >= 10". usually will be 0.
- */
-function olson_parse_on ($on) {
-
-    $rule = array();
-    $days = array('sun' => 0, 'mon' => 1,
-                  'tue' => 2, 'wed' => 3,
-                  'thu' => 4, 'fri' => 5,
-                  'sat' => 6);
-
-    if(is_numeric($on)) {
-        $rule['startday']  = intval($on); // Start searching from that day
-        $rule['weekday']   = -1;          // ...and stop there, no matter what weekday
-        $rule['skipweeks'] = 0;           // Don't skip any weeks.
-    }
-    else {
-        $on = strtolower($on);
-        if(substr($on, 0, 4) == 'last') {
-            // e.g. lastSun
-            if(!isset($days[substr($on, 4)])) {
-                trigger_error('Unknown last weekday: '.substr($on, 4));
-            }
-            else {
-                $rule['startday']  = -1;                    // Start from end of month
-                $rule['weekday']   = $days[substr($on, 4)]; // Find the first such weekday
-                $rule['skipweeks'] = 0;                     // Don't skip any weeks.
-            }
-        }
-        else if(substr($on, 3, 2) == '>=') {
-            // e.g. Sun>=8
-            if(!isset($days[substr($on, 0, 3)])) {
-                trigger_error('Unknown >= weekday: '.substr($on, 0, 3));
-            }
-            else {
-                $rule['startday']  = intval(substr($on, 5));   // Start from that day of the month
-                $rule['weekday']   = $days[substr($on, 0, 3)]; // Find the first such weekday
-                $rule['skipweeks'] = 0;                        // Don't skip any weeks.
-            }
-        }
-        else if(substr($on, 3, 2) == '<=') {
-            // e.g. Sun<=25
-            if(!isset($days[substr($on, 0, 3)])) {
-                trigger_error('Unknown <= weekday: '.substr($on, 0, 3));
-            }
-            else {
-                $rule['startday']  = -intval(substr($on, 5));  // Start from that day of the month; COUNT BACKWARDS (minus sign)
-                $rule['weekday']   = $days[substr($on, 0, 3)]; // Find the first such weekday
-                $rule['skipweeks'] = 0;                        // Don't skip any weeks.
-            }
-        }
-        else {
-            trigger_error('unknown on '.$on);
-        }
-    }
-    return $rule;
-}
-
-
-/**
- * olson_parse_at($at, $set, $gmtoffset)
- *
- * see `man zic`. This function translates
- *
- *      2        time in hours
- *      2:00     time in hours and minutes
- *     15:00     24-hour format time (for times after noon)
- *      1:28:14  time in hours, minutes, and seconds
- *
- *  Any of these forms may be followed by the letter w if the given
- *  time is local "wall clock" time, s if the given time  is  local
- *  "standard"  time, or u (or g or z) if the given time is univer-
- *  sal time; in the absence of an indicator, wall  clock  time  is
- *  assumed.
- *
- * @return string a moodle friendly $at, in GMT, which is what Moodle wants
- *
- *
- */
-function olson_parse_at ($at, $set = 'set', $gmtoffset) {
-
-    // find the time "signature";
-    $sig = '';
-    if (preg_match('/[ugzs]$/', $at, $matches)) {
-        $sig = $matches[0];
-        $at  = substr($at, 0, strlen($at)-1); // chop
-    }
-
-    $at = (strpos($at, ':') === false) ? $at . ':0' : $at;
-    list($hours, $mins) = explode(':', $at);
-
-    // GMT -- return as is!
-    if ( !empty($sig) && ( $sig === 'u'
-                           || $sig === 'g'
-                           || $sig === 'z'    )) {
-        return $at;
-    }
-
-    // Wall clock
-    if (empty($sig) || $sig === 'w') {
-        if ($set !== 'set'){ // wall clock is on DST, assume by 1hr
-            $hours = $hours-1;
-        }
-        $sig = 's';
-    }
-
-    // Standard time
-    if (!empty($sig) && $sig === 's') {
-        $mins = $mins + $hours*60 + $gmtoffset;
-        $hours = $mins / 60;
-        $hours = (int)$hours;
-        $mins  = abs($mins % 60);
-        return sprintf('%02d:%02d', $hours, $mins);
-    }
-
-    trigger_error('unhandled case - AT flag is ' . $matches[0]);
-}
index 511f945..ac02a97 100644 (file)
@@ -501,6 +501,19 @@ abstract class advanced_testcase extends PHPUnit_Framework_TestCase {
         self::setUser(1);
     }
 
+    /**
+     * Change server and default php timezones.
+     *
+     * @param string $servertimezone timezone to set in $CFG->timezone (not validated)
+     * @param string $defaultphptimezone timezone to fake default php timezone (must be valid)
+     */
+    public static function setTimezone($servertimezone = 'Australia/Perth', $defaultphptimezone = 'Australia/Perth') {
+        global $CFG;
+        $CFG->timezone = $servertimezone;
+        core_date::phpunit_override_default_php_timezone($defaultphptimezone);
+        core_date::set_default_server_timezone();
+    }
+
     /**
      * Get data generator
      * @static
index d127691..79f17db 100644 (file)
@@ -163,6 +163,15 @@ class phpunit_util extends testing_util {
                 $warnings[] = 'Warning: unexpected change of $COURSE';
             }
 
+            if ($CFG->ostype === 'WINDOWS') {
+                if (setlocale(LC_TIME, 0) !== 'English_Australia.1252') {
+                    $warnings[] = 'Warning: unexpected change of locale';
+                }
+            } else {
+                if (setlocale(LC_TIME, 0) !== 'en_AU.UTF-8') {
+                    $warnings[] = 'Warning: unexpected change of locale';
+                }
+            }
         }
 
         if (ini_get('max_execution_time') != 0) {
@@ -242,6 +251,16 @@ class phpunit_util extends testing_util {
         // fix PHP settings
         error_reporting($CFG->debug);
 
+        // Reset the date/time class.
+        core_date::phpunit_reset();
+
+        // Make sure the time locale is consistent - that is Australian English.
+        if ($CFG->ostype === 'WINDOWS') {
+            setlocale(LC_TIME, 'English_Australia.1252');
+        } else {
+            setlocale(LC_TIME, 'en_AU.UTF-8');
+        }
+
         // verify db writes just in case something goes wrong in reset
         if (self::$lastdbwrites != $DB->perf_get_writes()) {
             error_log('Unexpected DB writes in phpunit_util::reset_all_data()');
@@ -423,10 +442,6 @@ class phpunit_util extends testing_util {
         // So each time we reset the dataroot before running a test, the default files are still installed.
         self::save_original_data_files();
 
-        // install timezone info
-        $timezones = get_records_csv($CFG->libdir.'/timezone.txt', 'timezone');
-        update_timezone_records($timezones);
-
         // Store version hash in the database and in a file.
         self::store_versions_hash();
 
index 1caf138..1bd4d58 100644 (file)
@@ -552,4 +552,93 @@ class core_phpunit_advanced_testcase extends advanced_testcase {
     public function test_message_redirection_reset() {
         $this->assertFalse(phpunit_util::is_redirecting_messages(), 'Test reset must stop message redirection.');
     }
+
+    public function test_set_timezone() {
+        global $CFG;
+        $this->resetAfterTest();
+
+        $this->assertSame('Australia/Perth', $CFG->timezone);
+        $this->assertSame('Australia/Perth', date_default_timezone_get());
+
+        $this->setTimezone('Pacific/Auckland', 'Europe/Prague');
+        $this->assertSame('Pacific/Auckland', $CFG->timezone);
+        $this->assertSame('Pacific/Auckland', date_default_timezone_get());
+
+        $this->setTimezone('99', 'Europe/Prague');
+        $this->assertSame('99', $CFG->timezone);
+        $this->assertSame('Europe/Prague', date_default_timezone_get());
+
+        $this->setTimezone('xxx', 'Europe/Prague');
+        $this->assertSame('xxx', $CFG->timezone);
+        $this->assertSame('Europe/Prague', date_default_timezone_get());
+
+        $this->setTimezone();
+        $this->assertSame('Australia/Perth', $CFG->timezone);
+        $this->assertSame('Australia/Perth', date_default_timezone_get());
+
+        try {
+            $this->setTimezone('Pacific/Auckland', '');
+        } catch (Exception $e) {
+            $this->assertInstanceOf('PHPUnit_Framework_Error_Warning', $e);
+        }
+
+        try {
+            $this->setTimezone('Pacific/Auckland', 'xxxx');
+        } catch (Exception $e) {
+            $this->assertInstanceOf('PHPUnit_Framework_Error_Warning', $e);
+        }
+
+        try {
+            $this->setTimezone('Pacific/Auckland', null);
+        } catch (Exception $e) {
+            $this->assertInstanceOf('PHPUnit_Framework_Error_Warning', $e);
+        }
+
+    }
+
+    public function test_locale_reset() {
+        global $CFG;
+
+        $this->resetAfterTest();
+
+        // If this fails \phpunit_util::reset_all_data() must be updated.
+        $this->assertSame('en_AU.UTF-8', get_string('locale', 'langconfig'));
+        $this->assertSame('English_Australia.1252', get_string('localewin', 'langconfig'));
+
+        if ($CFG->ostype === 'WINDOWS') {
+            $this->assertSame('English_Australia.1252', setlocale(LC_TIME, 0));
+            setlocale(LC_TIME, 'English_USA.1252');
+        } else {
+            $this->assertSame('en_AU.UTF-8', setlocale(LC_TIME, 0));
+            setlocale(LC_TIME, 'en_US.UTF-8');
+        }
+
+        try {
+            phpunit_util::reset_all_data(true);
+        } catch (Exception $e) {
+            $this->assertInstanceOf('PHPUnit_Framework_Error_Warning', $e);
+        }
+
+        if ($CFG->ostype === 'WINDOWS') {
+            $this->assertSame('English_Australia.1252', setlocale(LC_TIME, 0));
+        } else {
+            $this->assertSame('en_AU.UTF-8', setlocale(LC_TIME, 0));
+        }
+
+        if ($CFG->ostype === 'WINDOWS') {
+            $this->assertSame('English_Australia.1252', setlocale(LC_TIME, 0));
+            setlocale(LC_TIME, 'English_USA.1252');
+        } else {
+            $this->assertSame('en_AU.UTF-8', setlocale(LC_TIME, 0));
+            setlocale(LC_TIME, 'en_US.UTF-8');
+        }
+
+        phpunit_util::reset_all_data(false);
+
+        if ($CFG->ostype === 'WINDOWS') {
+            $this->assertSame('English_Australia.1252', setlocale(LC_TIME, 0));
+        } else {
+            $this->assertSame('en_AU.UTF-8', setlocale(LC_TIME, 0));
+        }
+    }
 }
index 09afdba..4a63ba8 100644 (file)
@@ -140,11 +140,6 @@ if (defined('BEHAT_SITE_RUNNING')) {
     }
 }
 
-// Force timezone to be Australia/Perth for behat tests, so we don't get time zone problems.
-if (defined('BEHAT_SITE_RUNNING') || defined('BEHAT_TEST') || defined('BEHAT_UTIL')) {
-    @date_default_timezone_set('Australia/Perth');
-}
-
 // Normalise dataroot - we do not want any symbolic links, trailing / or any other weirdness there
 if (!isset($CFG->dataroot)) {
     if (isset($_SERVER['REMOTE_ADDR'])) {
@@ -284,14 +279,8 @@ if (!defined('CACHE_DISABLE_STORES')) {
     define('CACHE_DISABLE_STORES', false);
 }
 
-// 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')) {
-    $olddebug = error_reporting(0);
-    date_default_timezone_set(date_default_timezone_get());
-    error_reporting($olddebug);
-    unset($olddebug);
-}
+// Servers should define a default timezone in php.ini, but if they don't then make sure no errors are shown.
+date_default_timezone_set(@date_default_timezone_get());
 
 // Detect CLI scripts - CLI scripts are executed from command line, do not have session and we do not want HTML in output
 // In your new CLI scripts just add "define('CLI_SCRIPT', true);" before requiring config.php.
@@ -589,6 +578,9 @@ if (defined('COMPONENT_CLASSLOADER')) {
     spl_autoload_register('core_component::classloader');
 }
 
+// Remember the default PHP timezone, we will need it later.
+core_date::store_default_php_timezone();
+
 // Load up standard libraries
 require_once($CFG->libdir .'/filterlib.php');       // Functions for filtering test as it is output
 require_once($CFG->libdir .'/ajax/ajaxlib.php');    // Functions for managing our use of JavaScript and YUI
@@ -712,6 +704,9 @@ ini_set('arg_separator.output', '&amp;');
 // Work around for a PHP bug   see MDL-11237
 ini_set('pcre.backtrack_limit', 20971520);  // 20 MB
 
+// Set PHP default timezone to server timezone.
+core_date::set_default_server_timezone();
+
 // Location of standard files
 $CFG->wordlist = $CFG->libdir .'/wordlist.txt';
 $CFG->moddata  = 'moddata';
index 65cb7c4..23fa555 100644 (file)
@@ -983,42 +983,33 @@ function stats_get_start_from($str) {
 /**
  * Start of day
  * @param int $time timestamp
- * @return start of day
+ * @return int start of day
  */
 function stats_get_base_daily($time=0) {
-    global $CFG;
-
     if (empty($time)) {
         $time = time();
     }
-    if ($CFG->timezone == 99) {
-        $time = strtotime(date('d-M-Y', $time));
-        return $time;
-    } else {
-        $offset = get_timezone_offset($CFG->timezone);
-        $gtime = $time + $offset;
-        $gtime = intval($gtime / (60*60*24)) * 60*60*24;
-        return $gtime - $offset;
-    }
+
+    core_date::set_default_server_timezone();
+    $time = strtotime(date('d-M-Y', $time));
+
+    return $time;
 }
 
 /**
  * Start of week
  * @param int $time timestamp
- * @return start of week
+ * @return int start of week
  */
 function stats_get_base_weekly($time=0) {
     global $CFG;
 
     $time = stats_get_base_daily($time);
     $startday = $CFG->calendar_startwday;
-    if ($CFG->timezone == 99) {
-        $thisday = date('w', $time);
-    } else {
-        $offset = get_timezone_offset($CFG->timezone);
-        $gtime = $time + $offset;
-        $thisday = gmdate('w', $gtime);
-    }
+
+    core_date::set_default_server_timezone();
+    $thisday = date('w', $time);
+
     if ($thisday > $startday) {
         $time = $time - (($thisday - $startday) * 60*60*24);
     } else if ($thisday < $startday) {
@@ -1030,27 +1021,17 @@ function stats_get_base_weekly($time=0) {
 /**
  * Start of month
  * @param int $time timestamp
- * @return start of month
+ * @return int start of month
  */
 function stats_get_base_monthly($time=0) {
-    global $CFG;
-
     if (empty($time)) {
         $time = time();
     }
-    if ($CFG->timezone == 99) {
-        return strtotime(date('1-M-Y', $time));
 
-    } else {
-        $time = stats_get_base_daily($time);
-        $offset = get_timezone_offset($CFG->timezone);
-        $gtime = $time + $offset;
-        $day = gmdate('d', $gtime);
-        if ($day == 1) {
-            return $time;
-        }
-        return $gtime - (($day-1) * 60*60*24);
-    }
+    core_date::set_default_server_timezone();
+    $return = strtotime(date('1-M-Y', $time));
+
+    return $return;
 }
 
 /**
index d616c77..b589033 100644 (file)
@@ -1763,6 +1763,288 @@ class core_accesslib_testcase extends advanced_testcase {
         }
     }
 
+    /**
+     * Test that enrolled users SQL does not return any values for users in
+     * other courses.
+     */
+    public function test_get_enrolled_sql_different_course() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+        $student = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
+        $user = $this->getDataGenerator()->create_user();
+
+        // This user should not appear anywhere, we're not interested in that context.
+        $course2 = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user->id, $course2->id, $student->id);
+
+        $enrolled   = get_enrolled_users($context, '', 0, 'u.id', null, 0, 0, false);
+        $active     = get_enrolled_users($context, '', 0, 'u.id', null, 0, 0, true);
+        $suspended  = get_suspended_userids($context);
+
+        $this->assertFalse(isset($enrolled[$user->id]));
+        $this->assertFalse(isset($active[$user->id]));
+        $this->assertFalse(isset($suspended[$user->id]));
+        $this->assertCount(0, $enrolled);
+        $this->assertCount(0, $active);
+        $this->assertCount(0, $suspended);
+    }
+
+    /**
+     * Test that enrolled users SQL does not return any values for role
+     * assignments without an enrolment.
+     */
+    public function test_get_enrolled_sql_role_only() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+        $student = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
+        $user = $this->getDataGenerator()->create_user();
+
+        // Role assignment is not the same as course enrollment.
+        role_assign($student->id, $user->id, $context->id);
+
+        $enrolled   = get_enrolled_users($context, '', 0, 'u.id', null, 0, 0, false);
+        $active     = get_enrolled_users($context, '', 0, 'u.id', null, 0, 0, true);
+        $suspended  = get_suspended_userids($context);
+
+        $this->assertFalse(isset($enrolled[$user->id]));
+        $this->assertFalse(isset($active[$user->id]));
+        $this->assertFalse(isset($suspended[$user->id]));
+        $this->assertCount(0, $enrolled);
+        $this->assertCount(0, $active);
+        $this->assertCount(0, $suspended);
+    }
+
+    /**
+     * Test that multiple enrolments for the same user are counted correctly.
+     */
+    public function test_get_enrolled_sql_multiple_enrolments() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+        $student = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
+        $user = $this->getDataGenerator()->create_user();
+
+        // Add a suspended enrol.
+        $selfinstance = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'self'));
+        $selfplugin = enrol_get_plugin('self');
+        $selfplugin->update_status($selfinstance, ENROL_INSTANCE_ENABLED);
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, $student->id, 'self', 0, 0, ENROL_USER_SUSPENDED);
+
+        // Should be enrolled, but not active - user is suspended.
+        $enrolled   = get_enrolled_users($context, '', 0, 'u.id', null, 0, 0, false);
+        $active     = get_enrolled_users($context, '', 0, 'u.id', null, 0, 0, true);
+        $suspended  = get_suspended_userids($context);
+
+        $this->assertTrue(isset($enrolled[$user->id]));
+        $this->assertFalse(isset($active[$user->id]));
+        $this->assertTrue(isset($suspended[$user->id]));
+        $this->assertCount(1, $enrolled);
+        $this->assertCount(0, $active);
+        $this->assertCount(1, $suspended);
+
+        // Add an active enrol for the user. Any active enrol makes them enrolled.
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, $student->id);
+
+        // User should be active now.
+        $enrolled   = get_enrolled_users($context, '', 0, 'u.id', null, 0, 0, false);
+        $active     = get_enrolled_users($context, '', 0, 'u.id', null, 0, 0, true);
+        $suspended  = get_suspended_userids($context);
+
+        $this->assertTrue(isset($enrolled[$user->id]));
+        $this->assertTrue(isset($active[$user->id]));
+        $this->assertFalse(isset($suspended[$user->id]));
+        $this->assertCount(1, $enrolled);
+        $this->assertCount(1, $active);
+        $this->assertCount(0, $suspended);
+
+    }
+
+    public function get_enrolled_sql_provider() {
+        return array(
+            array(
+                // Two users who are enrolled.
+                'users' => array(
+                    array(
+                        'enrolled'  => true,
+                        'active'    => true,
+                    ),
+                    array(
+                        'enrolled'  => true,
+                        'active'    => true,
+                    ),
+                ),
+                'counts' => array(
+                    'enrolled'      => 2,
+                    'active'        => 2,
+                    'suspended'     => 0,
+                ),
+            ),
+            array(
+                // A user who is suspended.
+                'users' => array(
+                    array(
+                        'status'    => ENROL_USER_SUSPENDED,
+                        'enrolled'  => true,
+                        'suspended' => true,
+                    ),
+                ),
+                'counts' => array(
+                    'enrolled'      => 1,
+                    'active'        => 0,
+                    'suspended'     => 1,
+                ),
+            ),
+            array(
+                // One of each.
+                'users' => array(
+                    array(
+                        'enrolled'  => true,
+                        'active'    => true,
+                    ),
+                    array(
+                        'status'    => ENROL_USER_SUSPENDED,
+                        'enrolled'  => true,
+                        'suspended' => true,
+                    ),
+                ),
+                'counts' => array(
+                    'enrolled'      => 2,
+                    'active'        => 1,
+                    'suspended'     => 1,
+                ),
+            ),
+            array(
+                // One user who is not yet enrolled.
+                'users' => array(
+                    array(
+                        'timestart' => DAYSECS,
+                        'enrolled'  => true,
+                        'active'    => false,
+                        'suspended' => true,
+                    ),
+                ),
+                'counts' => array(
+                    'enrolled'      => 1,
+                    'active'        => 0,
+                    'suspended'     => 1,
+                ),
+            ),
+            array(
+                // One user who is no longer enrolled
+                'users' => array(
+                    array(
+                        'timeend'   => -DAYSECS,
+                        'enrolled'  => true,
+                        'active'    => false,
+                        'suspended' => true,
+                    ),
+                ),
+                'counts' => array(
+                    'enrolled'      => 1,
+                    'active'        => 0,
+                    'suspended'     => 1,
+                ),
+            ),
+            array(
+                // One user who is not yet enrolled, and one who is no longer enrolled.
+                'users' => array(
+                    array(
+                        'timeend'   => -DAYSECS,
+                        'enrolled'  => true,
+                        'active'    => false,
+                        'suspended' => true,
+                    ),
+                    array(
+                        'timestart' => DAYSECS,
+                        'enrolled'  => true,
+                        'active'    => false,
+                        'suspended' => true,
+                    ),
+                ),
+                'counts' => array(
+                    'enrolled'      => 2,
+                    'active'        => 0,
+                    'suspended'     => 2,
+                ),
+            ),
+        );
+    }
+
+    /**
+     * @dataProvider get_enrolled_sql_provider
+     */
+    public function test_get_enrolled_sql_course($users, $counts) {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+        $student = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
+        $createdusers = array();
+
+        foreach ($users as &$userdata) {
+            $user = $this->getDataGenerator()->create_user();
+            $userdata['id'] = $user->id;
+
+            $timestart  = 0;
+            $timeend    = 0;
+            $status     = null;
+            if (isset($userdata['timestart'])) {
+                $timestart = time() + $userdata['timestart'];
+            }
+            if (isset($userdata['timeend'])) {
+                $timeend = time() + $userdata['timeend'];
+            }
+            if (isset($userdata['status'])) {
+                $status = $userdata['status'];
+            }
+
+            // Enrol the user in the course.
+            $this->getDataGenerator()->enrol_user($user->id, $course->id, $student->id, 'manual', $timestart, $timeend, $status);
+        }
+
+        // After all users have been enroled, check expectations.
+        $enrolled   = get_enrolled_users($context, '', 0, 'u.id', null, 0, 0, false);
+        $active     = get_enrolled_users($context, '', 0, 'u.id', null, 0, 0, true);
+        $suspended  = get_suspended_userids($context);
+
+        foreach ($users as $userdata) {
+            if (isset($userdata['enrolled']) && $userdata['enrolled']) {
+                $this->assertTrue(isset($enrolled[$userdata['id']]));
+            } else {
+                $this->assertFalse(isset($enrolled[$userdata['id']]));
+            }
+
+            if (isset($userdata['active']) && $userdata['active']) {
+                $this->assertTrue(isset($active[$userdata['id']]));
+            } else {
+                $this->assertFalse(isset($active[$userdata['id']]));
+            }
+
+            if (isset($userdata['suspended']) && $userdata['suspended']) {
+                $this->assertTrue(isset($suspended[$userdata['id']]));
+            } else {
+                $this->assertFalse(isset($suspended[$userdata['id']]));
+            }
+        }
+
+        $this->assertCount($counts['enrolled'],     $enrolled);
+        $this->assertCount($counts['active'],       $active);
+        $this->assertCount($counts['suspended'],    $suspended);
+    }
+
     /**
      * A small functional test of permission evaluations.
      */
diff --git a/lib/tests/behat/timezone.feature b/lib/tests/behat/timezone.feature
new file mode 100644 (file)
index 0000000..6605dc2
--- /dev/null
@@ -0,0 +1,11 @@
+@core
+Feature: View timezone defaults
+  In order to run all other behat tests
+  As an admin
+  I need to verify the default timezone is Australia/Perth
+
+  Scenario: Admin sees default timezone Australia/Perth
+    When I log in as "admin"
+    And I navigate to "Location settings" node in "Site administration > Location"
+    Then I should see "Default: Australia/Perth"
+    And the field "Default timezone" matches value "Australia/Perth"
diff --git a/lib/tests/date_legacy_test.php b/lib/tests/date_legacy_test.php
new file mode 100644 (file)
index 0000000..d495bcd
--- /dev/null
@@ -0,0 +1,397 @@
+<?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/>.
+
+/**
+ * Tests legacy Moodle date/time functions.
+ *
+ * @package   core
+ * @copyright 2015 Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author    Petr Skoda <petr.skoda@totaralms.com>
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Tests legacy Moodle date/time functions.
+ *
+ * @package   core
+ * @copyright 2015 Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author    Petr Skoda <petr.skoda@totaralms.com>
+ */
+class core_date_legacy_testcase extends advanced_testcase {
+    public function test_settings() {
+        global $CFG;
+        $this->resetAfterTest();
+
+        $this->assertNotEmpty($CFG->timezone);
+
+        $this->assertSame('99', $CFG->forcetimezone);
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->assertSame('99', $user->timezone);
+    }
+
+    public function test_get_list_of_timezones() {
+        // Use timezones that are not problematic, this way we may test before
+        // and after the big tz rewrite.
+        $list = get_list_of_timezones();
+        $this->assertDebuggingCalled();
+        $this->assertArrayHasKey('Europe/London', $list);
+        $this->assertArrayHasKey('Pacific/Auckland', $list);
+        $this->assertArrayHasKey('America/New_York', $list);
+        $this->assertArrayHasKey('Europe/Berlin', $list);
+        $this->assertArrayHasKey('Europe/Prague', $list);
+        $this->assertArrayHasKey('Australia/Perth', $list);
+        $this->assertArrayHasKey('Australia/Lord_Howe', $list);
+    }
+
+    public function test_get_user_timezone() {
+        global $CFG, $USER;
+
+        $this->resetAfterTest();
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+
+        // All set to something.
+
+        $this->setTimezone('Pacific/Auckland', 'Pacific/Auckland');
+        $USER->timezone = 'Europe/Prague';
+
+        $tz = get_user_timezone();
+        $this->assertSame('Europe/Prague', $tz);
+
+        $tz = get_user_timezone(99);
+        $this->assertSame('Europe/Prague', $tz);
+        $tz = get_user_timezone('99');
+        $this->assertSame('Europe/Prague', $tz);
+
+        $tz = get_user_timezone('Europe/Berlin');
+        $this->assertSame('Europe/Berlin', $tz);
+
+        // User timezone not set.
+
+        $this->setTimezone('Pacific/Auckland', 'Pacific/Auckland');
+        $USER->timezone = '99';
+
+        $tz = get_user_timezone();
+        $this->assertSame('Pacific/Auckland', $tz);
+
+        $tz = get_user_timezone(99);
+        $this->assertSame('Pacific/Auckland', $tz);
+        $tz = get_user_timezone('99');
+        $this->assertSame('Pacific/Auckland', $tz);
+
+        $tz = get_user_timezone('Europe/Berlin');
+        $this->assertSame('Europe/Berlin', $tz);
+
+        // Server timezone not set.
+
+        $this->setTimezone('99', 'Pacific/Auckland');
+        $USER->timezone = 'Europe/Prague';
+
+        $tz = get_user_timezone();
+        $this->assertSame('Europe/Prague', $tz);
+
+        $tz = get_user_timezone(99);
+        $this->assertSame('Europe/Prague', $tz);
+        $tz = get_user_timezone('99');
+        $this->assertSame('Europe/Prague', $tz);
+
+        $tz = get_user_timezone('Europe/Berlin');
+        $this->assertSame('Europe/Berlin', $tz);
+
+        // Server and user timezone not set.
+
+        $this->setTimezone('99', 'Pacific/Auckland');
+        $USER->timezone = '99';
+
+        $tz = get_user_timezone();
+        $this->assertSame(99.0, $tz);
+
+        $tz = get_user_timezone(99);
+        $this->assertSame(99.0, $tz);
+        $tz = get_user_timezone('99');
+        $this->assertSame(99.0, $tz);
+
+        $tz = get_user_timezone('Europe/Berlin');
+        $this->assertSame('Europe/Berlin', $tz);
+    }
+
+    public function test_get_timezone_offset() {
+        // This is a useless function, the timezone offset may be changing!
+        $this->assertSame(60 * 60 * -5, get_timezone_offset('America/New_York'));
+        $this->assertDebuggingCalled();
+        $this->assertSame(60 * 60 * -1, get_timezone_offset(-1));
+        $this->assertSame(60 * 60 * 1, get_timezone_offset(1));
+        $this->assertSame(60 * 60 * 1, get_timezone_offset('Europe/Prague'));
+        $this->assertSame(60 * 60 * 8, get_timezone_offset('Australia/Perth'));
+        $this->assertSame(60 * 60 * 12, get_timezone_offset('Pacific/Auckland'));
+
+        // Known half an hour offsets.
+        $this->assertEquals(60 * 60 * -4.5, get_timezone_offset('-4.5'));
+        $this->assertEquals(60 * 60 * -4.5, get_timezone_offset('America/Caracas'));
+        $this->assertEquals(60 * 60 * 4.5, get_timezone_offset('4.5'));
+        $this->assertEquals(60 * 60 * 4.5, get_timezone_offset('Asia/Kabul'));
+        $this->assertEquals(60 * 60 * 5.5, get_timezone_offset('5.5'));
+        $this->assertEquals(60 * 60 * 5.5, get_timezone_offset('Asia/Kolkata'));
+        $this->assertEquals(60 * 60 * 6.5, get_timezone_offset('6.5'));
+        $this->assertEquals(60 * 60 * 6.5, get_timezone_offset('Asia/Rangoon'));
+        $this->assertEquals(60 * 60 * 9.5, get_timezone_offset('9.5'));
+        $this->assertEquals(60 * 60 * 9.5, get_timezone_offset('Australia/Darwin'));
+        $this->assertEquals(60 * 60 * 11.5, get_timezone_offset('11.5'));
+        $this->assertEquals(60 * 60 * 11.5, get_timezone_offset('Pacific/Norfolk'));
+
+        $this->resetDebugging();
+    }
+
+    public function test_get_user_timezone_offset() {
+        // This is a useless function, the timezone offset may be changing!
+        $this->assertSame(-5.0, get_user_timezone_offset('America/New_York'));
+        $this->assertDebuggingCalled();
+        $this->assertSame(-1.0, get_user_timezone_offset(-1));
+        $this->assertSame(1.0, get_user_timezone_offset(1));
+        $this->assertSame(1.0, get_user_timezone_offset('Europe/Prague'));
+        $this->assertSame(8.0, get_user_timezone_offset('Australia/Perth'));
+        $this->assertSame(12.0, get_user_timezone_offset('Pacific/Auckland'));
+
+        $this->resetDebugging();
+    }
+
+    public function test_dst_offset_on() {
+        $time = gmmktime(1, 1, 1, 3, 1, 2015);
+        $this->assertSame(3600, dst_offset_on($time, 'Pacific/Auckland'));
+        $this->assertSame(0, dst_offset_on($time, 'Australia/Perth'));
+        $this->assertSame(1800, dst_offset_on($time, 'Australia/Lord_Howe'));
+        $this->assertSame(0, dst_offset_on($time, 'Europe/Prague'));
+        $this->assertSame(0, dst_offset_on($time, 'America/New_York'));
+
+        $time = gmmktime(1, 1, 1, 5, 1, 2015);
+        $this->assertSame(0, dst_offset_on($time, 'Pacific/Auckland'));
+        $this->assertSame(0, dst_offset_on($time, 'Australia/Perth'));
+        $this->assertSame(0, dst_offset_on($time, 'Australia/Lord_Howe'));
+        $this->assertSame(3600, dst_offset_on($time, 'Europe/Prague'));
+        $this->assertSame(3600, dst_offset_on($time, 'America/New_York'));
+    }
+
+    public function test_make_timestamp() {
+        global $CFG;
+
+        $this->resetAfterTest();
+
+        // There are quite a lot of problems, let's pick some less problematic zones for now.
+        $timezones = array('Europe/Prague', 'Europe/London', 'Australia/Perth', 'Pacific/Auckland', 'America/New_York', '99');
+
+        $dates = array(
+            array(2, 1, 0, 40, 40),
+            array(4, 3, 0, 30, 22),
+            array(9, 5, 0, 20, 19),
+            array(11, 28, 0, 10, 45),
+        );
+        $years = array(1999, 2009, 2014, 2018);
+
+        $this->setTimezone('Pacific/Auckland', 'Pacific/Auckland');
+        foreach ($timezones as $tz) {
+            foreach ($years as $year) {
+                foreach ($dates as $date) {
+                    $result = make_timestamp($year, $date[0], $date[1], $date[2], $date[3], $date[4], $tz, true);
+                    $expected = new DateTime('now', new DateTimeZone(($tz == 99 ? 'Pacific/Auckland' : $tz)));
+                    $expected->setDate($year, $date[0], $date[1]);
+                    $expected->setTime($date[2], $date[3], $date[4]);
+                    $this->assertSame($expected->getTimestamp(), $result,
+                        'Incorrect result for data ' . $expected->format("D, d M Y H:i:s O") . ' ' . $tz);
+                }
+            }
+        }
+
+        $this->setTimezone('99', 'Pacific/Auckland');
+        foreach ($timezones as $tz) {
+            foreach ($years as $year) {
+                foreach ($dates as $date) {
+                    $result = make_timestamp($year, $date[0], $date[1], $date[2], $date[3], $date[4], $tz, true);
+                    $expected = new DateTime('now', new DateTimeZone(($tz == 99 ? 'Pacific/Auckland' : $tz)));
+                    $expected->setDate($year, $date[0], $date[1]);
+                    $expected->setTime($date[2], $date[3], $date[4]);
+                    $this->assertSame($expected->getTimestamp(), $result,
+                        'Incorrect result for data ' . $expected->format("D, d M Y H:i:s O") . ' ' . $tz);
+                }
+            }
+        }
+    }
+
+    public function test_usergetdate() {
+        global $CFG;
+
+        $this->resetAfterTest();
+
+        // There are quite a lot of problems, let's pick some less problematic zones for now.
+        $timezones = array('Europe/Prague', 'Europe/London', 'Australia/Perth', 'Pacific/Auckland', 'America/New_York', '99');
+
+        $dates = array(
+            array(2, 1, 0, 40, 40),
+            array(4, 3, 0, 30, 22),
+            array(9, 5, 0, 20, 19),
+            array(11, 28, 0, 10, 45),
+        );
+        $years = array(1999, 2009, 2014, 2018);
+
+        $this->setTimezone('Pacific/Auckland', 'Pacific/Auckland');
+        foreach ($timezones as $tz) {
+            foreach ($years as $year) {
+                foreach ($dates as $date) {
+                    $expected = new DateTime('now', new DateTimeZone(($tz == 99 ? 'Pacific/Auckland' : $tz)));
+                    $expected->setDate($year, $date[0], $date[1]);
+                    $expected->setTime($date[2], $date[3], $date[4]);
+                    $result = usergetdate($expected->getTimestamp(), $tz);
+                    unset($result[0]); // Extra introduced by getdate().
+                    $ex = array(
+                        'seconds' => $date[4],
+                        'minutes' => $date[3],
+                        'hours' => $date[2],
+                        'mday' => $date[1],
+                        'wday' => (int)$expected->format('w'),
+                        'mon' => $date[0],
+                        'year' => $year,
+                        'yday' => (int)$expected->format('z'),
+                        'weekday' => $expected->format('l'),
+                        'month' => $expected->format('F'),
+                    );
+                    $this->assertSame($ex, $result,
+                        'Incorrect result for data ' . $expected->format("D, d M Y H:i:s O") . ' ' . $tz);
+                }
+            }
+        }
+
+        $this->setTimezone('99', 'Pacific/Auckland');
+        foreach ($timezones as $tz) {
+            foreach ($years as $year) {
+                foreach ($dates as $date) {
+                    $expected = new DateTime('now', new DateTimeZone(($tz == 99 ? 'Pacific/Auckland' : $tz)));
+                    $expected->setDate($year, $date[0], $date[1]);
+                    $expected->setTime($date[2], $date[3], $date[4]);
+                    $result = usergetdate($expected->getTimestamp(), $tz);
+                    unset($result[0]); // Extra introduced by getdate().
+                    $ex = array(
+                        'seconds' => $date[4],
+                        'minutes' => $date[3],
+                        'hours' => $date[2],
+                        'mday' => $date[1],
+                        'wday' => (int)$expected->format('w'),
+                        'mon' => $date[0],
+                        'year' => $year,
+                        'yday' => (int)$expected->format('z'),
+                        'weekday' => $expected->format('l'),
+                        'month' => $expected->format('F'),
+                    );
+                    $this->assertSame($ex, $result,
+                        'Incorrect result for data ' . $expected->format("D, d M Y H:i:s O") . ' ' . $tz);
+                }
+            }
+        }
+    }
+
+    public function test_userdate() {
+        global $CFG;
+
+        $this->resetAfterTest();
+
+        $dates = array(
+            array(2, 1, 0, 40, 40),
+            array(4, 3, 0, 30, 22),
+            array(9, 5, 0, 20, 19),
+            array(11, 28, 0, 10, 45),
+        );
+        $years = array(1999, 2009, 2014, 2018);
+
+        $users = array();
+        $users[] = $this->getDataGenerator()->create_user(array('timezone' => 99));
+        $users[] = $this->getDataGenerator()->create_user(array('timezone' => 'Europe/Prague'));
+        $users[] = $this->getDataGenerator()->create_user(array('timezone' => 'Pacific/Auckland'));
+        $users[] = $this->getDataGenerator()->create_user(array('timezone' => 'Australia/Perth'));
+        $users[] = $this->getDataGenerator()->create_user(array('timezone' => 'America/New_York'));
+
+        $format = get_string('strftimedaydatetime', 'langconfig');
+
+        $this->setTimezone('Pacific/Auckland', 'Pacific/Auckland');
+        foreach ($years as $year) {
+            foreach ($dates as $date) {
+                $expected = new DateTime('now', new DateTimeZone('UTC'));
+                $expected->setDate($year, $date[0], $date[1]);
+                $expected->setTime($date[2], $date[3], $date[4]);
+
+                foreach ($users as $user) {
+                    $this->setUser($user);
+                    $expected->setTimezone(new DateTimeZone(($user->timezone == 99 ? 'Pacific/Auckland' : $user->timezone)));
+                    $result = userdate($expected->getTimestamp(), '', 99, false, false);
+                    date_default_timezone_set($expected->getTimezone()->getName());
+                    $ex = strftime($format, $expected->getTimestamp());
+                    date_default_timezone_set($CFG->timezone);
+                    $this->assertSame($ex, $result);
+                }
+            }
+        }
+    }
+
+    public function test_usertime() {
+        // This is a useless bad hack, it needs to be completely eliminated.
+
+        $time = gmmktime(1, 1, 1, 3, 1, 2015);
+        $this->assertSame($time - (60 * 60 * 1), usertime($time, '1'));
+        $this->assertSame($time - (60 * 60 * -1), usertime($time, '-1'));
+        $this->assertSame($time - (60 * 60 * 1), usertime($time, 'Europe/Prague'));
+        $this->assertSame($time - (60 * 60 * 8), usertime($time, 'Australia/Perth'));
+        $this->assertSame($time - (60 * 60 * 12), usertime($time, 'Pacific/Auckland'));
+        $this->assertSame($time - (60 * 60 * -5), usertime($time, 'America/New_York'));
+
+        $time = gmmktime(1, 1, 1, 5, 1, 2015);
+        $this->assertSame($time - (60 * 60 * 1), usertime($time, '1'));
+        $this->assertSame($time - (60 * 60 * -1), usertime($time, '-1'));
+        $this->assertSame($time - (60 * 60 * 1), usertime($time, 'Europe/Prague'));
+        $this->assertSame($time - (60 * 60 * 8), usertime($time, 'Australia/Perth'));
+        $this->assertSame($time - (60 * 60 * 12), usertime($time, 'Pacific/Auckland'));
+        $this->assertSame($time - (60 * 60 * -5), usertime($time, 'America/New_York'));
+    }
+
+    public function test_usertimezone() {
+        global $USER;
+        $this->resetAfterTest();
+
+        $this->setTimezone('Pacific/Auckland', 'Pacific/Auckland');
+
+        $USER->timezone = 'Europe/Prague';
+        $this->assertSame('Europe/Prague', usertimezone());
+
+        $USER->timezone = '1';
+        $this->assertSame('UTC+1', usertimezone());
+
+        $USER->timezone = '0';
+        $this->assertSame('UTC', usertimezone());
+
+        $USER->timezone = '99';
+        $this->assertSame('Pacific/Auckland', usertimezone());
+
+        $USER->timezone = '99';
+        $this->assertSame('Europe/Berlin', usertimezone('Europe/Berlin'));
+
+        $USER->timezone = '99';
+        $this->assertSame('Pacific/Auckland', usertimezone('99'));
+
+        $USER->timezone = 'Europe/Prague';
+        $this->assertSame('Europe/Prague', usertimezone('99'));
+    }
+}
diff --git a/lib/tests/date_test.php b/lib/tests/date_test.php
new file mode 100644 (file)
index 0000000..c84fcf0
--- /dev/null
@@ -0,0 +1,489 @@
+<?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/>.
+
+/**
+ * Tests core_date class.
+ *
+ * @package   core
+ * @copyright 2015 Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author    Petr Skoda <petr.skoda@totaralms.com>
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Tests core_date class.
+ *
+ * @package   core
+ * @copyright 2015 Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author    Petr Skoda <petr.skoda@totaralms.com>
+ */
+class core_date_testcase extends advanced_testcase {
+    public function test_get_default_php_timezone() {
+        $this->resetAfterTest();
+
+        $origtz = core_date::get_default_php_timezone();
+        $this->assertNotEmpty($origtz);
+
+        $this->setTimezone('Pacific/Auckland', 'Europe/Prague');
+        $this->assertSame('Europe/Prague', core_date::get_default_php_timezone());
+
+        $this->setTimezone('Pacific/Auckland', 'UTC');
+        $this->assertSame('UTC', core_date::get_default_php_timezone());
+
+        $this->setTimezone('Pacific/Auckland', 'GMT');
+        $this->assertSame('GMT', core_date::get_default_php_timezone());
+    }
+
+    public function test_normalise_timezone() {
+        $this->resetAfterTest();
+
+        $this->setTimezone('Pacific/Auckland');
+        $this->assertSame('Pacific/Auckland', core_date::normalise_timezone('Pacific/Auckland'));
+        $this->assertSame('Pacific/Auckland', core_date::normalise_timezone('99'));
+        $this->assertSame('Pacific/Auckland', core_date::normalise_timezone(99));
+        $this->assertSame('GMT', core_date::normalise_timezone('GMT'));
+        $this->assertSame('UTC', core_date::normalise_timezone('UTC'));
+        $this->assertSame('Pacific/Auckland', core_date::normalise_timezone('xxxxxxxx'));
+        $this->assertSame('Europe/Berlin', core_date::normalise_timezone('Central European Time'));
+        $this->assertSame('Etc/GMT', core_date::normalise_timezone('0'));
+        $this->assertSame('Etc/GMT', core_date::normalise_timezone('0.0'));
+        $this->assertSame('Etc/GMT-2', core_date::normalise_timezone(2));
+        $this->assertSame('Etc/GMT-2', core_date::normalise_timezone('2.0'));
+        $this->assertSame('Etc/GMT+2', core_date::normalise_timezone(-2));
+        $this->assertSame('Etc/GMT+2', core_date::normalise_timezone('-2.0'));
+        $this->assertSame('Etc/GMT-2', core_date::normalise_timezone('UTC+2'));
+        $this->assertSame('Etc/GMT+2', core_date::normalise_timezone('UTC-2'));
+        $this->assertSame('Etc/GMT-2', core_date::normalise_timezone('GMT+2'));
+        $this->assertSame('Etc/GMT+2', core_date::normalise_timezone('GMT-2'));
+        $this->assertSame('Etc/GMT+12', core_date::normalise_timezone(-12));
+        $this->assertSame('Pacific/Auckland', core_date::normalise_timezone(-13));
+        $this->assertSame('Pacific/Auckland', core_date::normalise_timezone(-14));
+        $this->assertSame('Etc/GMT-12', core_date::normalise_timezone(12));
+        $this->assertSame('Etc/GMT-13', core_date::normalise_timezone(13));
+        $this->assertSame('Pacific/Auckland', core_date::normalise_timezone(14));
+
+        $this->assertSame('America/Caracas', core_date::normalise_timezone(-4.5));
+        $this->assertSame('Asia/Kabul', core_date::normalise_timezone(4.5));
+        $this->assertSame('Asia/Kolkata', core_date::normalise_timezone(5.5));
+        $this->assertSame('Asia/Rangoon', core_date::normalise_timezone(6.5));
+        $this->assertSame('Australia/Darwin', core_date::normalise_timezone('9.5'));
+        $this->assertSame('Pacific/Norfolk', core_date::normalise_timezone('11.5'));
+
+        $this->setTimezone('99', 'Pacific/Auckland');
+        $this->assertSame('Pacific/Auckland', core_date::normalise_timezone('Pacific/Auckland'));
+        $this->assertSame('Pacific/Auckland', core_date::normalise_timezone('99'));
+        $this->assertSame('Pacific/Auckland', core_date::normalise_timezone(99));
+        $this->assertSame('GMT', core_date::normalise_timezone('GMT'));
+        $this->assertSame('UTC', core_date::normalise_timezone('UTC'));
+        $this->assertSame('Pacific/Auckland', core_date::normalise_timezone('xxxxxxxx'));
+        $this->assertSame('Europe/Berlin', core_date::normalise_timezone('Central European Time'));
+        $this->assertSame('Etc/GMT', core_date::normalise_timezone('0'));
+        $this->assertSame('Etc/GMT', core_date::normalise_timezone('0.0'));
+        $this->assertSame('Etc/GMT-2', core_date::normalise_timezone(2));
+        $this->assertSame('Etc/GMT-2', core_date::normalise_timezone('2.0'));
+        $this->assertSame('Etc/GMT+2', core_date::normalise_timezone(-2));
+        $this->assertSame('Etc/GMT+2', core_date::normalise_timezone('-2.0'));
+        $this->assertSame('Etc/GMT-2', core_date::normalise_timezone('UTC+2'));
+        $this->assertSame('Etc/GMT+2', core_date::normalise_timezone('UTC-2'));
+        $this->assertSame('Etc/GMT-2', core_date::normalise_timezone('GMT+2'));
+        $this->assertSame('Etc/GMT+2', core_date::normalise_timezone('GMT-2'));
+        $this->assertSame('Etc/GMT+12', core_date::normalise_timezone(-12));
+        $this->assertSame('Pacific/Auckland', core_date::normalise_timezone(-13));
+        $this->assertSame('Pacific/Auckland', core_date::normalise_timezone(-14));
+        $this->assertSame('Etc/GMT-12', core_date::normalise_timezone(12));
+        $this->assertSame('Etc/GMT-13', core_date::normalise_timezone(13));
+        $this->assertSame('Pacific/Auckland', core_date::normalise_timezone(14));
+
+        $this->setTimezone('Pacific/Auckland', 'Pacific/Auckland');
+        $tz = new DateTimeZone('Pacific/Auckland');
+        $this->assertSame('Pacific/Auckland', core_date::normalise_timezone($tz));
+    }
+
+    public function test_windows_conversion() {
+        $file = __DIR__ . '/fixtures/timezonewindows.xml';
+
+        $contents = file_get_contents($file);
+        preg_match_all('/<mapZone other="([^"]+)" territory="001" type="([^"]+)"\/>/', $contents, $matches, PREG_SET_ORDER);
+
+        $this->assertCount(104, $matches); // NOTE: If the file contents change edit the core_date class and update this.
+
+        foreach ($matches as $match) {
+            $result = core_date::normalise_timezone($match[1]);
+            if ($result == $match[2]) {
+                $this->assertSame($match[2], $result);
+            } else {
+                $data = new DateTime('now', new DateTimeZone($match[2]));
+                $expectedoffset = $data->getOffset();
+                $data = new DateTime('now', new DateTimeZone($result));
+                $resultoffset = $data->getOffset();
+                $this->assertSame($expectedoffset, $resultoffset, "$match[1] is expected to be converted to $match[2] not $result");
+            }
+        }
+    }
+
+    /**
+     * Sanity test for PHP stuff.
+     */
+    public function test_php_gmt_offsets() {
+        $this->resetAfterTest();
+
+        $this->setTimezone('Pacific/Auckland', 'Pacific/Auckland');
+
+        for ($i = -12; $i < 0; $i++) {
+            $date = new DateTime('now', new DateTimeZone("Etc/GMT{$i}"));
+            $this->assertSame(- $i * 60 * 60, $date->getOffset());
+            $date = new DateTime('now', new DateTimeZone(core_date::normalise_timezone("GMT{$i}")));
+            $this->assertSame($i * 60 * 60, $date->getOffset());
+            $date = new DateTime('now', new DateTimeZone(core_date::normalise_timezone("UTC{$i}")));
+            $this->assertSame($i * 60 * 60, $date->getOffset());
+        }
+
+        $date = new DateTime('now', new DateTimeZone('Etc/GMT'));
+        $this->assertSame(0, $date->getOffset());
+
+        for ($i = 1; $i <= 12; $i++) {
+            $date = new DateTime('now', new DateTimeZone("Etc/GMT+{$i}"));
+            $this->assertSame(- $i * 60 * 60, $date->getOffset());
+            $date = new DateTime('now', new DateTimeZone(core_date::normalise_timezone("GMT+{$i}")));
+            $this->assertSame($i * 60 * 60, $date->getOffset());
+            $date = new DateTime('now', new DateTimeZone(core_date::normalise_timezone("UTC+{$i}")));
+            $this->assertSame($i * 60 * 60, $date->getOffset());
+        }
+    }
+
+    public function test_get_localised_timezone() {
+        $this->resetAfterTest();
+
+        $this->setTimezone('Pacific/Auckland', 'Pacific/Auckland');
+
+        $result = core_date::get_localised_timezone('Pacific/Auckland');
+        $this->assertSame('Pacific/Auckland', $result);
+
+        $result = core_date::get_localised_timezone('99');
+        $this->assertSame('Server timezone (Pacific/Auckland)', $result);
+
+        $result = core_date::get_localised_timezone(99);
+        $this->assertSame('Server timezone (Pacific/Auckland)', $result);
+
+        $result = core_date::get_localised_timezone('Etc/GMT-1');
+        $this->assertSame('UTC+1', $result);
+
+        $result = core_date::get_localised_timezone('Etc/GMT+2');
+        $this->assertSame('UTC-2', $result);
+
+        $result = core_date::get_localised_timezone('GMT');
+        $this->assertSame('UTC', $result);
+
+        $result = core_date::get_localised_timezone('Etc/GMT');
+        $this->assertSame('UTC', $result);
+    }
+
+    public function test_get_list_of_timezones() {
+        $this->resetAfterTest();
+
+        $this->setTimezone('Pacific/Auckland', 'Pacific/Auckland');
+
+        $phpzones = DateTimeZone::listIdentifiers();
+
+        $zones = core_date::get_list_of_timezones();
+        $this->assertSame(count($phpzones), count($zones));
+        foreach ($zones as $zone => $zonename) {
+            $this->assertSame($zone, $zonename); // The same in English!
+            $this->assertContains($zone, $phpzones); // No extras expected.
+        }
+
+        $this->assertSame($zones, core_date::get_list_of_timezones(null, false));
+
+        $nnzones = core_date::get_list_of_timezones(null, true);
+        $last = $nnzones['99'];
+        $this->assertSame('Server timezone (Pacific/Auckland)', $last);
+        unset($nnzones['99']);
+        $this->assertSame($zones, $nnzones);
+
+        $nnzones = core_date::get_list_of_timezones('99', false);
+        $last = $nnzones['99'];
+        $this->assertSame('Server timezone (Pacific/Auckland)', $last);
+        unset($nnzones['99']);
+        $this->assertSame($zones, $nnzones);
+
+        $xxzones = core_date::get_list_of_timezones('xx', false);
+        $xx = $xxzones['xx'];
+        $this->assertSame('Invalid timezone "xx"', $xx);
+        unset($xxzones['xx']);
+        $this->assertSame($zones, $xxzones);
+
+        $xxzones = core_date::get_list_of_timezones('1', false);
+        $xx = $xxzones['1'];
+        $this->assertSame('Invalid timezone "UTC+1.0"', $xx);
+        unset($xxzones['1']);
+        $this->assertSame($zones, $xxzones);
+
+        $xxzones = core_date::get_list_of_timezones('-1.5', false);
+        $xx = $xxzones['-1.5'];
+        $this->assertSame('Invalid timezone "UTC-1.5"', $xx);
+        unset($xxzones['-1.5']);
+        $this->assertSame($zones, $xxzones);
+
+    }
+
+    public function test_get_server_timezone() {
+        global $CFG;
+        $this->resetAfterTest();
+
+        $this->setTimezone('Pacific/Auckland', 'Pacific/Auckland');
+        $this->assertSame('Pacific/Auckland', core_date::get_server_timezone());
+
+        $this->setTimezone('Pacific/Auckland', 'Europe/Prague');
+        $this->assertSame('Pacific/Auckland', core_date::get_server_timezone());
+
+        $this->setTimezone('99', 'Pacific/Auckland');
+        $this->assertSame('Pacific/Auckland', core_date::get_server_timezone());
+
+        $this->setTimezone(99, 'Pacific/Auckland');
+        $this->assertSame('Pacific/Auckland', core_date::get_server_timezone());
+
+        $this->setTimezone('Pacific/Auckland', 'Pacific/Auckland');
+        unset($CFG->timezone);
+        $this->assertSame('Pacific/Auckland', core_date::get_server_timezone());
+
+        // Admin should fix the settings.
+        $this->setTimezone('xxx/zzzz', 'Europe/Prague');
+        $this->assertSame('Europe/Prague', core_date::get_server_timezone());
+    }
+
+    public function test_get_server_timezone_object() {
+        $this->resetAfterTest();
+
+        $zones = core_date::get_list_of_timezones();
+        foreach ($zones as $zone) {
+            $this->setTimezone($zone, 'Pacific/Auckland');
+            $tz = core_date::get_server_timezone_object();
+            $this->assertInstanceOf('DateTimeZone', $tz);
+            $this->assertSame($zone, $tz->getName());
+        }
+    }
+
+    public function test_set_default_server_timezone() {
+        global $CFG;
+        $this->resetAfterTest();
+
+        $this->setTimezone('Europe/Prague', 'Pacific/Auckland');
+        unset($CFG->timezone);
+        date_default_timezone_set('UTC');
+        core_date::set_default_server_timezone();
+        $this->assertSame('Pacific/Auckland', date_default_timezone_get());
+
+        $this->setTimezone('', 'Pacific/Auckland');
+        date_default_timezone_set('UTC');
+        core_date::set_default_server_timezone();
+        $this->assertSame('Pacific/Auckland', date_default_timezone_get());
+
+        $this->setTimezone('99', 'Pacific/Auckland');
+        date_default_timezone_set('UTC');
+        core_date::set_default_server_timezone();
+        $this->assertSame('Pacific/Auckland', date_default_timezone_get());
+
+        $this->setTimezone(99, 'Pacific/Auckland');
+        date_default_timezone_set('UTC');
+        core_date::set_default_server_timezone();
+        $this->assertSame('Pacific/Auckland', date_default_timezone_get());
+
+        $this->setTimezone('Europe/Prague', 'Pacific/Auckland');
+        $CFG->timezone = 'UTC';
+        core_date::set_default_server_timezone();
+        $this->assertSame('UTC', date_default_timezone_get());
+
+        $this->setTimezone('Europe/Prague', 'Pacific/Auckland');
+        $CFG->timezone = 'Australia/Perth';
+        core_date::set_default_server_timezone();
+        $this->assertSame('Australia/Perth', date_default_timezone_get());
+
+        $this->setTimezone('0', 'Pacific/Auckland');
+        date_default_timezone_set('UTC');
+        core_date::set_default_server_timezone();
+        $this->assertSame('Etc/GMT', date_default_timezone_get());
+
+        $this->setTimezone('1', 'Pacific/Auckland');
+        date_default_timezone_set('UTC');
+        core_date::set_default_server_timezone();
+        $this->assertSame('Etc/GMT-1', date_default_timezone_get());
+
+        $this->setTimezone(1, 'Pacific/Auckland');
+        date_default_timezone_set('UTC');
+        core_date::set_default_server_timezone();
+        $this->assertSame('Etc/GMT-1', date_default_timezone_get());
+
+        $this->setTimezone('1.0', 'Pacific/Auckland');
+        date_default_timezone_set('UTC');
+        core_date::set_default_server_timezone();
+        $this->assertSame('Etc/GMT-1', date_default_timezone_get());
+    }
+
+    public function test_get_user_timezone() {
+        global $CFG, $USER;
+        $this->resetAfterTest();
+
+        // Null parameter.
+
+        $this->setTimezone('Europe/Prague', 'Pacific/Auckland');
+        $USER->timezone = 'Pacific/Auckland';
+        $CFG->forcetimezone = '99';
+        $this->assertSame('Pacific/Auckland', core_date::get_user_timezone(null));
+        $this->assertSame('Pacific/Auckland', core_date::get_user_timezone());
+
+        $this->setTimezone('Europe/Prague', 'Pacific/Auckland');
+        $USER->timezone = 'Pacific/Auckland';
+        $CFG->forcetimezone = 'Europe/Berlin';
+        $this->assertSame('Europe/Berlin', core_date::get_user_timezone(null));
+        $this->assertSame('Europe/Berlin', core_date::get_user_timezone());
+
+        $this->setTimezone('Europe/Prague', 'Pacific/Auckland');
+        $USER->timezone = 'Pacific/Auckland';
+        $CFG->forcetimezone = 'xxx/yyy';
+        $this->assertSame('Europe/Prague', core_date::get_user_timezone(null));
+        $this->assertSame('Europe/Prague', core_date::get_user_timezone());
+
+        $this->setTimezone('Europe/Prague', 'Pacific/Auckland');
+        $USER->timezone = 'abc/def';
+        $CFG->forcetimezone = '99';
+        $this->assertSame('Europe/Prague', core_date::get_user_timezone(null));
+        $this->assertSame('Europe/Prague', core_date::get_user_timezone());
+
+        $this->setTimezone('xxx/yyy', 'Europe/London');
+        $USER->timezone = 'abc/def';
+        $CFG->forcetimezone = 'Europe/Berlin';
+        $this->assertSame('Europe/Berlin', core_date::get_user_timezone(null));
+        $this->assertSame('Europe/Berlin', core_date::get_user_timezone());
+
+        $this->setTimezone('xxx/yyy', 'Europe/London');
+        $USER->timezone = 'abc/def';
+        $CFG->forcetimezone = 99;
+        $this->assertSame('Europe/London', core_date::get_user_timezone(null));
+        $this->assertSame('Europe/London', core_date::get_user_timezone());
+
+        // User object parameter.
+        $admin = get_admin();
+
+        $this->setTimezone('Europe/London');
+        $USER->timezone = 'Pacific/Auckland';
+        $CFG->forcetimezone = '99';
+        $admin->timezone = 'Australia/Perth';
+        $this->assertSame('Australia/Perth', core_date::get_user_timezone($admin));
+
+        $this->setTimezone('Europe/Prague');
+        $USER->timezone = 'Pacific/Auckland';
+        $CFG->forcetimezone = '99';
+        $admin->timezone = '99';
+        $this->assertSame('Europe/Prague', core_date::get_user_timezone($admin));
+
+        $this->setTimezone('99', 'Europe/London');
+        $USER->timezone = 'Pacific/Auckland';
+        $CFG->forcetimezone = '99';
+        $admin->timezone = '99';
+        $this->assertSame('Europe/London', core_date::get_user_timezone($admin));
+
+        $this->setTimezone('Europe/Prague');
+        $USER->timezone = 'Pacific/Auckland';
+        $CFG->forcetimezone = '99';
+        $admin->timezone = 'xx/zz';
+        $this->assertSame('Europe/Prague', core_date::get_user_timezone($admin));
+
+        $this->setTimezone('Europe/Prague');
+        $USER->timezone = 'Pacific/Auckland';
+        $CFG->forcetimezone = '99';
+        unset($admin->timezone);
+        $this->assertSame('Europe/Prague', core_date::get_user_timezone($admin));
+
+        $this->setTimezone('Europe/Prague');
+        $USER->timezone = 'Pacific/Auckland';
+        $CFG->forcetimezone = 'Europe/Berlin';
+        $admin->timezone = 'Australia/Perth';
+        $this->assertSame('Europe/Berlin', core_date::get_user_timezone($admin));
+
+        // Other scalar parameter.
+
+        $this->setTimezone('Europe/Prague');
+        $CFG->forcetimezone = '99';
+
+        $USER->timezone = 'Pacific/Auckland';
+        $this->assertSame('Pacific/Auckland', core_date::get_user_timezone('99'));
+        $this->assertSame('Etc/GMT-1', core_date::get_user_timezone('1'));
+        $this->assertSame('Etc/GMT+1', core_date::get_user_timezone(-1));
+        $this->assertSame('Europe/London', core_date::get_user_timezone('Europe/London'));
+        $USER->timezone = '99';
+        $this->assertSame('Europe/Prague', core_date::get_user_timezone('99'));
+        $this->assertSame('Europe/London', core_date::get_user_timezone('Europe/London'));
+        $this->assertSame('Europe/Prague', core_date::get_user_timezone('xxx/zzz'));
+        $USER->timezone = 'xxz/zzz';
+        $this->assertSame('Europe/Prague', core_date::get_user_timezone('99'));
+
+        $this->setTimezone('99', 'Europe/Prague');
+        $CFG->forcetimezone = '99';
+
+        $USER->timezone = 'Pacific/Auckland';
+        $this->assertSame('Pacific/Auckland', core_date::get_user_timezone('99'));
+        $this->assertSame('Europe/London', core_date::get_user_timezone('Europe/London'));
+        $USER->timezone = '99';
+        $this->assertSame('Europe/Prague', core_date::get_user_timezone('99'));
+        $this->assertSame('Europe/London', core_date::get_user_timezone('Europe/London'));
+        $this->assertSame('Europe/Prague', core_date::get_user_timezone('xxx/zzz'));
+        $USER->timezone = 99;
+        $this->assertSame('Europe/London', core_date::get_user_timezone('Europe/London'));
+        $this->assertSame('Europe/Prague', core_date::get_user_timezone('xxx/z