Merge branch 'MDL-43369-master' of git://github.com/jamiepratt/moodle
authorSam Hemelryk <sam@moodle.com>
Tue, 4 Feb 2014 02:50:48 +0000 (15:50 +1300)
committerSam Hemelryk <sam@moodle.com>
Tue, 4 Feb 2014 02:50:48 +0000 (15:50 +1300)
293 files changed:
admin/cli/backup.php [new file with mode: 0644]
admin/tool/behat/lang/en/tool_behat.php
admin/tool/behat/renderer.php
admin/tool/behat/version.php
admin/webservice/service_user_settings.php
auth/db/tests/db_test.php
auth/ldap/tests/plugin_test.php
badges/tests/badgeslib_test.php
blocks/comments/tests/events_test.php
blog/tests/bloglib_test.php
cache/classes/loaders.php
cohort/tests/cohortlib_test.php
config-dist.php
course/dndupload.js
course/edit_form.php
course/editsection_form.php
course/format/renderer.php
course/moodleform_mod.php
course/tests/courselib_test.php
enrol/database/tests/sync_test.php
enrol/imsenterprise/lib.php
enrol/imsenterprise/tests/imsenterprise_test.php
enrol/manual/manage.php
enrol/manual/unenrolself.php
enrol/meta/tests/plugin_test.php
enrol/paypal/unenrolself.php
enrol/self/lib.php
enrol/self/unenrolself.php
enrol/tests/enrollib_test.php
files/tests/externallib_test.php
grade/grading/form/guide/backup/moodle2/backup_gradingform_guide_plugin.class.php
grade/grading/form/guide/backup/moodle2/restore_gradingform_guide_plugin.class.php
grade/tests/edittreelib_test.php
grade/tests/externallib_test.php
group/clientlib.js
group/groupings.php
group/overview.php
install/lang/fr/error.php
lang/en/auth.php
lang/en/dbtransfer.php
lib/behat/classes/behat_command.php
lib/behat/form_field/behat_form_editor.php
lib/behat/form_field/behat_form_field.php
lib/behat/form_field/behat_form_select.php
lib/classes/event/assessable_submitted.php
lib/classes/event/assessable_uploaded.php
lib/classes/event/base.php
lib/classes/event/blog_association_created.php
lib/classes/event/comment_created.php
lib/classes/event/comment_deleted.php
lib/classes/event/comments_viewed.php
lib/classes/event/course_module_instance_list_viewed.php
lib/classes/event/course_module_viewed.php
lib/classes/event/user_enrolment_created.php
lib/classes/event/user_enrolment_deleted.php
lib/classes/event/user_login_failed.php [new file with mode: 0644]
lib/classes/lock/db_record_lock_factory.php [new file with mode: 0644]
lib/classes/lock/file_lock_factory.php [new file with mode: 0644]
lib/classes/lock/lock.php [new file with mode: 0644]
lib/classes/lock/lock_config.php [new file with mode: 0644]
lib/classes/lock/lock_factory.php [new file with mode: 0644]
lib/classes/lock/postgres_lock_factory.php [new file with mode: 0644]
lib/classes/plugin_manager.php
lib/classes/update/deployer.php
lib/db/install.xml
lib/db/upgrade.php
lib/ddl/database_manager.php
lib/ddl/mysql_sql_generator.php
lib/dml/moodle_database.php
lib/dml/mssql_native_moodle_database.php
lib/dml/mysqli_native_moodle_database.php
lib/dml/oci_native_moodle_database.php
lib/dml/pdo_moodle_database.php
lib/dml/pgsql_native_moodle_database.php
lib/dml/sqlite3_pdo_moodle_database.php
lib/dml/sqlsrv_native_moodle_database.php
lib/dml/tests/dml_test.php
lib/dtl/database_exporter.php
lib/filestorage/tgz_extractor.php
lib/javascript-static.js
lib/moodlelib.php
lib/outputactions.php
lib/outputrenderers.php
lib/pdflib.php
lib/phpunit/classes/advanced_testcase.php
lib/phpunit/classes/event_mock.php
lib/setuplib.php
lib/tablelib.php
lib/tests/accesslib_test.php
lib/tests/authlib_test.php
lib/tests/behat/behat_hooks.php
lib/tests/code_test.php [deleted file]
lib/tests/completionlib_test.php
lib/tests/coursecatlib_test.php
lib/tests/event_content_viewed_test.php
lib/tests/event_course_module_instance_list_viewed.php
lib/tests/event_course_module_viewed.php
lib/tests/event_test.php
lib/tests/events_test.php
lib/tests/fixtures/event_fixtures.php
lib/tests/lock_config_test.php [new file with mode: 0644]
lib/tests/lock_test.php [new file with mode: 0644]
lib/tests/messagelib_test.php
lib/tests/moodlelib_test.php
lib/upgrade.txt
lib/upgradelib.php
lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-debug.js
lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-min.js
lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue.js
lib/yui/build/moodle-core-lockscroll/moodle-core-lockscroll-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-lockscroll/moodle-core-lockscroll-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-lockscroll/moodle-core-lockscroll.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert-debug.js
lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert-min.js
lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert.js
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-debug.js
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-min.js
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-debug.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-min.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception.js
lib/yui/src/chooserdialogue/js/chooserdialogue.js
lib/yui/src/lockscroll/build.json [new file with mode: 0644]
lib/yui/src/lockscroll/js/lockscroll.js [new file with mode: 0644]
lib/yui/src/lockscroll/meta/lockscroll.json [new file with mode: 0644]
lib/yui/src/notification/js/alert.js
lib/yui/src/notification/js/confirm.js
lib/yui/src/notification/js/dialogue.js
lib/yui/src/notification/js/exception.js
lib/yui/src/notification/meta/notification.json
message/tests/behat/message_participants.feature [new file with mode: 0644]
mod/assign/classes/event/all_submissions_downloaded.php
mod/assign/classes/event/assessable_submitted.php
mod/assign/classes/event/extension_granted.php
mod/assign/classes/event/identities_revealed.php
mod/assign/classes/event/marker_updated.php
mod/assign/classes/event/statement_accepted.php
mod/assign/classes/event/submission_duplicated.php
mod/assign/classes/event/submission_graded.php
mod/assign/classes/event/submission_locked.php
mod/assign/classes/event/submission_status_updated.php
mod/assign/classes/event/submission_unlocked.php
mod/assign/classes/event/submission_updated.php
mod/assign/classes/event/workflow_state_updated.php
mod/assign/feedback/comments/locallib.php
mod/assign/feedback/editpdf/classes/document_services.php
mod/assign/feedback/editpdf/classes/pdf.php
mod/assign/gradingtable.php
mod/assign/locallib.php
mod/assign/submission/comments/tests/events_test.php
mod/assign/submission/file/classes/event/assessable_uploaded.php
mod/assign/submission/file/tests/events_test.php
mod/assign/submission/onlinetext/classes/event/assessable_uploaded.php
mod/assign/submission/onlinetext/tests/events_test.php
mod/assign/tests/behat/quickgrading.feature [new file with mode: 0644]
mod/assign/tests/externallib_test.php
mod/assign/version.php
mod/assignment/version.php
mod/book/classes/event/chapter_created.php
mod/book/classes/event/chapter_deleted.php
mod/book/classes/event/chapter_updated.php
mod/book/classes/event/chapter_viewed.php
mod/book/tests/events_test.php
mod/book/tool/exportimscp/classes/event/book_exported.php
mod/book/tool/exportimscp/tests/events_test.php
mod/book/tool/importhtml/tests/locallib_test.php
mod/book/tool/print/classes/event/book_printed.php
mod/book/tool/print/classes/event/chapter_printed.php
mod/book/tool/print/tests/events_test.php
mod/book/version.php
mod/chat/classes/event/message_sent.php
mod/chat/classes/event/sessions_viewed.php
mod/chat/lib.php
mod/chat/tests/events_test.php
mod/chat/version.php
mod/choice/classes/event/answer_submitted.php
mod/choice/classes/event/answer_updated.php
mod/choice/classes/event/report_viewed.php
mod/choice/report.php
mod/choice/tests/events_test.php
mod/choice/version.php
mod/data/tests/lib_test.php
mod/data/version.php
mod/feedback/classes/event/response_deleted.php
mod/feedback/classes/event/response_submitted.php
mod/feedback/tests/events_test.php
mod/feedback/version.php
mod/folder/classes/event/folder_updated.php
mod/folder/tests/events_test.php
mod/folder/version.php
mod/forum/classes/event/assessable_uploaded.php
mod/forum/classes/post_form.php
mod/forum/lib.php
mod/forum/tests/externallib_test.php
mod/forum/tests/lib_test.php
mod/forum/version.php
mod/glossary/tests/events_test.php
mod/glossary/version.php
mod/imscp/version.php
mod/label/version.php
mod/lesson/classes/event/essay_assessed.php
mod/lesson/classes/event/essay_attempt_viewed.php
mod/lesson/classes/event/highscore_added.php
mod/lesson/classes/event/highscores_viewed.php
mod/lesson/classes/event/lesson_ended.php
mod/lesson/classes/event/lesson_started.php
mod/lesson/tests/events_test.php
mod/lesson/version.php
mod/lti/version.php
mod/page/version.php
mod/quiz/attemptlib.php
mod/quiz/classes/event/attempt_abandoned.php
mod/quiz/classes/event/attempt_becameoverdue.php
mod/quiz/classes/event/attempt_started.php
mod/quiz/classes/event/attempt_submitted.php
mod/quiz/locallib.php
mod/quiz/tests/events_test.php
mod/quiz/tests/quizdisplayoptions_test.php
mod/quiz/version.php
mod/resource/tests/events_test.php
mod/resource/version.php
mod/scorm/classes/event/attempt_deleted.php
mod/scorm/classes/event/course_module_viewed.php
mod/scorm/classes/event/interactions_viewed.php
mod/scorm/classes/event/report_viewed.php
mod/scorm/classes/event/sco_launched.php
mod/scorm/classes/event/tracks_viewed.php
mod/scorm/classes/event/user_report_viewed.php
mod/scorm/tests/event_test.php
mod/scorm/version.php
mod/survey/version.php
mod/url/version.php
mod/wiki/classes/event/comments_viewed.php
mod/wiki/classes/event/page_created.php
mod/wiki/classes/event/page_deleted.php
mod/wiki/classes/event/page_diff_viewed.php
mod/wiki/classes/event/page_history_viewed.php
mod/wiki/classes/event/page_locks_deleted.php
mod/wiki/classes/event/page_map_viewed.php
mod/wiki/classes/event/page_updated.php
mod/wiki/classes/event/page_version_deleted.php
mod/wiki/classes/event/page_version_restored.php
mod/wiki/classes/event/page_version_viewed.php
mod/wiki/classes/event/page_viewed.php
mod/wiki/tests/events_test.php
mod/wiki/version.php
mod/workshop/classes/event/assessable_uploaded.php
mod/workshop/classes/event/course_module_viewed.php
mod/workshop/version.php
notes/tests/events_test.php
question/behaviour/manualgraded/tests/walkthrough_test.php
question/editlib.php
question/engine/datalib.php
question/engine/tests/helpers.php
question/engine/tests/qubaid_condition_test.php [moved from question/engine/tests/datalib_test.php with 98% similarity]
question/engine/tests/question_engine_data_mapper_test.php [new file with mode: 0644]
question/format/xml/format.php
question/format/xml/tests/xmlformat_test.php
question/preview.php
question/question.php
question/type/edit_question_form.php
question/type/essay/tests/helper.php
question/type/multianswer/tests/helper.php
question/type/questiontypebase.php
question/upgrade.txt
question/yui/build/moodle-question-preview/moodle-question-preview-debug.js [new file with mode: 0644]
question/yui/build/moodle-question-preview/moodle-question-preview-min.js [new file with mode: 0644]
question/yui/build/moodle-question-preview/moodle-question-preview.js [new file with mode: 0644]
question/yui/src/preview/build.json [new file with mode: 0644]
question/yui/src/preview/js/preview.js [moved from question/preview.js with 70% similarity]
question/yui/src/preview/meta/preview.json [new file with mode: 0644]
report/loglive/index.php
report/participation/index.php
repository/filepicker.js
repository/tests/behat/cancel_add_file.feature
repository/tests/behat/create_folders.feature
repository/tests/behat/create_shortcut.feature
repository/tests/behat/delete_files.feature
repository/tests/behat/overwrite_file.feature
repository/tests/behat/zip_and_unzip.feature
repository/tests/repositorylib_test.php
theme/base/style/core.css
theme/base/style/course.css
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/less/moodle/responsive.less
theme/bootstrapbase/less/moodle/user.less
theme/bootstrapbase/style/moodle.css
version.php
webservice/tests/events_test.php

diff --git a/admin/cli/backup.php b/admin/cli/backup.php
new file mode 100644 (file)
index 0000000..2770c03
--- /dev/null
@@ -0,0 +1,119 @@
+<?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/>.
+
+/**
+ * This script allows to do backup.
+ *
+ * @package    core
+ * @subpackage cli
+ * @copyright  2013 Lancaster University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', 1);
+
+require(dirname(dirname(dirname(__FILE__))).'/config.php');
+require_once($CFG->libdir.'/clilib.php');
+require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
+
+// Now get cli options.
+list($options, $unrecognized) = cli_get_params(array(
+    'courseid' => false,
+    'courseshortname' => '',
+    'destination' => '',
+    'help' => false,
+    ), array('h' => 'help'));
+
+if ($unrecognized) {
+    $unrecognized = implode("\n  ", $unrecognized);
+    cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
+}
+
+if ($options['help'] || !($options['courseid'] || $options['courseshortname'])) {
+    $help = <<<EOL
+Perform backup of the given course.
+
+Options:
+--courseid=INTEGER          Course ID for backup.
+--courseshortname=STRING    Course shortname for backup.
+--destination=STRING        Path where to store backup file. If not set the backup
+                            will be stored within the course backup file area.
+-h, --help                  Print out this help.
+
+Example:
+\$sudo -u www-data /usr/bin/php admin/cli/backup.php --courseid=2 --destination=/moodle/backup/\n
+EOL;
+
+    echo $help;
+    die;
+}
+
+$admin = get_admin();
+if (!$admin) {
+    mtrace("Error: No admin account was found");
+    die;
+}
+
+// Do we need to store backup somewhere else?
+$dir = rtrim($options['destination'], '/');
+if (!empty($dir)) {
+    if (!file_exists($dir) || !is_dir($dir) || !is_writable($dir)) {
+        mtrace("Destination directory does not exists or not writable.");
+        die;
+    }
+}
+
+// Check that the course exists.
+if ($options['courseid']) {
+    $course = $DB->get_record('course', array('id' => $options['courseid']), '*', MUST_EXIST);
+} else if ($options['courseshortname']) {
+    $course = $DB->get_record('course', array('shortname' => $options['courseshortname']), '*', MUST_EXIST);
+}
+
+cli_heading('Performing backup...');
+$bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE,
+                            backup::INTERACTIVE_YES, backup::MODE_GENERAL, $admin->id);
+// Set the default filename.
+$format = $bc->get_format();
+$type = $bc->get_type();
+$id = $bc->get_id();
+$users = $bc->get_plan()->get_setting('users')->get_value();
+$anonymised = $bc->get_plan()->get_setting('anonymize')->get_value();
+$filename = backup_plan_dbops::get_default_backup_filename($format, $type, $id, $users, $anonymised);
+$bc->get_plan()->get_setting('filename')->set_value($filename);
+
+// Execution.
+$bc->finish_ui();
+$bc->execute_plan();
+$results = $bc->get_results();
+$file = $results['backup_destination']; // May be empty if file already moved to target location.
+
+// Do we need to store backup somewhere else?
+if (!empty($dir)) {
+    if ($file) {
+        mtrace("Writing " . $dir.'/'.$filename);
+        if ($file->copy_content_to($dir.'/'.$filename)) {
+            $file->delete();
+            mtrace("Backup completed.");
+        } else {
+            mtrace("Destination directory does not exist or is not writable. Leaving the backup in the course backup file area.");
+        }
+    }
+} else {
+    mtrace("Backup completed, the new file is listed in the backup area of the given course");
+}
+$bc->destroy();
+exit(0);
\ No newline at end of file
index d8b2b51..b07e92f 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['aim'] = 'This administration tool helps developers and test writers to create .feature files describing Moodle\'s functionalities and run them automatically.';
+$string['aim'] = 'This administration tool helps developers and test writers to create .feature files describing Moodle\'s functionalities and run them automatically. Step definitions available for use in .feature files are listed below.';
 $string['allavailablesteps'] = 'All the available steps definitions';
+$string['errorbehatcommand'] = 'Error running behat CLI command. Try running "{$a} --help" manually from CLI to find out more about the problem.';
+$string['errorcomposer'] = 'Composer dependencies are not installed.';
+$string['errordataroot'] = '$CFG->behat_dataroot is not set or is invalid.';
+$string['errorsetconfig'] = '$CFG->behat_dataroot, $CFG->behat_prefix and $CFG->behat_wwwroot need to be set in config.php.';
+$string['erroruniqueconfig'] = '$CFG->behat_dataroot, $CFG->behat_prefix and $CFG->behat_wwwroot values need to be different than $CFG->dataroot, $CFG->prefix, $CFG->wwwroot, $CFG->phpunit_dataroot and $CFG->phpunit_prefix values.';
 $string['giveninfo'] = 'Given. Processes to set up the environment';
 $string['infoheading'] = 'Info';
 $string['installinfo'] = 'Read {$a} for installation and tests execution info';
-$string['moreinfoin'] = 'More info in {$a}';
 $string['newstepsinfo'] = 'Read {$a} for info about how to add new steps definitions';
 $string['newtestsinfo'] = 'Read {$a} for info about how to write new tests';
 $string['nostepsdefinitions'] = 'There aren\'t steps definitions matching this filters';
 $string['pluginname'] = 'Acceptance testing';
-$string['runclitool'] = 'To list the steps definitions you need to run the Behat CLI tool to create the $CFG->behat_dataroot directory. Go to your moodle dirroot and run "{$a}"';
 $string['stepsdefinitionscomponent'] = 'Area';
 $string['stepsdefinitionscontains'] = 'Contains';
 $string['stepsdefinitionsfilters'] = 'Steps definitions';
@@ -41,6 +44,7 @@ $string['theninfo'] = 'Then. Checkings to ensure the outcomes are the expected o
 $string['unknownexceptioninfo'] = 'There was a problem with Selenium or your browser. Please ensure you are using the latest version of Selenium. Error:';
 $string['viewsteps'] = 'Filter';
 $string['wheninfo'] = 'When. Actions that provokes an event';
-$string['wrongbehatsetup'] = 'Something is wrong with behat setup, ensure:<ul>
-<li>You ran "php admin/tool/behat/cli/init.php" from your moodle root directory</li>
-<li>vendor/bin/behat file has execution permissions</li></ul>';
+$string['wrongbehatsetup'] = 'Something is wrong with the behat setup and so step definitions cannot be listed: <b>{$a->errormsg}</b><br/><br/>Please check:<ul>
+<li>$CFG->behat_dataroot, $CFG->behat_prefix and $CFG->behat_wwwroot are set in config.php with different values from $CFG->dataroot, $CFG->prefix and $CFG->wwwroot.</li>
+<li>You ran "{$a->behatinit}" from your Moodle root directory.</li>
+<li>Dependencies are installed in vendor/ and {$a->behatcommand} file has execution permissions.</li></ul>';
index 324241b..cc5b4ab 100644 (file)
@@ -45,37 +45,7 @@ class tool_behat_renderer extends plugin_renderer_base {
      */
     public function render_stepsdefinitions($stepsdefinitions, $form) {
 
-        $title = get_string('pluginname', 'tool_behat');
-
-        // Header.
-        $html = $this->output->header();
-        $html .= $this->output->heading($title);
-
-        // Info.
-        $installurl = behat_command::DOCS_URL . '#Installation';
-        $installlink = html_writer::tag('a', $installurl, array('href' => $installurl, 'target' => '_blank'));
-        $writetestsurl = behat_command::DOCS_URL . '#Writting_features';
-        $writetestslink = html_writer::tag('a', $writetestsurl, array('href' => $writetestsurl, 'target' => '_blank'));
-        $writestepsurl = behat_command::DOCS_URL . '#Adding_steps_definitions';
-        $writestepslink = html_writer::tag('a', $writestepsurl, array('href' => $writestepsurl, 'target' => '_blank'));
-        $infos = array(
-            get_string('installinfo', 'tool_behat', $installlink),
-            get_string('newtestsinfo', 'tool_behat', $writetestslink),
-            get_string('newstepsinfo', 'tool_behat', $writestepslink)
-        );
-
-        // List of steps.
-        $html .= $this->output->box_start();
-        $html .= html_writer::tag('h1', get_string('infoheading', 'tool_behat'));
-        $html .= html_writer::tag('div', get_string('aim', 'tool_behat'));
-        $html .= html_writer::empty_tag('div');
-        $html .= html_writer::empty_tag('ul');
-        $html .= html_writer::empty_tag('li');
-        $html .= implode(html_writer::end_tag('li') . html_writer::empty_tag('li'), $infos);
-        $html .= html_writer::end_tag('li');
-        $html .= html_writer::end_tag('ul');
-        $html .= html_writer::end_tag('div');
-        $html .= $this->output->box_end();
+        $html = $this->generic_info();
 
         // Form.
         ob_start();
@@ -123,4 +93,75 @@ class tool_behat_renderer extends plugin_renderer_base {
 
         return $html;
     }
+
+    /**
+     * Renders an error message adding the generic info about the tool purpose and setup.
+     *
+     * @param string $msg The error message
+     * @return string HTML
+     */
+    public function render_error($msg) {
+
+        $html = $this->generic_info();
+
+        $a = new stdClass();
+        $a->errormsg = $msg;
+        $a->behatcommand = behat_command::get_behat_command();
+        $a->behatinit = 'php admin' . DIRECTORY_SEPARATOR . 'tool' . DIRECTORY_SEPARATOR .
+            'behat' . DIRECTORY_SEPARATOR . 'cli' . DIRECTORY_SEPARATOR . 'init.php';
+
+        $msg = get_string('wrongbehatsetup', 'tool_behat', $a);
+
+        // Error box including generic error string + specific error msg.
+        $html .= $this->output->box_start('box errorbox');
+        $html .= html_writer::tag('div', $msg);
+        $html .= $this->output->box_end();
+
+        $html .= $this->output->footer();
+
+        return $html;
+    }
+
+    /**
+     * Generic info about the tool.
+     *
+     * @return string
+     */
+    protected function generic_info() {
+
+        $title = get_string('pluginname', 'tool_behat');
+
+        // Header.
+        $html = $this->output->header();
+        $html .= $this->output->heading($title);
+
+        // Info.
+        $installurl = behat_command::DOCS_URL . '#Installation';
+        $installlink = html_writer::tag('a', $installurl, array('href' => $installurl, 'target' => '_blank'));
+        $writetestsurl = behat_command::DOCS_URL . '#Writting_features';
+        $writetestslink = html_writer::tag('a', $writetestsurl, array('href' => $writetestsurl, 'target' => '_blank'));
+        $writestepsurl = behat_command::DOCS_URL . '#Adding_steps_definitions';
+        $writestepslink = html_writer::tag('a', $writestepsurl, array('href' => $writestepsurl, 'target' => '_blank'));
+        $infos = array(
+            get_string('installinfo', 'tool_behat', $installlink),
+            get_string('newtestsinfo', 'tool_behat', $writetestslink),
+            get_string('newstepsinfo', 'tool_behat', $writestepslink)
+        );
+
+        // List of steps.
+        $html .= $this->output->box_start();
+        $html .= html_writer::tag('h1', get_string('infoheading', 'tool_behat'));
+        $html .= html_writer::tag('div', get_string('aim', 'tool_behat'));
+        $html .= html_writer::empty_tag('div');
+        $html .= html_writer::empty_tag('ul');
+        $html .= html_writer::empty_tag('li');
+        $html .= implode(html_writer::end_tag('li') . html_writer::empty_tag('li'), $infos);
+        $html .= html_writer::end_tag('li');
+        $html .= html_writer::end_tag('ul');
+        $html .= html_writer::end_tag('div');
+        $html .= $this->output->box_end();
+
+        return $html;
+    }
+
 }
index 12b96cd..223011a 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2013110501;
+$plugin->version   = 2014012900;
 $plugin->requires  = 2013110500; // Requires Moodle 2.5.
 $plugin->component = 'tool_behat';
index 18d0ef5..8a87fdb 100644 (file)
@@ -68,7 +68,7 @@ if ($usersettingsform->is_cancelled()) {
     //TODO: assign capability
 
     //display successful notification
-    $notification = $OUTPUT->notification(get_string('usersettingssaved', 'webservice'), 'success');
+    $notification = $OUTPUT->notification(get_string('usersettingssaved', 'webservice'), 'notifysuccess');
 }
 
 echo $OUTPUT->header();
index 97a1eca..9fccdd2 100644 (file)
@@ -28,11 +28,17 @@ defined('MOODLE_INTERNAL') || die();
 
 
 class auth_db_testcase extends advanced_testcase {
+    /** @var string Original error log */
+    protected $oldlog;
 
     protected function init_auth_database() {
         global $DB, $CFG;
         require_once("$CFG->dirroot/auth/db/auth.php");
 
+        // Discard error logs from AdoDB.
+        $this->oldlog = ini_get('error_log');
+        ini_set('error_log', "$CFG->dataroot/testlog.log");
+
         $dbman = $DB->get_manager();
 
         set_config('extencoding', 'utf-8', 'auth/db');
@@ -133,6 +139,8 @@ class auth_db_testcase extends advanced_testcase {
         $dbman = $DB->get_manager();
         $table = new xmldb_table('auth_db_users');
         $dbman->drop_table($table);
+
+        ini_set('error_log', $this->oldlog);
     }
 
     public function test_plugin() {
index 955b81e..13cdcc0 100644 (file)
@@ -268,10 +268,8 @@ class auth_ldap_plugin_testcase extends advanced_testcase {
         $sink->close();
 
         // Check that the event is valid.
-        $this->assertCount(2, $events);
-        $event = $events[0];
-        $this->assertInstanceOf('\core\event\user_updated', $event);
-        $event = $events[1];
+        $this->assertCount(1, $events);
+        $event = reset($events);
         $this->assertInstanceOf('\core\event\user_loggedin', $event);
         $this->assertEquals('user', $event->objecttable);
         $this->assertEquals('2', $event->objectid);
index 97b3271..aa074b4 100644 (file)
@@ -233,7 +233,7 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
         $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
         $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY));
         $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_ACTIVITY, 'badgeid' => $badge->id));
-        $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY, 'module_'.$this->module->id => $this->module->id));
+        $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY, 'module_'.$this->module->cmid => $this->module->cmid));
 
         // Set completion for forum activity.
         $c = new completion_info($this->course);
index 7cb8e88..1e7aae4 100644 (file)
@@ -88,7 +88,7 @@ class block_comments_events_testcase extends advanced_testcase {
         $this->assertEquals($url, $event->get_url());
 
         // Comments when block is on module (wiki) page.
-        $context = context_module::instance($this->wiki->id);
+        $context = context_module::instance($this->wiki->cmid);
         $args = new stdClass;
         $args->context   = $context;
         $args->course    = $this->course;
@@ -111,8 +111,9 @@ class block_comments_events_testcase extends advanced_testcase {
         // Checking that the event contains the expected values.
         $this->assertInstanceOf('\block_comments\event\comment_created', $event);
         $this->assertEquals($context, $event->get_context());
-        $url = new moodle_url('/mod/wiki/view.php', array('id' => $this->wiki->id));
+        $url = new moodle_url('/mod/wiki/view.php', array('id' => $this->wiki->cmid));
         $this->assertEquals($url, $event->get_url());
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -152,7 +153,7 @@ class block_comments_events_testcase extends advanced_testcase {
         $this->assertEquals($url, $event->get_url());
 
         // Comments when block is on module (wiki) page.
-        $context = context_module::instance($this->wiki->id);
+        $context = context_module::instance($this->wiki->cmid);
         $args = new stdClass;
         $args->context   = $context;
         $args->course    = $this->course;
@@ -176,7 +177,8 @@ class block_comments_events_testcase extends advanced_testcase {
         // Checking that the event contains the expected values.
         $this->assertInstanceOf('\block_comments\event\comment_deleted', $event);
         $this->assertEquals($context, $event->get_context());
-        $url = new moodle_url('/mod/wiki/view.php', array('id' => $this->wiki->id));
+        $url = new moodle_url('/mod/wiki/view.php', array('id' => $this->wiki->cmid));
         $this->assertEquals($url, $event->get_url());
+        $this->assertEventContextNotUsed($event);
     }
 }
index 0109fd4..b4af764 100644 (file)
@@ -183,6 +183,7 @@ class core_bloglib_testcase extends advanced_testcase {
         $this->assertEventLegacyLogData($arr, $event);
         $this->assertEquals("blog_entry_added", $event->get_legacy_eventname());
         $this->assertEventLegacyData($blog, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -215,6 +216,7 @@ class core_bloglib_testcase extends advanced_testcase {
         $this->assertEventLegacyData($blog, $event);
         $arr = array (SITEID, 'blog', 'update', 'index.php?userid=' . $this->userid . '&entryid=' . $blog->id, $blog->subject);
         $this->assertEventLegacyLogData($arr, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -249,6 +251,7 @@ class core_bloglib_testcase extends advanced_testcase {
                 $blog->id);
         $this->assertEventLegacyLogData($arr, $event);
         $this->assertEventLegacyData($blog, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
 
@@ -301,6 +304,7 @@ class core_bloglib_testcase extends advanced_testcase {
         $arr = array(SITEID, 'blog', 'add association', 'index.php?userid=' . $this->userid . '&entryid=' . $blog->id,
                      $blog->subject, $this->cmid, $this->userid);
         $this->assertEventLegacyLogData($arr, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -381,6 +385,7 @@ class core_bloglib_testcase extends advanced_testcase {
         $this->assertEquals($url, $event->get_url());
         $arr = array(SITEID, 'blog', 'view', $url2->out(), 'view blog entry');
         $this->assertEventLegacyLogData($arr, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -416,6 +421,7 @@ class core_bloglib_testcase extends advanced_testcase {
         $this->assertEquals($this->postid, $event->other['itemid']);
         $url = new moodle_url('/blog/index.php', array('entryid' => $this->postid));
         $this->assertEquals($url, $event->get_url());
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -452,6 +458,7 @@ class core_bloglib_testcase extends advanced_testcase {
         $this->assertEquals($this->postid, $event->other['itemid']);
         $url = new moodle_url('/blog/index.php', array('entryid' => $this->postid));
         $this->assertEquals($url, $event->get_url());
+        $this->assertEventContextNotUsed($event);
     }
 }
 
index 994aef2..c59af74 100644 (file)
@@ -1742,7 +1742,7 @@ class cache_session extends cache {
      * Purges the session cache of all data belonging to the current user.
      */
     public function purge_current_user() {
-        $keys = $this->get_store()->find_all($this->get_key_prefix());
+        $keys = $this->get_store()->find_by_prefix($this->get_key_prefix());
         $this->get_store()->delete_many($keys);
     }
 
index 64f40c7..6392b84 100644 (file)
@@ -106,6 +106,7 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertEquals($cohort->contextid, $event->contextid);
         $this->assertEquals($cohort, $event->get_record_snapshot('cohort', $id));
         $this->assertEventLegacyData($cohort, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     public function test_cohort_update_cohort() {
@@ -176,6 +177,7 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertEquals($updatedcohort->contextid, $event->contextid);
         $this->assertEquals($cohort, $event->get_record_snapshot('cohort', $id));
         $this->assertEventLegacyData($cohort, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     public function test_cohort_delete_cohort() {
@@ -213,6 +215,7 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertEquals($cohort->id, $event->objectid);
         $this->assertEquals($cohort, $event->get_record_snapshot('cohort', $cohort->id));
         $this->assertEventLegacyData($cohort, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     public function test_cohort_delete_category() {
@@ -270,6 +273,7 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertEquals($user->id, $event->relateduserid);
         $this->assertEquals($USER->id, $event->userid);
         $this->assertEventLegacyData((object) array('cohortid' => $cohort->id, 'userid' => $user->id), $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     public function test_cohort_remove_member() {
@@ -313,6 +317,7 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertEquals($user->id, $event->relateduserid);
         $this->assertEquals($USER->id, $event->userid);
         $this->assertEventLegacyData((object) array('cohortid' => $cohort->id, 'userid' => $user->id), $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     public function test_cohort_is_member() {
index 6733547..b125d6a 100644 (file)
@@ -470,6 +470,33 @@ $CFG->admin = 'admin';
 // will be sent to supportemail.
 //      $CFG->supportuserid = -20;
 //
+// Moodle 2.7 introduces a locking api for critical tasks (e.g. cron).
+// The default locking system to use is DB locking for MySQL and Postgres, and File
+// locking for Oracle and SQLServer. If $CFG->preventfilelocking is set, then the default
+// will always be DB locking. It can be manually set to one of the lock
+// factory classes listed below, or one of your own custom classes implementing the
+// \core\lock\lock_factory interface.
+//
+//      $CFG->lock_factory = "auto";
+//
+// The list of available lock factories is:
+//
+// "\\core\\lock\\file_lock_factory" - File locking
+//      Uses lock files stored by default in the dataroot. Whether this
+//      works on clusters depends on the file system used for the dataroot.
+//
+// "\\core\\lock\\db_row_lock_factory" - DB locking based on table rows.
+//
+// "\\core\\lock\\postgres_lock_factory" - DB locking based on postgres advisory locks.
+//
+// "\\core\\lock\\mysql_lock_factory" - DB locking based on mysql lock functions.
+//
+// Settings used by the lock factories
+//
+// Location for lock files used by the File locking factory. This must exist
+// on a shared file system that supports locking.
+//      $CFG->lock_file_root = $CFG->dataroot . '/lock';
+//
 //=========================================================================
 // 7. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
 //=========================================================================
index 00b19d4..5207f10 100644 (file)
@@ -102,31 +102,32 @@ M.course_dndupload = {
      * is available (or to explain why it is not available)
      */
     add_status_div: function() {
-        var coursecontents = document.getElementById(this.pagecontentid);
+        var Y = this.Y,
+            coursecontents = Y.one('#' + this.pagecontentid),
+            div,
+            handlefile = (this.handlers.filehandlers.length > 0),
+            handletext = false,
+            handlelink = false,
+            i = 0,
+            styletop,
+            styletopunit;
+
         if (!coursecontents) {
             return;
         }
 
-        var div = document.createElement('div');
-        div.id = 'dndupload-status';
-        div.style.opacity = 0.0;
-        coursecontents.insertBefore(div, coursecontents.firstChild);
+        div = Y.Node.create('<div id="dndupload-status"></div>').setStyle('opacity', '0.0');
+        coursecontents.insert(div, 0);
 
-        var Y = this.Y;
-        div = Y.one(div);
-        var handlefile = (this.handlers.filehandlers.length > 0);
-        var handletext = false;
-        var handlelink = false;
-        var i;
-        for (i=0; i<this.handlers.types.length; i++) {
+        for (i = 0; i < this.handlers.types.length; i++) {
             switch (this.handlers.types[i].identifier) {
-            case 'text':
-            case 'text/html':
-                handletext = true;
-                break;
-            case 'url':
-                handlelink = true;
-                break;
+                case 'text':
+                case 'text/html':
+                    handletext = true;
+                    break;
+                case 'url':
+                    handlelink = true;
+                    break;
             }
         }
         $msgident = 'dndworking';
@@ -141,16 +142,20 @@ M.course_dndupload = {
         }
         div.setContent(M.util.get_string($msgident, 'moodle'));
 
+        styletop = div.getStyle('top') || '0px';
+        styletopunit = styletop.replace(/^\d+/, '');
+        styletop = parseInt(styletop.replace(/\D*$/, ''), 10);
+
         var fadeanim = new Y.Anim({
             node: '#dndupload-status',
             from: {
                 opacity: 0.0,
-                top: '-30px'
+                top: (styletop - 30).toString() + styletopunit
             },
 
             to: {
                 opacity: 1.0,
-                top: '0px'
+                top: styletop.toString() + styletopunit
             },
             duration: 0.5
         });
index 2dcbce2..1de01c9 100644 (file)
@@ -321,6 +321,7 @@ class course_edit_form extends moodleform {
                     $options[$grouping->id] = format_string($grouping->name);
                 }
             }
+            core_collator::asort($options);
             $gr_el =& $mform->getElement('defaultgroupingid');
             $gr_el->load($options);
         }
index 7663e93..7161284 100644 (file)
@@ -67,13 +67,14 @@ class editsection_form extends moodleform {
             // Grouping conditions - only if grouping is enabled at site level
             if (!empty($CFG->enablegroupmembersonly)) {
                 $options = array();
-                $options[0] = get_string('none');
                 if ($groupings = $DB->get_records('groupings', array('courseid' => $course->id))) {
                     foreach ($groupings as $grouping) {
                         $options[$grouping->id] = format_string(
                                 $grouping->name, true, array('context' => $context));
                     }
                 }
+                core_collator::asort($options);
+                $options = array(0 => get_string('none')) + $options;
                 $mform->addElement('select', 'groupingid', get_string('groupingsection', 'group'), $options);
                 $mform->addHelpButton('groupingid', 'groupingsection', 'group');
             }
index b719a13..edc7d87 100644 (file)
@@ -373,7 +373,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
                 if ($cancomplete && $completioninfo->is_enabled($thismod) != COMPLETION_TRACKING_NONE) {
                     $total++;
                     $completiondata = $completioninfo->get_data($thismod, true);
-                    if ($completiondata->completionstate == COMPLETION_COMPLETE) {
+                    if ($completiondata->completionstate == COMPLETION_COMPLETE ||
+                            $completiondata->completionstate == COMPLETION_COMPLETE_PASS) {
                         $complete++;
                     }
                 }
index b398be4..9b54833 100644 (file)
@@ -504,12 +504,13 @@ abstract class moodleform_mod extends moodleform {
         if ($this->_features->groupings or $this->_features->groupmembersonly) {
             //groupings selector - used for normal grouping mode or also when restricting access with groupmembersonly
             $options = array();
-            $options[0] = get_string('none');
             if ($groupings = $DB->get_records('groupings', array('courseid'=>$COURSE->id))) {
                 foreach ($groupings as $grouping) {
                     $options[$grouping->id] = format_string($grouping->name);
                 }
             }
+            core_collator::asort($options);
+            $options = array(0 => get_string('none')) + $options;
             $mform->addElement('select', 'groupingid', get_string('grouping', 'group'), $options);
             $mform->addHelpButton('groupingid', 'grouping', 'group');
         }
index 3f8d5b8..f2f1a69 100644 (file)
@@ -26,8 +26,9 @@
 defined('MOODLE_INTERNAL') || die();
 
 global $CFG;
-require_once($CFG->dirroot.'/course/lib.php');
-require_once($CFG->dirroot.'/course/tests/fixtures/course_capability_assignment.php');
+require_once($CFG->dirroot . '/course/lib.php');
+require_once($CFG->dirroot . '/course/tests/fixtures/course_capability_assignment.php');
+require_once($CFG->dirroot . '/enrol/imsenterprise/tests/imsenterprise_test.php');
 
 class core_course_courselib_testcase extends advanced_testcase {
 
@@ -1400,8 +1401,10 @@ class core_course_courselib_testcase extends advanced_testcase {
         // Catch the events.
         $sink = $this->redirectEvents();
 
-        // Create the course.
-        $course = $this->getDataGenerator()->create_course();
+        // Create the course with an id number which is used later when generating a course via the imsenterprise plugin.
+        $data = new stdClass();
+        $data->idnumber = 'idnumber';
+        $course = $this->getDataGenerator()->create_course($data);
         // Get course from DB for comparison.
         $course = $DB->get_record('course', array('id' => $course->id));
 
@@ -1420,6 +1423,33 @@ class core_course_courselib_testcase extends advanced_testcase {
         $this->assertEventLegacyData($course, $event);
         $expectedlog = array(SITEID, 'course', 'new', 'view.php?id=' . $course->id, $course->fullname . ' (ID ' . $course->id . ')');
         $this->assertEventLegacyLogData($expectedlog, $event);
+
+        // Now we want to trigger creating a course via the imsenterprise.
+        // Delete the course we created earlier, as we want the imsenterprise plugin to create this.
+        // We do not want print out any of the text this function generates while doing this, which is why
+        // we are using ob_start() and ob_end_clean().
+        ob_start();
+        delete_course($course);
+        ob_end_clean();
+
+        // Create the XML file we want to use.
+        $imstestcase = new enrol_imsenterprise_testcase();
+        $imstestcase->imsplugin = enrol_get_plugin('imsenterprise');
+        $imstestcase->set_test_config();
+        $imstestcase->set_xml_file(false, array($course));
+
+        // Capture the event.
+        $sink = $this->redirectEvents();
+        $imstestcase->imsplugin->cron();
+        $events = $sink->get_events();
+        $sink->close();
+        $event = $events[0];
+
+        // Validate the event triggered is \core\event\course_created. There is no need to validate the other values
+        // as they have already been validated in the previous steps. Here we only want to make sure that when the
+        // imsenterprise plugin creates a course an event is triggered.
+        $this->assertInstanceOf('\core\event\course_created', $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -1498,6 +1528,7 @@ class core_course_courselib_testcase extends advanced_testcase {
         $this->assertEventLegacyData($movedcoursehidden, $event);
         $expectedlog = array($movedcoursehidden->id, 'course', 'move', 'edit.php?id=' . $movedcoursehidden->id, $movedcoursehidden->id);
         $this->assertEventLegacyLogData($expectedlog, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -1543,6 +1574,7 @@ class core_course_courselib_testcase extends advanced_testcase {
         $this->assertEventLegacyData($course, $event);
         $expectedlog = array(SITEID, 'course', 'delete', 'view.php?id=' . $course->id, $course->fullname . '(ID ' . $course->id . ')');
         $this->assertEventLegacyLogData($expectedlog, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -1584,6 +1616,7 @@ class core_course_courselib_testcase extends advanced_testcase {
         $course->context = $coursecontext;
         $course->options = array();
         $this->assertEventLegacyData($course, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -1646,6 +1679,7 @@ class core_course_courselib_testcase extends advanced_testcase {
         $this->assertEventLegacyData($category2, $event);
         $expectedlog = array(SITEID, 'category', 'delete', 'index.php', $category2->name . '(ID ' . $category2->id . ')');
         $this->assertEventLegacyLogData($expectedlog, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -1711,6 +1745,7 @@ class core_course_courselib_testcase extends advanced_testcase {
             'samesite' => $rc->is_samesite()
         );
         $this->assertEventLegacyData($legacydata, $event);
+        $this->assertEventContextNotUsed($event);
 
         // Destroy the resource controller since we are done using it.
         $rc->destroy();
@@ -1765,6 +1800,7 @@ class core_course_courselib_testcase extends advanced_testcase {
         $sectionnum = $section->section;
         $expectedlegacydata = array($course->id, "course", "editsection", 'editsection.php?id=' . $id, $sectionnum);
         $this->assertEventLegacyLogData($expectedlegacydata, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     public function test_course_integrity_check() {
@@ -1924,6 +1960,7 @@ class core_course_courselib_testcase extends advanced_testcase {
 
         $arr = array($cm->course, "course", "add mod", "../mod/assign/view.php?id=$mod->id", "assign $cm->instance");
         $this->assertEventLegacyLogData($arr, $event);
+        $this->assertEventContextNotUsed($event);
 
     }
 
@@ -2029,7 +2066,7 @@ class core_course_courselib_testcase extends advanced_testcase {
 
         $arr = array($cm->course, "course", "update mod", "../mod/forum/view.php?id=$mod->id", "forum $cm->instance");
         $this->assertEventLegacyLogData($arr, $event);
-
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
index e46d1b1..9e73cc9 100644 (file)
@@ -31,9 +31,16 @@ class enrol_database_testcase extends advanced_testcase {
     protected static $users = array();
     protected static $roles = array();
 
+    /** @var string Original error log */
+    protected $oldlog;
+
     protected function init_enrol_database() {
         global $DB, $CFG;
 
+        // Discard error logs from AdoDB.
+        $this->oldlog = ini_get('error_log');
+        ini_set('error_log', "$CFG->dataroot/testlog.log");
+
         $dbman = $DB->get_manager();
 
         set_config('dbencoding', 'utf-8', 'enrol_database');
@@ -160,6 +167,8 @@ class enrol_database_testcase extends advanced_testcase {
         self::$courses = null;
         self::$users = null;
         self::$roles = null;
+
+        ini_set('error_log', $this->oldlog);
     }
 
     protected function reset_enrol_database() {
index 34a0442..6db0851 100644 (file)
@@ -377,24 +377,11 @@ class enrol_imsenterprise_plugin extends enrol_plugin {
                         } else {
                             $course->category = $this->get_default_category_id();
                         }
-                        $course->timecreated = time();
                         $course->startdate = time();
                         // Choose a sort order that puts us at the start of the list!
                         $course->sortorder = 0;
-                        $courseid = $DB->insert_record('course', $course);
 
-                        // Setup default enrolment plugins.
-                        $course->id = $courseid;
-                        enrol_course_updated(true, $course, null);
-
-                        // Setup the blocks.
-                        $course = $DB->get_record('course', array('id' => $courseid));
-                        blocks_add_default_course_blocks($course);
-
-                        // Create default 0-section.
-                        course_create_sections_if_missing($course, 0);
-
-                        add_to_log(SITEID, "course", "new", "view.php?id=$course->id", "$course->fullname (ID $course->id)");
+                        $course = create_course($course);
 
                         $this->log_line("Created course $coursecode in Moodle (Moodle ID is $course->id)");
                     }
index 390f276..a86006e 100644 (file)
@@ -42,7 +42,7 @@ class enrol_imsenterprise_testcase extends advanced_testcase {
     /**
      * @var $imsplugin enrol_imsenterprise_plugin IMS plugin instance.
      */
-    protected $imsplugin;
+    public $imsplugin;
 
     /**
      * Setup required for all tests.
@@ -254,7 +254,7 @@ class enrol_imsenterprise_testcase extends advanced_testcase {
     /**
      * Sets the plugin configuration for testing
      */
-    protected function set_test_config() {
+    public function set_test_config() {
         $this->imsplugin->set_config('mailadmins', false);
         $this->imsplugin->set_config('prev_path', '');
         $this->imsplugin->set_config('createnewusers', true);
@@ -268,7 +268,7 @@ class enrol_imsenterprise_testcase extends advanced_testcase {
      * @param bool|array $users false or array of users StdClass
      * @param bool|array $courses false or of courses StdClass
      */
-    protected function set_xml_file($users = false, $courses = false) {
+    public function set_xml_file($users = false, $courses = false) {
 
         $xmlcontent = '<enterprise>';
 
index 7d91e4b..6c86e6f 100644 (file)
@@ -123,7 +123,6 @@ if ($canenrol && optional_param('add', false, PARAM_BOOL) && confirm_sesskey())
                 $timeend = $timestart + $extendperiod;
             }
             $enrol_manual->enrol_user($instance, $adduser->id, $roleid, $timestart, $timeend);
-            add_to_log($course->id, 'course', 'enrol', '../enrol/users.php?id='.$course->id, $course->id); //there should be userid somewhere!
         }
 
         $potentialuserselector->invalidate_selected_users();
@@ -139,7 +138,6 @@ if ($canunenrol && optional_param('remove', false, PARAM_BOOL) && confirm_sesske
     if (!empty($userstounassign)) {
         foreach($userstounassign as $removeuser) {
             $enrol_manual->unenrol_user($instance, $removeuser->id);
-            add_to_log($course->id, 'course', 'unenrol', '../enrol/users.php?id='.$course->id, $course->id); //there should be userid somewhere!
         }
 
         $potentialuserselector->invalidate_selected_users();
index c7304fb..9dd3bc5 100644 (file)
@@ -49,7 +49,7 @@ $PAGE->set_title($plugin->get_instance_name($instance));
 
 if ($confirm and confirm_sesskey()) {
     $plugin->unenrol_user($instance, $USER->id);
-    add_to_log($course->id, 'course', 'unenrol', '../enrol/users.php?id='.$course->id, $course->id); //TODO: there should be userid somewhere!
+
     redirect(new moodle_url('/index.php'));
 }
 
index 05cb522..a23c148 100644 (file)
@@ -474,6 +474,7 @@ class enrol_meta_plugin_testcase extends advanced_testcase {
         $expectedlegacyeventdata->enrol = 'meta';
         $expectedlegacyeventdata->courseid = $course2->id;
         $this->assertEventLegacyData($expectedlegacyeventdata, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -507,6 +508,7 @@ class enrol_meta_plugin_testcase extends advanced_testcase {
         $this->assertEquals(0, $DB->count_records('user_enrolments'));
         $this->assertInstanceOf('\core\event\user_enrolment_deleted', $event);
         $this->assertEquals('user_unenrolled', $event->get_legacy_eventname());
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -546,5 +548,6 @@ class enrol_meta_plugin_testcase extends advanced_testcase {
         $expectedlegacyeventdata->enrol = 'meta';
         $expectedlegacyeventdata->courseid = $course2->id;
         $this->assertEventLegacyData($expectedlegacyeventdata, $event);
+        $this->assertEventContextNotUsed($event);
     }
 }
index 8fd16d2..61155c8 100644 (file)
@@ -49,7 +49,7 @@ $PAGE->set_title($plugin->get_instance_name($instance));
 
 if ($confirm and confirm_sesskey()) {
     $plugin->unenrol_user($instance, $USER->id);
-    add_to_log($course->id, 'course', 'unenrol', '../enrol/users.php?id='.$course->id, $course->id); //there should be userid somewhere!
+
     redirect(new moodle_url('/index.php'));
 }
 
index d64281a..edc114f 100644 (file)
@@ -195,7 +195,6 @@ class enrol_self_plugin extends enrol_plugin {
         }
 
         $this->enrol_user($instance, $USER->id, $instance->roleid, $timestart, $timeend);
-        add_to_log($instance->courseid, 'course', 'enrol', '../enrol/users.php?id='.$instance->courseid, $instance->courseid); //TODO: There should be userid somewhere!
 
         if ($instance->password and $instance->customint1 and $data->enrolpassword !== $instance->password) {
             // It must be a group enrolment, let's assign group too.
index 7e7d269..2da62ee 100644 (file)
@@ -49,7 +49,7 @@ $PAGE->set_title($plugin->get_instance_name($instance));
 
 if ($confirm and confirm_sesskey()) {
     $plugin->unenrol_user($instance, $USER->id);
-    add_to_log($course->id, 'course', 'unenrol', '../enrol/users.php?id='.$course->id, $course->id); //TODO: there should be userid somewhere!
+
     redirect(new moodle_url('/index.php'));
 }
 
index fcf9cad..61a4392 100644 (file)
@@ -310,10 +310,56 @@ class core_enrollib_testcase extends advanced_testcase {
         $dbuserenrolled = $DB->get_record('user_enrolments', array('userid' => $admin->id));
         $this->assertInstanceOf('\core\event\user_enrolment_created', $event);
         $this->assertEquals($dbuserenrolled->id, $event->objectid);
+        $this->assertEquals(context_course::instance($course1->id), $event->get_context());
         $this->assertEquals('user_enrolled', $event->get_legacy_eventname());
         $expectedlegacyeventdata = $dbuserenrolled;
         $expectedlegacyeventdata->enrol = $manual->get_name();
         $expectedlegacyeventdata->courseid = $course1->id;
         $this->assertEventLegacyData($expectedlegacyeventdata, $event);
+        $expected = array($course1->id, 'course', 'enrol', '../enrol/users.php?id=' . $course1->id, $course1->id);
+        $this->assertEventLegacyLogData($expected, $event);
+        $this->assertEventContextNotUsed($event);
+    }
+
+    /**
+     * Test user_enrolment_deleted event.
+     */
+    public function test_user_enrolment_deleted_event() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $manualplugin = enrol_get_plugin('manual');
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $student = $DB->get_record('role', array('shortname' => 'student'));
+
+        $enrol = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'manual'), '*', MUST_EXIST);
+
+        // Enrol user.
+        $manualplugin->enrol_user($enrol, $user->id, $student->id);
+
+        // Get the user enrolment information, used to validate legacy event data.
+        $dbuserenrolled = $DB->get_record('user_enrolments', array('userid' => $user->id));
+
+        // Unenrol user and capture event.
+        $sink = $this->redirectEvents();
+        $manualplugin->unenrol_user($enrol, $user->id);
+        $events = $sink->get_events();
+        $sink->close();
+        $event = array_pop($events);
+
+        // Validate the event.
+        $this->assertInstanceOf('\core\event\user_enrolment_deleted', $event);
+        $this->assertEquals(context_course::instance($course->id), $event->get_context());
+        $this->assertEquals('user_unenrolled', $event->get_legacy_eventname());
+        $expectedlegacyeventdata = $dbuserenrolled;
+        $expectedlegacyeventdata->enrol = $manualplugin->get_name();
+        $expectedlegacyeventdata->courseid = $course->id;
+        $expectedlegacyeventdata->lastenrol = true;
+        $this->assertEventLegacyData($expectedlegacyeventdata, $event);
+        $expected = array($course->id, 'course', 'unenrol', '../enrol/users.php?id=' . $course->id, $course->id);
+        $this->assertEventLegacyLogData($expected, $event);
+        $this->assertEventContextNotUsed($event);
     }
 }
index 36383fb..bf34e14 100644 (file)
@@ -223,7 +223,7 @@ class core_files_externallib_testcase extends advanced_testcase {
         // Insert the information about the file.
         $contentid = $DB->insert_record('data_content', $datacontent);
         // Required information for uploading a file.
-        $context = context_module::instance($module->id);
+        $context = context_module::instance($module->cmid);
         $usercontext = context_user::instance($USER->id);
         $component = 'mod_data';
         $filearea = 'content';
@@ -301,7 +301,7 @@ class core_files_externallib_testcase extends advanced_testcase {
         $modified = 0;
         // Context level and instance ID are used to determine what the context is.
         $contextlevel = 'module';
-        $instanceid = $module->id;
+        $instanceid = $module->cmid;
         $testfilelisting = core_files_external::get_files($nocontext, $component, $filearea, $itemid, '/', $filename, $modified, $contextlevel, $instanceid);
         $this->assertEquals($testfilelisting, $testdata);
     }
index 75af83a..6ca09c9 100644 (file)
@@ -65,7 +65,7 @@ class backup_gradingform_guide_plugin extends backup_gradingform_plugin {
 
         $pluginwrapper->add_child($criteria);
         $criteria->add_child($criterion);
-        $criteria->add_child($comments);
+        $pluginwrapper->add_child($comments);
         $comments->add_child($comment);
 
         // Set sources to populate the data.
index dd99814..f833404 100644 (file)
@@ -48,6 +48,11 @@ class restore_gradingform_guide_plugin extends restore_gradingform_plugin {
         $paths[] = new restore_path_element('gradingform_guide_comment',
             $this->get_pathfor('/guidecomments/guidecomment'));
 
+        // MDL-37714: Correctly locate frequent used comments in both the
+        // current and incorrect old format.
+        $paths[] = new restore_path_element('gradingform_guide_comment_legacy',
+            $this->get_pathfor('/guidecriteria/guidecomments/guidecomment'));
+
         return $paths;
     }
 
@@ -99,6 +104,20 @@ class restore_gradingform_guide_plugin extends restore_gradingform_plugin {
         $DB->insert_record('gradingform_guide_comments', $data);
     }
 
+    /**
+     * Processes comments element data
+     *
+     * @param array|stdClass $data The data to insert as a comment
+     */
+    public function process_gradingform_guide_comment_legacy($data) {
+        global $DB;
+
+        $data = (object)$data;
+        $data->definitionid = $this->get_new_parentid('grading_definition');
+
+        $DB->insert_record('gradingform_guide_comments', $data);
+    }
+
     /**
      * Processes filling element data
      *
index f3d768c..051c155 100644 (file)
@@ -52,7 +52,7 @@ class core_grade_edittreelib_testcase extends advanced_testcase {
         $scale = $this->getDataGenerator()->create_scale();
         $course = $this->getDataGenerator()->create_course();
         $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id));
-        $modulecontext = context_module::instance($assign->id);
+        $modulecontext = context_module::instance($assign->cmid);
         // The generator returns a dummy object, lets get the real assign object.
         $assign = new assign($modulecontext, false, false);
         $cm = $assign->get_course_module();
index 3230236..e341297 100644 (file)
@@ -66,7 +66,7 @@ class core_grading_externallib_testcase extends externallib_advanced_testcase {
         // Create a teacher and give them capabilities.
         $coursecontext = context_course::instance($course->id);
         $roleid = $this->assignUserCapability('moodle/course:viewparticipants', $coursecontext->id, 3);
-        $modulecontext = context_module::instance($cm->id);
+        $modulecontext = context_module::instance($cm->cmid);
         $this->assignUserCapability('mod/assign:grade', $modulecontext->id, $roleid);
 
         // Create the teacher's enrolment record.
@@ -146,7 +146,7 @@ class core_grading_externallib_testcase extends externallib_advanced_testcase {
         $DB->insert_record('gradingform_rubric_levels', $rubriclevel2);
 
         // Call the external function.
-        $cmids = array ($cm->id);
+        $cmids = array ($cm->cmid);
         $areaname = 'submissions';
         $result = core_grading_external::get_definitions($cmids, $areaname);
 
@@ -209,7 +209,7 @@ class core_grading_externallib_testcase extends externallib_advanced_testcase {
         // Create a teacher and give them capabilities.
         $coursecontext = context_course::instance($course->id);
         $roleid = $this->assignUserCapability('moodle/course:viewparticipants', $coursecontext->id, 3);
-        $modulecontext = context_module::instance($assign->id);
+        $modulecontext = context_module::instance($assign->cmid);
         $this->assignUserCapability('mod/assign:grade', $modulecontext->id, $roleid);
 
         // Create the teacher's enrolment record.
index fef76b1..ca22495 100644 (file)
@@ -64,22 +64,6 @@ function UpdatableGroupsCombo(wwwRoot, courseId) {
         }
 
     };
-
-    // Add onchange event to groups list box.
-    // Okay, this is not working in IE. The onchange is never fired...
-    // I'm hard coding the onchange in ../index.php. Not ideal, but it works
-    // then. vyshane AT moodle DOT com.
-    /*
-    groupsComboEl = document.getElementById("groups");
-    if (groupsComboEl) {
-        groupsComboEl.setAttribute("onchange", "membersCombo.refreshMembers(this.options[this.selectedIndex].value);");
-    }
-    */
-
-    // Hide the updategroups input since AJAX will take care of this.
-    YUI().use('yui2-dom', function (Y) {
-        Y.YUI2.util.Dom.setStyle("updategroups", "display", "none");
-    });
 }
 
 
@@ -131,9 +115,10 @@ function UpdatableMembersCombo(wwwRoot, courseId) {
     };
 
     // Hide the updatemembers input since AJAX will take care of this.
-    YUI().use('yui2-dom', function (Y) {
-        Y.YUI2.util.Dom.setStyle("updatemembers", "display", "none");
-    });
+    var updatemembers = Y.one('#updatemembers');
+    if (updatemembers) {
+        updatemembers.hide();
+    }
 }
 
 /**
index ec8b094..063397e 100644 (file)
@@ -68,9 +68,13 @@ echo $OUTPUT->heading($strgroupings);
 $data = array();
 if ($groupings = $DB->get_records('groupings', array('courseid'=>$course->id), 'name')) {
     $canchangeidnumber = has_capability('moodle/course:changeidnumber', $context);
+    foreach ($groupings as $gid => $grouping) {
+        $groupings[$gid]->formattedname = format_string($grouping->name, true, array('context' => $context));
+    }
+    core_collator::asort_objects_by_property($groupings, 'formattedname');
     foreach($groupings as $grouping) {
         $line = array();
-        $line[0] = format_string($grouping->name);
+        $line[0] = $grouping->formattedname;
 
         if ($groups = groups_get_all_groups($courseid, 0, $grouping->id)) {
             $groupnames = array();
index b3ae333..967f2c1 100644 (file)
@@ -62,8 +62,12 @@ $strfiltergroups     = get_string('filtergroups', 'group');
 $strnogroups         = get_string('nogroups', 'group');
 $strdescription      = get_string('description');
 
-// Get all groupings
+// Get all groupings and sort them by formatted name.
 $groupings = $DB->get_records('groupings', array('courseid'=>$courseid), 'name');
+foreach ($groupings as $gid => $grouping) {
+    $groupings[$gid]->formattedname = format_string($grouping->name, true, array('context' => $context));
+}
+core_collator::asort_objects_by_property($groupings, 'formattedname');
 $members = array();
 foreach ($groupings as $grouping) {
     $members[$grouping->id] = array();
@@ -136,7 +140,7 @@ echo $strfiltergroups;
 $options = array();
 $options[0] = get_string('all');
 foreach ($groupings as $grouping) {
-    $options[$grouping->id] = strip_tags(format_string($grouping->name));
+    $options[$grouping->id] = strip_tags($grouping->formattedname);
 }
 $popupurl = new moodle_url($rooturl.'&group='.$groupid);
 $select = new single_select($popupurl, 'grouping', $options, $groupingid, array());
@@ -199,7 +203,7 @@ foreach ($members as $gpgid=>$groupdata) {
     if ($gpgid < 0) {
         echo $OUTPUT->heading($strnotingrouping, 3);
     } else {
-        echo $OUTPUT->heading(format_string($groupings[$gpgid]->name), 3);
+        echo $OUTPUT->heading($groupings[$gpgid]->formattedname, 3);
         $description = file_rewrite_pluginfile_urls($groupings[$gpgid]->description, 'pluginfile.php', $context->id, 'grouping', 'description', $gpgid);
         $options = new stdClass;
         $options->noclean = true;
index 0388de7..fed5364 100644 (file)
@@ -46,7 +46,8 @@ $string['dmlexceptiononinstall'] = '<p>Une erreur de base de données est surven
 $string['downloadedfilecheckfailed'] = 'La vérification du fichier téléchargé à échoué';
 $string['invalidmd5'] = 'Le code de contrôle md5 n\'est pas valide';
 $string['missingrequiredfield'] = 'Un champ obligatoire n\'est pas renseigné';
-$string['remotedownloaderror'] = 'Le téléchargement de composants sur votre serveur a échoué. Veuillez vérifier les réglages de proxy. L\'extension cURL de PHP est vivement recommandée.<br /><br />Vous devez télécharger manuellement le fichier <a href="{$a->url}">{$a->url}</a>, le copier sur votre serveur à l\'emplacement « {$a->dest} » et le décompresser à cet endroit';
+$string['remotedownloaderror'] = '<p>Le téléchargement du composant sur votre serveur a échoué. Veuillez vérifier les réglages de proxy. L\'extension cURL de PHP est vivement recommandée.</p>
+<p>Vous devez télécharger manuellement le fichier <a href="{$a->url}">{$a->url}</a>, le copier sur votre serveur à l\'emplacement « {$a->dest} » et le décompresser à cet endroit.</p>';
 $string['wrongdestpath'] = 'Chemin de destination incorrect';
 $string['wrongsourcebase'] = 'Adresse URL de base de la source incorrecte';
 $string['wrongzipfilename'] = 'Nom de fichier ZIP incorrect';
index 6e8a439..1d73f7e 100644 (file)
@@ -83,6 +83,7 @@ $string['errorminpasswordupper'] = 'Passwords must have at least {$a} upper case
 $string['errorpasswordupdate'] = 'Error updating password, password not changed';
 $string['event_user_loggedin'] = 'User has logged in';
 $string['eventuserloggedinas'] = 'User logged in as another user';
+$string['eventuserloginfailed'] = 'User login failed';
 $string['forcechangepassword'] = 'Force change password';
 $string['forcechangepasswordfirst_help'] = 'Force users to change password on their first login to Moodle.';
 $string['forcechangepassword_help'] = 'Force users to change password on their next login to Moodle.';
index ca9a20f..0bd37d5 100644 (file)
@@ -35,4 +35,5 @@ $string['checkingsourcetables'] = 'Checking source table structure';
 $string['importschemaexception'] = 'Current database structure does not match all install.xml files. <br /> {$a}';
 $string['importversionmismatchexception'] = 'Current version {$a->currentver} does not match exported version {$a->schemaver}.';
 $string['malformedxmlexception'] = 'Malformed XML found, can not continue.';
+$string['tablex'] = 'Table {$a}:';
 $string['unknowntableexception'] = 'Unknown table {$a} found in export file.';
index 76a4d25..a7060bd 100644 (file)
@@ -114,8 +114,7 @@ class behat_command {
     /**
      * Checks if behat is set up and working
      *
-     * Uses notice() instead of behat_error() because is
-     * also called from web interface
+     * Notifies failures both from CLI and web interface.
      *
      * It checks behat dependencies have been installed and runs
      * the behat help command to ensure it works as expected
@@ -125,24 +124,11 @@ class behat_command {
     public static function behat_setup_problem() {
         global $CFG;
 
-        $clibehaterrorstr = "Behat dependencies not installed. Ensure you ran the composer installer. " . self::DOCS_URL . "#Installation\n";
-
         // Moodle setting.
         if (!self::are_behat_dependencies_installed()) {
 
-
-            // With HTML.
-            if (!CLI_SCRIPT) {
-
-                $msg = get_string('wrongbehatsetup', 'tool_behat');
-                $docslink = self::DOCS_URL . '#Installation';
-                $docslink = html_writer::tag('a', $docslink, array('href' => $docslink, 'target' => '_blank'));
-                $msg .= get_string('moreinfoin', 'tool_behat', $docslink);
-            } else {
-                $msg = $clibehaterrorstr;
-            }
-
-            self::output_msg($msg);
+            // Returning composer error code to avoid conflicts with behat and moodle error codes.
+            self::output_msg(get_string('errorcomposer', 'tool_behat'));
             return BEHAT_EXITCODE_COMPOSER;
         }
 
@@ -150,22 +136,40 @@ class behat_command {
         list($output, $code) = self::run(' --help');
 
         if ($code != 0) {
+
             // Returning composer error code to avoid conflicts with behat and moodle error codes.
-            if (!CLI_SCRIPT) {
-                $msg = get_string('wrongbehatsetup', 'tool_behat');
-            } else {
-                $msg = $clibehaterrorstr;
-            }
-            self::output_msg($msg);
+            self::output_msg(get_string('errorbehatcommand', 'tool_behat', self::get_behat_command()));
             return BEHAT_EXITCODE_COMPOSER;
         }
 
+        // No empty values.
+        if (empty($CFG->behat_dataroot) || empty($CFG->behat_prefix) || empty($CFG->behat_wwwroot)) {
+            self::output_msg(get_string('errorsetconfig', 'tool_behat'));
+            return BEHAT_EXITCODE_CONFIG;
+
+        }
+
+        // Not repeated values.
+        // We only need to check this when the behat site is not running as
+        // at this point, when it is running, all $CFG->behat_* vars have
+        // already been copied to $CFG->dataroot, $CFG->prefix and $CFG->wwwroot.
+        if (!defined('BEHAT_SITE_RUNNING') &&
+                ($CFG->behat_prefix == $CFG->prefix ||
+                $CFG->behat_dataroot == $CFG->dataroot ||
+                $CFG->behat_wwwroot == $CFG->wwwroot ||
+                (!empty($CFG->phpunit_prefix) && $CFG->phpunit_prefix == $CFG->behat_prefix) ||
+                (!empty($CFG->phpunit_dataroot) && $CFG->phpunit_dataroot == $CFG->behat_dataroot)
+                )) {
+            self::output_msg(get_string('erroruniqueconfig', 'tool_behat'));
+            return BEHAT_EXITCODE_CONFIG;
+        }
+
         // Checking behat dataroot existence otherwise echo about admin/tool/behat/cli/init.php.
         if (!empty($CFG->behat_dataroot)) {
             $CFG->behat_dataroot = realpath($CFG->behat_dataroot);
         }
         if (empty($CFG->behat_dataroot) || !is_dir($CFG->behat_dataroot) || !is_writable($CFG->behat_dataroot)) {
-            self::output_msg(get_string('runclitool', 'tool_behat', 'php admin/tool/behat/cli/init.php'));
+            self::output_msg(get_string('errordataroot', 'tool_behat'));
             return BEHAT_EXITCODE_CONFIG;
         }
 
@@ -193,13 +197,25 @@ class behat_command {
      * @return void
      */
     protected static function output_msg($msg) {
+        global $CFG, $PAGE;
 
+        // If we are using the web interface we want pretty messages.
         if (!CLI_SCRIPT) {
-            // General info about the tool purpose.
-            $msg = get_string('aim', 'tool_behat') . '<br /><br />' . $msg;
-            notice($msg);
+
+            $renderer = $PAGE->get_renderer('tool_behat');
+            echo $renderer->render_error($msg);
+
+            // Stopping execution.
+            exit(1);
+
         } else {
-            echo $msg;
+
+            // We continue execution after this.
+            $clibehaterrorstr = "Ensure you set \$CFG->behat_* vars in config.php " .
+                "and you ran admin/tool/behat/cli/init.php.\n" .
+                "More info in " . self::DOCS_URL . "#Installation\n\n";
+
+            echo 'Error: ' . $msg . "\n\n" . $clibehaterrorstr;
         }
     }
 
index 5773d12..4f100ac 100644 (file)
@@ -58,6 +58,7 @@ class behat_form_editor extends behat_form_field {
                 if ($editorid = $this->get_editor_id()) {
 
                     // Set the value to the iframe and save it to the textarea.
+                    $value = str_replace('"', '\"', $value);
                     $this->session->executeScript('
                         tinyMCE.get("'.$editorid.'").setContent("' . $value . '");
                         tinyMCE.get("'.$editorid.'").save();
index 8db4579..284befd 100644 (file)
@@ -114,10 +114,22 @@ class behat_form_field {
         // Textareas are considered text based elements.
         $tagname = strtolower($this->field->getTagName());
         if ($tagname == 'textarea') {
-            return false;
-        }
 
-        if ($tagname == 'input') {
+            if (!$this->running_javascript()) {
+                return false;
+            }
+
+            // If there is an iframe with $id + _ifr there a TinyMCE editor loaded.
+            $xpath = '//iframe[@id="' . $this->field->getAttribute('id') . '_ifr"]';
+            if (!$this->session->getPage()->find('xpath', $xpath)) {
+
+                // Generic one if it is a normal textarea.
+                return false;
+            }
+
+            $classname = 'behat_form_editor';
+
+        } else if ($tagname == 'input') {
             $type = $this->field->getAttribute('type');
             switch ($type) {
                 case 'text':
index dce4028..208b95a 100644 (file)
@@ -94,8 +94,23 @@ class behat_form_select extends behat_form_field {
             return;
         }
 
+        // Wrapped in try & catch as the element may disappear if an AJAX request was submitted.
+        try {
+            $multiple = $this->field->hasAttribute('multiple');
+        } catch (Exception $e) {
+            // We do not specify any specific Exception type as there are
+            // different exceptions that can be thrown by the driver and
+            // we can not control them all, also depending on the selenium
+            // version the exception type can change.
+            return;
+        }
+
+        // Wait for all the possible AJAX requests that have been
+        // already triggered by selectOption() to be finished.
+        $this->session->wait(behat_base::TIMEOUT * 1000, behat_base::PAGE_READY_JS);
+
         // Single select sometimes needs an extra click in the option.
-        if (!$this->field->hasAttribute('multiple')) {
+        if (!$multiple) {
 
             // Using the driver direcly because Element methods are messy when dealing
             // with elements inside containers.
@@ -104,11 +119,6 @@ class behat_form_select extends behat_form_field {
                 // Wrapped in a try & catch as we can fall into race conditions
                 // and the element may not be there.
                 try {
-
-                    // Wait for all the possible AJAX requests that have been
-                    // already triggered by selectOption() to be finished.
-                    $this->session->wait(behat_base::TIMEOUT * 1000, behat_base::PAGE_READY_JS);
-
                     current($optionnodes)->click();
                 } catch (Exception $e) {
                     // We continue and return as this means that the element is not there or it is not the same.
@@ -118,10 +128,6 @@ class behat_form_select extends behat_form_field {
 
         } else {
 
-            // Wait for all the possible AJAX requests that have been
-            // already triggered by selectOption() to be finished.
-            $this->session->wait(behat_base::TIMEOUT * 1000, behat_base::PAGE_READY_JS);
-
             // Wrapped in a try & catch as we can fall into race conditions
             // and the element may not be there.
             try {
index 8f49ae1..66dc0b9 100644 (file)
@@ -59,7 +59,7 @@ abstract class assessable_submitted extends \core\event\base {
      * @return void
      */
     protected function validate_data() {
-        if (!$this->context->contextlevel === CONTEXT_MODULE) {
+        if (!$this->contextlevel === CONTEXT_MODULE) {
             throw new \coding_exception('Content level must be CONTEXT_MODULE.');
         }
     }
index efdc7ec..83a3361 100644 (file)
@@ -66,7 +66,7 @@ abstract class assessable_uploaded extends \core\event\base {
      * @return void
      */
     protected function validate_data() {
-        if (!$this->context->contextlevel === CONTEXT_MODULE) {
+        if (!$this->contextlevel === CONTEXT_MODULE) {
             throw new \coding_exception('Content level must be CONTEXT_MODULE.');
         } else if (!isset($this->other['pathnamehashes']) || !is_array($this->other['pathnamehashes'])) {
             throw new \coding_exception('pathnamehashes must be set in $other and must be an array.');
index d1cbe32..38a999e 100644 (file)
@@ -355,7 +355,7 @@ abstract class base implements \IteratorAggregate {
         if (isset($this->context)) {
             return $this->context;
         }
-        $this->context = \context::instance_by_id($this->data['contextid'], false);
+        $this->context = \context::instance_by_id($this->data['contextid'], IGNORE_MISSING);
         return $this->context;
     }
 
index f09d4d7..0fa689b 100644 (file)
@@ -69,8 +69,8 @@ class blog_association_created extends \core\event\base {
      * @return string
      */
     public function get_description() {
-        return "Blog association added between entry id $this->other['blogid'] and $this->other['associatetype'] with id
-                $this->other['associateid']";
+        return "Blog association added between entry id {$this->other['blogid']} and {$this->other['associatetype']} with id
+                {$this->other['associateid']}";
     }
 
     /**
index 09935b8..420c4b8 100644 (file)
@@ -79,7 +79,12 @@ abstract class comment_created extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return $this->context->get_url();
+        $context = $this->get_context();
+        if ($context) {
+            return $context->get_url();
+        } else {
+            return null;
+        }
     }
 
     /**
index 2c20348..57084fb 100644 (file)
@@ -79,7 +79,12 @@ abstract class comment_deleted extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return $this->context->get_url();
+        $context = $this->get_context();
+        if ($context) {
+            return $context->get_url();
+        } else {
+            return null;
+        }
     }
 
     /**
index a048d01..fbea09d 100644 (file)
@@ -72,6 +72,11 @@ abstract class comments_viewed extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return $this->context->get_url();
+        $context = $this->get_context();
+        if ($context) {
+            return $context->get_url();
+        } else {
+            return null;
+        }
     }
 }
index cf94035..f83b5d7 100644 (file)
@@ -105,7 +105,7 @@ abstract class course_module_instance_list_viewed extends base{
      * @return void
      */
     protected function validate_data() {
-        if ($this->context->contextlevel !== CONTEXT_COURSE) {
+        if ($this->contextlevel !== CONTEXT_COURSE) {
             throw new \coding_exception('The context must be a course level context.');
         }
     }
index 43e1504..b455b34 100644 (file)
@@ -72,7 +72,7 @@ abstract class course_module_viewed extends base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url("/mod/$this->objecttable/view.php", array('id' => $this->context->instanceid));
+        return new \moodle_url("/mod/$this->objecttable/view.php", array('id' => $this->contextinstanceid));
     }
 
     /**
@@ -81,8 +81,8 @@ abstract class course_module_viewed extends base {
      * @return array|null
      */
     protected function get_legacy_logdata() {
-        return array($this->courseid, $this->objecttable, 'view', 'view.php?id=' . $this->context->instanceid, $this->objectid,
-                     $this->context->instanceid);
+        return array($this->courseid, $this->objecttable, 'view', 'view.php?id=' . $this->contextinstanceid, $this->objectid,
+                     $this->contextinstanceid);
     }
 
     /**
index 23539ca..825eb53 100644 (file)
@@ -97,6 +97,15 @@ class user_enrolment_created extends base {
         return $legacyeventdata;
     }
 
+    /**
+     * Return legacy data for add_to_log().
+     *
+     * @return array
+     */
+    protected function get_legacy_logdata() {
+        return array($this->courseid, 'course', 'enrol', '../enrol/users.php?id=' . $this->courseid, $this->courseid);
+    }
+
     /**
      * Custom validation.
      *
index e178aab..4c7a905 100644 (file)
@@ -95,6 +95,14 @@ class user_enrolment_deleted extends base {
         return (object)$this->other['userenrolment'];
     }
 
+    /**
+     * Return legacy data for add_to_log().
+     *
+     * @return array
+     */
+    protected function get_legacy_logdata() {
+        return array($this->courseid, 'course', 'unenrol', '../enrol/users.php?id=' . $this->courseid, $this->courseid);
+    }
 
     /**
      * Custom validation.
diff --git a/lib/classes/event/user_login_failed.php b/lib/classes/event/user_login_failed.php
new file mode 100644 (file)
index 0000000..6533a10
--- /dev/null
@@ -0,0 +1,109 @@
+<?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/>.
+
+/**
+ * User login failed event.
+ *
+ * @package    core
+ * @copyright  2014 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * User login failed event class.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      @type string username name of user.
+ *      @type int reason failure reason.
+ * }
+ *
+ * @package    core
+ * @copyright  2014 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class user_login_failed extends \core\event\base {
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->context = \context_system::instance();
+        $this->data['crud'] = 'r';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventuserloginfailed', 'auth');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return 'Login failed for user "' . $this->other['username'] . '" for reason id: ' . $this->other['reason'];
+    }
+
+    /**
+     * Get URL related to the action.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        if (isset($this->data['userid'])) {
+            return new \moodle_url('/user/profile.php', array('id' => $this->data['userid']));
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Return legacy data for add_to_log().
+     *
+     * @return array
+     */
+    protected function get_legacy_logdata() {
+        return array(SITEID, 'login', 'error', 'index.php', $this->other['username']);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception when validation does not pass.
+     * @return void
+     */
+    protected function validate_data() {
+        if (!isset($this->other['reason'])) {
+            throw new \coding_exception("other['reason'] has to be specified.");
+        } else if (!isset($this->other['username'])) {
+            throw new \coding_exception("other['username'] has to be specified.");
+        }
+    }
+
+}
diff --git a/lib/classes/lock/db_record_lock_factory.php b/lib/classes/lock/db_record_lock_factory.php
new file mode 100644 (file)
index 0000000..70cbdb2
--- /dev/null
@@ -0,0 +1,254 @@
+<?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/>.
+
+/**
+ * This is a db record locking factory.
+ *
+ * @package    core
+ * @category   lock
+ * @copyright  Damyon Wiese 2013
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\lock;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * This is a db record locking factory.
+ *
+ * This lock factory uses record locks relying on sql of the form "SET XXX where YYY" and checking if the
+ * value was set. It supports timeouts, autorelease and can work on any DB. The downside - is this
+ * will always be slower than some shared memory type locking function.
+ *
+ * @package   core
+ * @category  lock
+ * @copyright Damyon Wiese 2013
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class db_record_lock_factory implements lock_factory {
+
+    /** @var \moodle_database $db Hold a reference to the global $DB */
+    protected $db;
+
+    /** @var string $type Used to prefix lock keys */
+    protected $type;
+
+    /** @var array $openlocks - List of held locks - used by auto-release */
+    protected $openlocks = array();
+
+    /**
+     * Is available.
+     * @return boolean - True if this lock type is available in this environment.
+     */
+    public function is_available() {
+        return true;
+    }
+
+    /**
+     * Almighty constructor.
+     * @param string $type - Used to prefix lock keys.
+     */
+    public function __construct($type) {
+        global $DB;
+
+        $this->type = $type;
+        // Save a reference to the global $DB so it will not be released while we still have open locks.
+        $this->db = $DB;
+
+        \core_shutdown_manager::register_function(array($this, 'auto_release'));
+    }
+
+    /**
+     * Return information about the blocking behaviour of the lock type on this platform.
+     * @return boolean - True
+     */
+    public function supports_timeout() {
+        return true;
+    }
+
+    /**
+     * Will this lock type will be automatically released when a process ends.
+     *
+     * @return boolean - True (shutdown handler)
+     */
+    public function supports_auto_release() {
+        return true;
+    }
+
+    /**
+     * Multiple locks for the same resource can be held by a single process.
+     * @return boolean - False - not process specific.
+     */
+    public function supports_recursion() {
+        return false;
+    }
+
+    /**
+     * This function generates a unique token for the lock to use.
+     * It is important that this token is not solely based on time as this could lead
+     * to duplicates in a clustered environment (especially on VMs due to poor time precision).
+     */
+    protected function generate_unique_token() {
+        $uuid = '';
+
+        if (function_exists("uuid_create")) {
+            $context = null;
+            uuid_create($context);
+
+            uuid_make($context, UUID_MAKE_V4);
+            uuid_export($context, UUID_FMT_STR, $uuid);
+        } else {
+            // Fallback uuid generation based on:
+            // "http://www.php.net/manual/en/function.uniqid.php#94959".
+            $uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
+
+                // 32 bits for "time_low".
+                mt_rand(0, 0xffff), mt_rand(0, 0xffff),
+
+                // 16 bits for "time_mid".
+                mt_rand(0, 0xffff),
+
+                // 16 bits for "time_hi_and_version",
+                // four most significant bits holds version number 4.
+                mt_rand(0, 0x0fff) | 0x4000,
+
+                // 16 bits, 8 bits for "clk_seq_hi_res",
+                // 8 bits for "clk_seq_low",
+                // two most significant bits holds zero and one for variant DCE1.1.
+                mt_rand(0, 0x3fff) | 0x8000,
+
+                // 48 bits for "node".
+                mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff));
+        }
+        return trim($uuid);
+    }
+
+
+    /**
+     * Create and get a lock
+     * @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
+     * @param int $timeout - The number of seconds to wait for a lock before giving up.
+     * @param int $maxlifetime - Unused by this lock type.
+     * @return boolean - true if a lock was obtained.
+     */
+    public function get_lock($resource, $timeout, $maxlifetime = 86400) {
+
+        $token = $this->generate_unique_token();
+        $now = time();
+        $giveuptime = $now + $timeout;
+        $expires = $now + $maxlifetime;
+
+        if (!$this->db->record_exists('lock_db', array('resourcekey' => $resource))) {
+            $record = new \stdClass();
+            $record->resourcekey = $resource;
+            $result = $this->db->insert_record('lock_db', $record);
+        }
+
+        $params = array('expires' => $expires,
+                        'token' => $token,
+                        'resourcekey' => $resource,
+                        'now' => $now);
+        $sql = 'UPDATE {lock_db}
+                   SET
+                       expires = :expires,
+                       owner = :token
+                 WHERE
+                       resourcekey = :resourcekey AND
+                       (owner IS NULL OR expires < :now)';
+
+        do {
+            $now = time();
+            $params['now'] = $now;
+            $this->db->execute($sql, $params);
+
+            $countparams = array('owner' => $token, 'resourcekey' => $resource);
+            $result = $this->db->count_records('lock_db', $countparams);
+            $locked = $result === 1;
+            if (!$locked) {
+                usleep(rand(10000, 250000)); // Sleep between 10 and 250 milliseconds.
+            }
+            // Try until the giveup time.
+        } while (!$locked && $now < $giveuptime);
+
+        if ($locked) {
+            $this->openlocks[$token] = 1;
+            return new lock($token, $this);
+        }
+
+        return false;
+    }
+
+    /**
+     * Release a lock that was previously obtained with @lock.
+     * @param lock $lock - a lock obtained from this factory.
+     * @return boolean - true if the lock is no longer held (including if it was never held).
+     */
+    public function release_lock(lock $lock) {
+        $params = array('noexpires' => null,
+                        'token' => $lock->get_key(),
+                        'noowner' => null);
+
+        $sql = 'UPDATE {lock_db}
+                    SET
+                        expires = :noexpires,
+                        owner = :noowner
+                    WHERE
+                        owner = :token';
+        $result = $this->db->execute($sql, $params);
+        if ($result) {
+            unset($this->openlocks[$lock->get_key()]);
+        }
+        return $result;
+    }
+
+    /**
+     * Extend a lock that was previously obtained with @lock.
+     * @param lock $lock - a lock obtained from this factory.
+     * @param int $maxlifetime - the new lifetime for the lock (in seconds).
+     * @return boolean - true if the lock was extended.
+     */
+    public function extend_lock(lock $lock, $maxlifetime = 86400) {
+        $now = time();
+        $expires = $now + $maxlifetime;
+        $params = array('expires' => $expires,
+                        'token' => $lock->get_key());
+
+        $sql = 'UPDATE {lock_db}
+                    SET
+                        expires = :expires,
+                    WHERE
+                        owner = :token';
+
+        $this->db->execute($sql, $params);
+        $countparams = array('owner' => $lock->get_key());
+        $result = $this->count_records('lock_db', $countparams);
+
+        return $result === 0;
+    }
+
+    /**
+     * Auto release any open locks on shutdown.
+     * This is required, because we may be using persistent DB connections.
+     */
+    public function auto_release() {
+        // Called from the shutdown handler. Must release all open locks.
+        foreach ($this->openlocks as $key => $unused) {
+            $lock = new lock($key, $this);
+            $this->release_lock($lock);
+        }
+    }
+}
diff --git a/lib/classes/lock/file_lock_factory.php b/lib/classes/lock/file_lock_factory.php
new file mode 100644 (file)
index 0000000..5062de8
--- /dev/null
@@ -0,0 +1,200 @@
+<?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/>.
+
+/**
+ * Flock based file locking factory.
+ *
+ * The file lock factory returns file locks locked with the flock function. Works OK, except on some
+ * NFS, exotic shared storage and exotic server OSes (like windows). On windows, a second attempt to get a
+ * lock will block indefinitely instead of timing out.
+ *
+ * @package    core
+ * @category   lock
+ * @copyright  Damyon Wiese 2013
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\lock;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Flock based file locking factory.
+ *
+ * The file lock factory returns file locks locked with the flock function. Works OK, except on some
+ * NFS, exotic shared storage and exotic server OSes (like windows). On windows, a second attempt to get a
+ * lock will block indefinitely instead of timing out.
+ *
+ * @package   core
+ * @category  lock
+ * @copyright Damyon Wiese 2013
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class file_lock_factory implements lock_factory {
+
+    /** @var string $type - The type of lock, e.g. cache, cron, session. */
+    protected $type;
+
+    /** @var string $lockdirectory - Full system path to the directory used to store file locks. */
+    protected $lockdirectory;
+
+    /** @var boolean $verbose - If true, debugging info about the owner of the lock will be written to the lock file. */
+    protected $verbose;
+
+    /**
+     * Create this lock factory.
+     *
+     * @param string $type - The type, e.g. cron, cache, session
+     */
+    public function __construct($type) {
+        global $CFG;
+
+        $this->type = $type;
+        if (!isset($CFG->file_lock_root)) {
+            $this->lockdirectory = $CFG->dataroot . '/lock';
+        } else {
+            $this->lockdirectory = $CFG->file_lock_root;
+        }
+        $this->verbose = false;
+        if ($CFG->debugdeveloper) {
+            $this->verbose = true;
+        }
+    }
+
+    /**
+     * Return information about the blocking behaviour of the lock type on this platform.
+     * @return boolean - False if attempting to get a lock will block indefinitely.
+     */
+    public function supports_timeout() {
+        global $CFG;
+
+        return $CFG->ostype !== 'WINDOWS';
+    }
+
+    /**
+     * This lock type will be automatically released when a process ends.
+     * @return boolean - True
+     */
+    public function supports_auto_release() {
+        return true;
+    }
+
+    /**
+     * Is available.
+     * @return boolean - True if preventfilelocking is not set - or the file_lock_root is not in dataroot.
+     */
+    public function is_available() {
+        global $CFG;
+        $preventfilelocking = !empty($CFG->preventfilelocking);
+        $lockdirisdataroot = true;
+        if (!empty($CFG->file_lock_root) && strpos($CFG->file_lock_root, $CFG->dataroot) !== 0) {
+            $lockdirisdataroot = false;
+        }
+        return !$preventfilelocking || !$lockdirisdataroot;
+    }
+
+    /**
+     * Multiple locks for the same resource cannot be held from a single process.
+     * @return boolean - False
+     */
+    public function supports_recursion() {
+        return false;
+    }
+
+    /**
+     * Get some info that might be useful for debugging.
+     * @return boolean - string
+     */
+    protected function get_debug_info() {
+        return 'host:' . php_uname('n') . ', pid:' . getmypid() . ', time:' . time();
+    }
+
+    /**
+     * Get a lock within the specified timeout or return false.
+     * @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
+     * @param int $timeout - The number of seconds to wait for a lock before giving up.
+     * @param int $maxlifetime - Unused by this lock type.
+     * @return boolean - true if a lock was obtained.
+     */
+    public function get_lock($resource, $timeout, $maxlifetime = 86400) {
+        $giveuptime = time() + $timeout;
+
+        $hash = md5($this->type . '_' . $resource);
+        $lockdir = $this->lockdirectory . '/' . substr($hash, 0, 2);
+
+        if (!check_dir_exists($lockdir, true, true)) {
+            return false;
+        }
+
+        $lockfilename = $lockdir . '/' . $hash;
+
+        $filehandle = fopen($lockfilename, "wb");
+
+        // Could not open the lock file.
+        if (!$filehandle) {
+            return false;
+        }
+
+        do {
+            // Will block on windows. So sad.
+            $wouldblock = false;
+            $locked = flock($filehandle, LOCK_EX | LOCK_NB, $wouldblock);
+            if (!$locked && $wouldblock) {
+                usleep(rand(10000, 250000)); // Sleep between 10 and 250 milliseconds.
+            }
+            // Try until the giveup time.
+        } while (!$locked && $wouldblock && time() < $giveuptime);
+
+        if (!$locked) {
+            fclose($filehandle);
+            return false;
+        }
+        if ($this->verbose) {
+            fwrite($filehandle, $this->get_debug_info());
+        }
+        return new lock($filehandle, $this);
+    }
+
+    /**
+     * Release a lock that was previously obtained with @lock.
+     * @param lock $lock - A lock obtained from this factory.
+     * @return boolean - true if the lock is no longer held (including if it was never held).
+     */
+    public function release_lock(lock $lock) {
+        $handle = $lock->get_key();
+
+        if (!$handle) {
+            // We didn't have a lock.
+            return false;
+        }
+
+        $result = flock($handle, LOCK_UN);
+        fclose($handle);
+        return $result;
+    }
+
+    /**
+     * Extend a lock that was previously obtained with @lock.
+     * @param lock $lock - not used
+     * @param int $maxlifetime - not used
+     * @return boolean - true if the lock was extended.
+     */
+    public function extend_lock(lock $lock, $maxlifetime = 86400) {
+        // Not supported by this factory.
+        return false;
+    }
+
+}
diff --git a/lib/classes/lock/lock.php b/lib/classes/lock/lock.php
new file mode 100644 (file)
index 0000000..9bdb040
--- /dev/null
@@ -0,0 +1,113 @@
+<?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/>.
+
+/**
+ * Class representing a lock
+ *
+ * The methods available for a specific lock type are only known by it's factory.
+ *
+ * @package    core
+ * @copyright  Damyon Wiese 2013
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\lock;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class representing a lock
+ *
+ * The methods available for a specific lock type are only known by it's factory.
+ *
+ * @package   core
+ * @category  lock
+ * @copyright Damyon Wiese 2013
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class lock {
+
+    /** @var string|int $key A unique key representing a held lock */
+    protected $key = '';
+
+    /** @var lock_factory $factory The factory that generated this lock */
+    protected $factory;
+
+    /** @var bool $released Has this lock been released? If a lock falls out of scope without being released - show a warning. */
+    protected $released;
+
+    /**
+     * Construct a lock containing the unique key required to release it.
+     * @param mixed $key - The lock key. The type of this is up to the lock_factory being used.
+     *      For file locks this is a file handle. For MySQL this is a string.
+     * @param lock_factory $factory - The factory that generated this lock.
+     */
+    public function __construct($key, $factory) {
+        $this->factory = $factory;
+        $this->key = $key;
+        $this->released = false;
+    }
+
+    /**
+     * Return the unique key representing this lock.
+     * @return string|int lock key.
+     */
+    public function get_key() {
+        return $this->key;
+    }
+
+    /**
+     * Extend the lifetime of this lock. Not supported by all factories.
+     * @param int $maxlifetime - the new lifetime for the lock (in seconds).
+     * @return bool
+     */
+    public function extend($maxlifetime = 86400) {
+        if ($this->factory) {
+            return $this->factory->extend_lock($this, $maxlifetime);
+        }
+        return false;
+    }
+
+    /**
+     * Release this lock
+     * @return bool
+     */
+    public function release() {
+        $this->released = true;
+        if (empty($this->factory)) {
+            return false;
+        }
+        $result = $this->factory->release_lock($this);
+        // Release any held references to the factory.
+        unset($this->factory);
+        $this->factory = null;
+        $this->key = '';
+        return $result;
+    }
+
+    /**
+     * Print debugging if this lock falls out of scope before being released.
+     */
+    public function __destruct() {
+        if (!$this->released && defined('PHPUNIT_TEST')) {
+            $this->release();
+            throw new \coding_exception('\core\lock\lock(' . $this->key . ') has fallen out of scope ' .
+                                        'without being released.' . "\n" .
+                                        'Locks must ALWAYS be released by calling $mylock->release().');
+        }
+    }
+
+}
diff --git a/lib/classes/lock/lock_config.php b/lib/classes/lock/lock_config.php
new file mode 100644 (file)
index 0000000..43541a6
--- /dev/null
@@ -0,0 +1,78 @@
+<?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/>.
+
+/**
+ * Lock configuration class, used to get an instance of the currently configured lock factory.
+ *
+ * @package    core
+ * @category   lock
+ * @copyright  Damyon Wiese 2013
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\lock;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Lock configuration class, used to get an instance of the currently configured lock factory.
+ *
+ * @package   core
+ * @category  lock
+ * @copyright Damyon Wiese 2013
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class lock_config {
+
+    /**
+     * Get an instance of the currently configured locking subclass.
+     *
+     * @param string $type - Unique namespace for the locks generated by this factory. e.g. core_cron
+     * @return \core\lock\lock_factory
+     * @throws \coding_exception
+     */
+    public static function get_lock_factory($type) {
+        global $CFG, $DB;
+        $lockfactory = null;
+
+        if (isset($CFG->lock_factory) && $CFG->lock_factory != 'auto') {
+            if (!class_exists($CFG->lock_factory)) {
+                // In this case I guess it is not safe to continue. Different cluster nodes could end up using different locking
+                // types because of an installation error.
+                throw new \coding_exception('Lock factory set in $CFG does not exist: ' . $CFG->lock_factory);
+            }
+            $lockfactoryclass = $CFG->lock_factory;
+            $lockfactory = new $lockfactoryclass($type);
+        } else {
+            $dbtype = clean_param($DB->get_dbfamily(), PARAM_ALPHA);
+
+            // DB Specific lock factory is preferred - should support auto-release.
+            $lockfactoryclass = "\\core\\lock\\${dbtype}_lock_factory";
+            if (!class_exists($lockfactoryclass)) {
+                $lockfactoryclass = '\core\lock\file_lock_factory';
+            }
+            /* @var lock_factory $lockfactory */
+            $lockfactory = new $lockfactoryclass($type);
+            if (!$lockfactory->is_available()) {
+                // Final fallback - DB row locking.
+                $lockfactory = new \core\lock\db_record_lock_factory($type);
+            }
+        }
+
+        return $lockfactory;
+    }
+
+}
diff --git a/lib/classes/lock/lock_factory.php b/lib/classes/lock/lock_factory.php
new file mode 100644 (file)
index 0000000..b03e774
--- /dev/null
@@ -0,0 +1,106 @@
+<?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/>.
+
+/**
+ * Defines abstract factory class for generating locks.
+ *
+ * @package    core
+ * @copyright  Damyon Wiese 2013
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\lock;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Defines abstract factory class for generating locks.
+ *
+ * @package   core
+ * @category  lock
+ * @copyright Damyon Wiese 2013
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+interface lock_factory {
+
+    /**
+     * Define the constructor signature required by the lock_config class.
+     *
+     * @param string $type - The type this lock is used for (e.g. cron, cache)
+     */
+    public function __construct($type);
+
+    /**
+     * Return information about the blocking behaviour of the locks on this platform.
+     *
+     * @return boolean - False if attempting to get a lock will block indefinitely.
+     */
+    public function supports_timeout();
+
+    /**
+     * Will this lock be automatically released when the process ends.
+     * This should never be relied upon in code - but is useful in the case of
+     * fatal errors. If a lock type does not support this auto release,
+     * the max lock time parameter must be obeyed to eventually clean up a lock.
+     *
+     * @return boolean - True if this lock type will be automatically released when the current process ends.
+     */
+    public function supports_auto_release();
+
+    /**
+     * Supports recursion.
+     *
+     * @return boolean - True if attempting to get 2 locks on the same resource will "stack"
+     */
+    public function supports_recursion();
+
+    /**
+     * Is available.
+     *
+     * @return boolean - True if this lock type is available in this environment.
+     */
+    public function is_available();
+
+    /**
+     * Get a lock within the specified timeout or return false.
+     *
+     * @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
+     * @param int $timeout - The number of seconds to wait for a lock before giving up.
+     *                       Not all lock types will support this.
+     * @param int $maxlifetime - The number of seconds to wait before reclaiming a stale lock.
+     *                       Not all lock types will use this - e.g. if they support auto releasing
+     *                       a lock when a process ends.
+     * @return \core\lock\lock|boolean - An instance of \core\lock\lock if the lock was obtained, or false.
+     */
+    public function get_lock($resource, $timeout, $maxlifetime = 86400);
+
+    /**
+     * Release a lock that was previously obtained with @lock.
+     *
+     * @param lock $lock - The lock to release.
+     * @return boolean - True if the lock is no longer held (including if it was never held).
+     */
+    public function release_lock(lock $lock);
+
+    /**
+     * Extend the timeout on a held lock.
+     *
+     * @param lock $lock - lock obtained from this factory
+     * @param int $maxlifetime - new max time to hold the lock
+     * @return boolean - True if the lock was extended.
+     */
+    public function extend_lock(lock $lock, $maxlifetime = 86400);
+}
diff --git a/lib/classes/lock/postgres_lock_factory.php b/lib/classes/lock/postgres_lock_factory.php
new file mode 100644 (file)
index 0000000..f27d532
--- /dev/null
@@ -0,0 +1,243 @@
+<?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/>.
+
+/**
+ * Postgres advisory locking factory.
+ *
+ * @package    core
+ * @category   lock
+ * @copyright  Damyon Wiese 2013
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\lock;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Postgres advisory locking factory.
+ *
+ * Postgres locking implementation using advisory locks. Some important points. Postgres has
+ * 2 different forms of lock functions, some accepting a single int, and some accepting 2 ints. This implementation
+ * uses the 2 int version so that it uses a separate namespace from the session locking. The second note,
+ * is because postgres uses integer keys for locks, we first need to map strings to a unique integer. This is
+ * done by storing the strings in the lock_db table and using the auto-id returned. There is a static cache for
+ * id's in this function.
+ *
+ * @package   core
+ * @category  lock
+ * @copyright Damyon Wiese 2013
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class postgres_lock_factory implements lock_factory {
+
+    /** @var int $dblockid - used as a namespace for these types of locks (separate from session locks) */
+    protected $dblockid = -1;
+
+    /** @var array $lockidcache - static cache for string -> int conversions required for pg advisory locks. */
+    protected static $lockidcache = array();
+
+    /** @var \moodle_database $db Hold a reference to the global $DB */
+    protected $db;
+
+    /** @var string $type Used to prefix lock keys */
+    protected $type;
+
+    /** @var array $openlocks - List of held locks - used by auto-release */
+    protected $openlocks = array();
+
+    /**
+     * Calculate a unique instance id based on the database name and prefix.
+     * @return int.
+     */
+    protected function get_unique_db_instance_id() {
+        global $CFG;
+
+        $strkey = $CFG->dbname . ':' . $CFG->prefix;
+        $intkey = crc32($strkey);
+        // Normalize between 64 bit unsigned int and 32 bit signed ints. Php could return either from crc32.
+        if (PHP_INT_SIZE == 8) {
+            if ($intkey > 0x7FFFFFFF) {
+                $intkey -= 0x100000000;
+            }
+        }
+
+        return $intkey;
+    }
+
+    /**
+     * Almighty constructor.
+     * @param string $type - Used to prefix lock keys.
+     */
+    public function __construct($type) {
+        global $DB;
+
+        $this->type = $type;
+        $this->dblockid = $this->get_unique_db_instance_id();
+        // Save a reference to the global $DB so it will not be released while we still have open locks.
+        $this->db = $DB;
+
+        \core_shutdown_manager::register_function(array($this, 'auto_release'));
+    }
+
+    /**
+     * Is available.
+     * @return boolean - True if this lock type is available in this environment.
+     */
+    public function is_available() {
+        return $this->db->get_dbfamily() === 'postgres';
+    }
+
+    /**
+     * Return information about the blocking behaviour of the lock type on this platform.
+     * @return boolean - Defer to the DB driver.
+     */
+    public function supports_timeout() {
+        return true;
+    }
+
+    /**
+     * Will this lock type will be automatically released when a process ends.
+     *
+     * @return boolean - Via shutdown handler.
+     */
+    public function supports_auto_release() {
+        return true;
+    }
+
+    /**
+     * Multiple locks for the same resource can be held by a single process.
+     * @return boolean - Defer to the DB driver.
+     */
+    public function supports_recursion() {
+        return true;
+    }
+
+    /**
+     * This function generates the unique index for a specific lock key.
+     * Once an index is assigned to a key, it never changes - so this is
+     * statically cached.
+     *
+     * @param string $key
+     * @return int
+     * @throws \moodle_exception
+     */
+    protected function get_index_from_key($key) {
+        if (isset(self::$lockidcache[$key])) {
+            return self::$lockidcache[$key];
+        }
+
+        $index = 0;
+        $record = $this->db->get_record('lock_db', array('resourcekey' => $key));
+        if ($record) {
+            $index = $record->id;
+        }
+
+        if (!$index) {
+            $record = new \stdClass();
+            $record->resourcekey = $key;
+            try {
+                $index = $this->db->insert_record('lock_db', $record);
+            } catch (\dml_exception $de) {
+                // Race condition - never mind - now the value is guaranteed to exist.
+                $record = $this->db->get_record('lock_db', array('resourcekey' => $key));
+                if ($record) {
+                    $index = $record->id;
+                }
+            }
+        }
+
+        if (!$index) {
+            throw new \moodle_exception('Could not generate unique index for key');
+        }
+
+        self::$lockidcache[$key] = $index;
+        return $index;
+    }
+
+    /**
+     * Create and get a lock
+     * @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
+     * @param int $timeout - The number of seconds to wait for a lock before giving up.
+     * @param int $maxlifetime - Unused by this lock type.
+     * @return boolean - true if a lock was obtained.
+     */
+    public function get_lock($resource, $timeout, $maxlifetime = 86400) {
+        $giveuptime = time() + $timeout;
+
+        $token = $this->get_index_from_key($resource);
+
+        $params = array('locktype' => $this->dblockid,
+                        'token' => $token);
+
+        $locked = false;
+
+        do {
+            $result = $this->db->get_record_sql('SELECT pg_try_advisory_lock(:locktype, :token) AS locked', $params);
+            $locked = $result->locked === 't';
+            if (!$locked) {
+                usleep(rand(10000, 250000)); // Sleep between 10 and 250 milliseconds.
+            }
+            // Try until the giveup time.
+        } while (!$locked && time() < $giveuptime);
+
+        if ($locked) {
+            $this->openlocks[$token] = 1;
+            return new lock($token, $this);
+        }
+        return false;
+    }
+
+    /**
+     * Release a lock that was previously obtained with @lock.
+     * @param lock $lock - a lock obtained from this factory.
+     * @return boolean - true if the lock is no longer held (including if it was never held).
+     */
+    public function release_lock(lock $lock) {
+        $params = array('locktype' => $this->dblockid,
+                        'token' => $lock->get_key());
+        $result = $this->db->get_record_sql('SELECT pg_advisory_unlock(:locktype, :token) AS unlocked', $params);
+        $result = $result->unlocked === 't';
+        if ($result) {
+            unset($this->openlocks[$lock->get_key()]);
+        }
+        return $result;
+    }
+
+    /**
+     * Extend a lock that was previously obtained with @lock.
+     * @param lock $lock - a lock obtained from this factory.
+     * @param int $maxlifetime - the new lifetime for the lock (in seconds).
+     * @return boolean - true if the lock was extended.
+     */
+    public function extend_lock(lock $lock, $maxlifetime = 86400) {
+        // Not supported by this factory.
+        return false;
+    }
+
+    /**
+     * Auto release any open locks on shutdown.
+     * This is required, because we may be using persistent DB connections.
+     */
+    public function auto_release() {
+        // Called from the shutdown handler. Must release all open locks.
+        foreach ($this->openlocks as $key => $unused) {
+            $lock = new lock($key, $this);
+            $this->release_lock($lock);
+        }
+    }
+
+}
index 40bd6b3..7ae4380 100644 (file)
@@ -162,6 +162,7 @@ class core_plugin_manager {
 
         $this->installedplugins = array();
 
+        // TODO: Delete this block once Moodle 2.6 or later becomes minimum required version to upgrade.
         if ($CFG->version < 2013092001.02) {
             // We did not upgrade the database yet.
             $modules = $DB->get_records('modules', array(), 'name ASC', 'id, name, version');
@@ -661,6 +662,10 @@ class core_plugin_manager {
             return 'svn';
         }
 
+        if (is_dir($pluginroot.'/.hg')) {
+            return 'mercurial';
+        }
+
         return false;
     }
 
index 0eccb8e..d255609 100644 (file)
@@ -189,6 +189,10 @@ class deployer {
             return 'svn';
         }
 
+        if (is_dir($pluginroot.'/.hg')) {
+            return 'mercurial';
+        }
+
         return false;
     }
 
index 99a8360..4dd6833 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20140112" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20140115" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
       </KEYS>
     </TABLE>
+    <TABLE NAME="lock_db" COMMENT="Stores active and inactive lock types for db locking method.">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="resourcekey" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="String identifying the resource to be locked. Should use frankenstyle format."/>
+        <FIELD NAME="expires" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Expiry time for an active lock."/>
+        <FIELD NAME="owner" TYPE="char" LENGTH="36" NOTNULL="false" SEQUENCE="false" COMMENT="uuid indicating the owner of the lock."/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="resourcekey_uniq" UNIQUE="true" FIELDS="resourcekey" COMMENT="Unique index for resourcekey"/>
+        <INDEX NAME="expires_idx" UNIQUE="false" FIELDS="expires" COMMENT="Index on expires column"/>
+        <INDEX NAME="owner_idx" UNIQUE="false" FIELDS="owner" COMMENT="Index on owner"/>
+      </INDEXES>
+    </TABLE>
   </TABLES>
 </XMLDB>
index 4937628..0fe0e43 100644 (file)
@@ -2927,5 +2927,46 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2014011701.00);
     }
 
+    if ($oldversion < 2014012300.01) {
+        // Remove deleted users home pages.
+        $sql = "DELETE FROM {my_pages}
+                WHERE EXISTS (SELECT {user}.id
+                                  FROM {user}
+                                  WHERE {user}.id = {my_pages}.userid
+                                  AND {user}.deleted = 1)
+                AND {my_pages}.private = 1";
+        $DB->execute($sql);
+
+        // Reached main savepoint.
+        upgrade_main_savepoint(true, 2014012300.01);
+    }
+
+    if ($oldversion < 2014012400.00) {
+        // Define table lock_db to be created.
+        $table = new xmldb_table('lock_db');
+
+        // Adding fields to table lock_db.
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('resourcekey', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('expires', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
+        $table->add_field('owner', XMLDB_TYPE_CHAR, '36', null, null, null, null);
+
+        // Adding keys to table lock_db.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+
+        // Adding indexes to table lock_db.
+        $table->add_index('resourcekey_uniq', XMLDB_INDEX_UNIQUE, array('resourcekey'));
+        $table->add_index('expires_idx', XMLDB_INDEX_NOTUNIQUE, array('expires'));
+        $table->add_index('owner_idx', XMLDB_INDEX_NOTUNIQUE, array('owner'));
+
+        // Conditionally launch create table for lock_db.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2014012400.00);
+    }
+
     return true;
 }
index 1112baf..741f790 100644 (file)
@@ -69,26 +69,21 @@ class database_manager {
     /**
      * This function will execute an array of SQL commands.
      *
-     * @param array $sqlarr Array of sql statements to execute.
-     * @throws ddl_exception This exception is thrown if any error is found.
+     * @param string[] $sqlarr Array of sql statements to execute.
+     * @throws ddl_change_structure_exception This exception is thrown if any error is found.
      */
     protected function execute_sql_arr(array $sqlarr) {
-        foreach ($sqlarr as $sql) {
-            $this->execute_sql($sql);
-        }
+        $this->mdb->change_database_structure($sqlarr);
     }
 
     /**
      * Execute a given sql command string.
      *
      * @param string $sql The sql string you wish to be executed.
-     * @throws ddl_exception This exception is thrown if any error is found.
+     * @throws ddl_change_structure_exception This exception is thrown if any error is found.
      */
     protected function execute_sql($sql) {
-        if (!$this->mdb->change_database_structure($sql)) {
-            // in case driver does not throw exceptions yet ;-)
-            throw new ddl_change_structure_exception($this->mdb->get_last_error(), $sql);
-        }
+        $this->mdb->change_database_structure($sql);
     }
 
     /**
index bbd6de2..7c11f47 100644 (file)
@@ -124,22 +124,66 @@ class mysql_sql_generator extends sql_generator {
 
         $sqlarr = parent::getCreateTableSQL($xmldb_table);
 
-        // Let's inject the extra MySQL tweaks.
-        foreach ($sqlarr as $i=>$sql) {
-            if (strpos($sql, 'CREATE TABLE ') === 0) {
+        // This is a very nasty hack that tries to use just one query per created table
+        // because MySQL is stupidly slow when modifying empty tables.
+        // Note: it is safer to inject everything on new lines because there might be some trailing -- comments.
+        $sqls = array();
+        $prevcreate = null;
+        $matches = null;
+        foreach ($sqlarr as $sql) {
+            if (preg_match('/^CREATE TABLE ([^ ]+)/', $sql, $matches)) {
+                $prevcreate = $matches[1];
+                $sql = preg_replace('/\s*\)\s*$/s', '/*keyblock*/)', $sql);
+                // Let's inject the extra MySQL tweaks here.
                 if ($engine) {
-                    $sqlarr[$i] .= " ENGINE = $engine";
+                    $sql .= "\n ENGINE = $engine";
                 }
                 if ($collation) {
                     if (strpos($collation, 'utf8_') === 0) {
-                        $sqlarr[$i] .= " DEFAULT CHARACTER SET utf8";
+                        $sql .= "\n DEFAULT CHARACTER SET utf8";
                     }
-                    $sqlarr[$i] .= " DEFAULT COLLATE = $collation";
+                    $sql .= "\n DEFAULT COLLATE = $collation";
                 }
+                $sqls[] = $sql;
+                continue;
             }
+            if ($prevcreate) {
+                if (preg_match('/^ALTER TABLE '.$prevcreate.' COMMENT=(.*)$/s', $sql, $matches)) {
+                    $prev = array_pop($sqls);
+                    $prev .= "\n COMMENT=$matches[1]";
+                    $sqls[] = $prev;
+                    continue;
+                }
+                if (preg_match('/^CREATE INDEX ([^ ]+) ON '.$prevcreate.' (.*)$/s', $sql, $matches)) {
+                    $prev = array_pop($sqls);
+                    if (strpos($prev, '/*keyblock*/')) {
+                        $prev = str_replace('/*keyblock*/', "\n, KEY $matches[1] $matches[2]/*keyblock*/", $prev);
+                        $sqls[] = $prev;
+                        continue;
+                    } else {
+                        $sqls[] = $prev;
+                    }
+                }
+                if (preg_match('/^CREATE UNIQUE INDEX ([^ ]+) ON '.$prevcreate.' (.*)$/s', $sql, $matches)) {
+                    $prev = array_pop($sqls);
+                    if (strpos($prev, '/*keyblock*/')) {
+                        $prev = str_replace('/*keyblock*/', "\n, UNIQUE KEY $matches[1] $matches[2]/*keyblock*/", $prev);
+                        $sqls[] = $prev;
+                        continue;
+                    } else {
+                        $sqls[] = $prev;
+                    }
+                }
+            }
+            $prevcreate = null;
+            $sqls[] = $sql;
         }
 
-        return $sqlarr;
+        foreach ($sqls as $key => $sql) {
+            $sqls[$key] = str_replace('/*keyblock*/', "\n", $sql);
+        }
+
+        return $sqls;
     }
 
     /**
index 070f0cb..d53a172 100644 (file)
@@ -91,6 +91,8 @@ abstract class moodle_database {
     protected $reads = 0;
     /** @var int The database writes (performance counter).*/
     protected $writes = 0;
+    /** @var float Time queries took to finish, seconds with microseconds.*/
+    protected $queriestime = 0;
 
     /** @var int Debug level. */
     protected $debug  = 0;
@@ -459,7 +461,10 @@ abstract class moodle_database {
         $logerrors = !empty($this->dboptions['logerrors']);
         $iserror   = ($error !== false);
 
-        $time = microtime(true) - $this->last_time;
+        $time = $this->query_time();
+
+        // Will be shown or not depending on MDL_PERF values rather than in dboptions['log*].
+        $this->queriestime = $this->queriestime + $time;
 
         if ($logall or ($logslow and ($logslow < ($time+0.00001))) or ($iserror and $logerrors)) {
             $this->loggingquery = true;
@@ -489,6 +494,14 @@ abstract class moodle_database {
         }
     }
 
+    /**
+     * Returns the time elapsed since the query started.
+     * @return float Seconds with microseconds
+     */
+    protected function query_time() {
+        return microtime(true) - $this->last_time;
+    }
+
     /**
      * Returns database server info array
      * @return array Array containing 'description' and 'version' at least.
@@ -543,7 +556,7 @@ abstract class moodle_database {
         if (!$this->get_debug()) {
             return;
         }
-        $time = microtime(true) - $this->last_time;
+        $time = $this->query_time();
         $message = "Query took: {$time} seconds.\n";
         if (CLI_SCRIPT) {
             echo $message;
@@ -1079,9 +1092,9 @@ abstract class moodle_database {
 
     /**
      * Do NOT use in code, this is for use by database_manager only!
-     * @param string $sql query
+     * @param string|array $sql query or array of queries
      * @return bool true
-     * @throws dml_exception A DML specific exception is thrown for any errors.
+     * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
      */
     public abstract function change_database_structure($sql);
 
@@ -1595,6 +1608,45 @@ abstract class moodle_database {
      */
     public abstract function insert_record($table, $dataobject, $returnid=true, $bulk=false);
 
+    /**
+     * Insert multiple records into database as fast as possible.
+     *
+     * Order of inserts is maintained, but the operation is not atomic,
+     * use transactions if necessary.
+     *
+     * This method is intended for inserting of large number of small objects,
+     * do not use for huge objects with text or binary fields.
+     *
+     * @since 2.7
+     *
+     * @param string $table  The database table to be inserted into
+     * @param array|Traversable $dataobjects list of objects to be inserted, must be compatible with foreach
+     * @return void does not return new record ids
+     *
+     * @throws coding_exception if data objects have different structure
+     * @throws dml_exception A DML specific exception is thrown for any errors.
+     */
+    public function insert_records($table, $dataobjects) {
+        if (!is_array($dataobjects) and !($dataobjects instanceof Traversable)) {
+            throw new coding_exception('insert_records() passed non-traversable object');
+        }
+
+        $fields = null;
+        // Note: override in driver if there is a faster way.
+        foreach ($dataobjects as $dataobject) {
+            if (!is_array($dataobject) and !is_object($dataobject)) {
+                throw new coding_exception('insert_records() passed invalid record object');
+            }
+            $dataobject = (array)$dataobject;
+            if ($fields === null) {
+                $fields = array_keys($dataobject);
+            } else if ($fields !== array_keys($dataobject)) {
+                throw new coding_exception('All dataobjects in insert_records() must have the same structure!');
+            }
+            $this->insert_record($table, $dataobject, false);
+        }
+    }
+
     /**
      * Import a record into a table, id field is required.
      * Safety checks are NOT carried out. Lobs are supported.
@@ -2466,4 +2518,12 @@ abstract class moodle_database {
     public function perf_get_queries() {
         return $this->writes + $this->reads;
     }
+
+    /**
+     * Time waiting for the database engine to finish running all queries.
+     * @return float Number of seconds with microseconds
+     */
+    public function perf_get_queries_time() {
+        return $this->queriestime;
+    }
 }
index 0a324b1..1360898 100644 (file)
@@ -327,7 +327,7 @@ class mssql_native_moodle_database extends moodle_database {
         if ($result) {
             while ($row = mssql_fetch_row($result)) {
                 $tablename = reset($row);
-                if ($this->prefix !== '') {
+                if ($this->prefix !== false && $this->prefix !== '') {
                     if (strpos($tablename, $this->prefix) !== 0) {
                         continue;
                     }
@@ -595,17 +595,26 @@ class mssql_native_moodle_database extends moodle_database {
 
     /**
      * Do NOT use in code, to be used by database_manager only!
-     * @param string $sql query
+     * @param string|array $sql query
      * @return bool true
-     * @throws dml_exception A DML specific exception is thrown for any errors.
+     * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
      */
     public function change_database_structure($sql) {
-        $this->reset_caches();
+        $this->get_manager(); // Includes DDL exceptions classes ;-)
+        $sqls = (array)$sql;
 
-        $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
-        $result = mssql_query($sql, $this->mssql);
-        $this->query_end($result);
+        try {
+            foreach ($sqls as $sql) {
+                $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
+                $result = mssql_query($sql, $this->mssql);
+                $this->query_end($result);
+            }
+        } catch (ddl_change_structure_exception $e) {
+            $this->reset_caches();
+            throw $e;
+        }
 
+        $this->reset_caches();
         return true;
     }
 
index 3cb62a3..c3c2769 100644 (file)
@@ -37,6 +37,7 @@ require_once(__DIR__.'/mysqli_native_moodle_temptables.php');
  */
 class mysqli_native_moodle_database extends moodle_database {
 
+    /** @var mysqli $mysqli */
     protected $mysqli = null;
 
     private $transactions_supported = null;
@@ -506,7 +507,7 @@ class mysqli_native_moodle_database extends moodle_database {
      * Returns detailed information about columns in table. This information is cached internally.
      * @param string $table name
      * @param bool $usecache
-     * @return array array of database_column_info objects indexed with column names
+     * @return database_column_info[] array of database_column_info objects indexed with column names
      */
     public function get_columns($table, $usecache=true) {
 
@@ -821,17 +822,38 @@ class mysqli_native_moodle_database extends moodle_database {
 
     /**
      * Do NOT use in code, to be used by database_manager only!
-     * @param string $sql query
+     * @param string|array $sql query
      * @return bool true
-     * @throws dml_exception A DML specific exception is thrown for any errors.
+     * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
      */
     public function change_database_structure($sql) {
-        $this->reset_caches();
+        $this->get_manager(); // Includes DDL exceptions classes ;-)
+        if (is_array($sql)) {
+            $sql = implode("\n;\n", $sql);
+        }
 
-        $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
-        $result = $this->mysqli->query($sql);
-        $this->query_end($result);
+        try {
+            $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
+            $result = $this->mysqli->multi_query($sql);
+            if ($result === false) {
+                $this->query_end(false);
+            }
+            while ($this->mysqli->more_results()) {
+                $result = $this->mysqli->next_result();
+                if ($result === false) {
+                    $this->query_end(false);
+                }
+            }
+            $this->query_end(true);
+        } catch (ddl_change_structure_exception $e) {
+            while (@$this->mysqli->more_results()) {
+                @$this->mysqli->next_result();
+            }
+            $this->reset_caches();
+            throw $e;
+        }
 
+        $this->reset_caches();
         return true;
     }
 
@@ -1122,6 +1144,124 @@ class mysqli_native_moodle_database extends moodle_database {
         return $this->insert_record_raw($table, $cleaned, $returnid, $bulk);
     }
 
+    /**
+     * Insert multiple records into database as fast as possible.
+     *
+     * Order of inserts is maintained, but the operation is not atomic,
+     * use transactions if necessary.
+     *
+     * This method is intended for inserting of large number of small objects,
+     * do not use for huge objects with text or binary fields.
+     *
+     * @since 2.7
+     *
+     * @param string $table  The database table to be inserted into
+     * @param array|Traversable $dataobjects list of objects to be inserted, must be compatible with foreach
+     * @return void does not return new record ids
+     *
+     * @throws coding_exception if data objects have different structure
+     * @throws dml_exception A DML specific exception is thrown for any errors.
+     */
+    public function insert_records($table, $dataobjects) {
+        if (!is_array($dataobjects) and !$dataobjects instanceof Traversable) {
+            throw new coding_exception('insert_records() passed non-traversable object');
+        }
+
+        // MySQL has a relatively small query length limit by default,
+        // make sure 'max_allowed_packet' in my.cnf is high enough
+        // if you change the following default...
+        static $chunksize = null;
+        if ($chunksize === null) {
+            if (!empty($this->dboptions['bulkinsertsize'])) {
+                $chunksize = (int)$this->dboptions['bulkinsertsize'];
+
+            } else {
+                if (PHP_INT_SIZE === 4) {
+                    // Bad luck for Windows, we cannot do any maths with large numbers.
+                    $chunksize = 5;
+                } else {
+                    $sql = "SHOW VARIABLES LIKE 'max_allowed_packet'";
+                    $this->query_start($sql, null, SQL_QUERY_AUX);
+                    $result = $this->mysqli->query($sql);
+                    $this->query_end($result);
+                    $size = 0;
+                    if ($rec = $result->fetch_assoc()) {
+                        $size = $rec['Value'];
+                    }
+                    $result->close();
+                    // Hopefully 200kb per object are enough.
+                    $chunksize = (int)($size / 200000);
+                    if ($chunksize > 50) {
+                        $chunksize = 50;
+                    }
+                }
+            }
+        }
+
+        $columns = $this->get_columns($table, true);
+        $fields = null;
+        $count = 0;
+        $chunk = array();
+        foreach ($dataobjects as $dataobject) {
+            if (!is_array($dataobject) and !is_object($dataobject)) {
+                throw new coding_exception('insert_records() passed invalid record object');
+            }
+            $dataobject = (array)$dataobject;
+            if ($fields === null) {
+                $fields = array_keys($dataobject);
+                $columns = array_intersect_key($columns, $dataobject);
+                unset($columns['id']);
+            } else if ($fields !== array_keys($dataobject)) {
+                throw new coding_exception('All dataobjects in insert_records() must have the same structure!');
+            }
+
+            $count++;
+            $chunk[] = $dataobject;
+
+            if ($count === $chunksize) {
+                $this->insert_chunk($table, $chunk, $columns);
+                $chunk = array();
+                $count = 0;
+            }
+        }
+
+        if ($count) {
+            $this->insert_chunk($table, $chunk, $columns);
+        }
+    }
+
+    /**
+     * Insert records in chunks.
+     *
+     * Note: can be used only from insert_records().
+     *
+     * @param string $table
+     * @param array $chunk
+     * @param database_column_info[] $columns
+     */
+    protected function insert_chunk($table, array $chunk, array $columns) {
+        $fieldssql = '('.implode(',', array_keys($columns)).')';
+
+        $valuessql = '('.implode(',', array_fill(0, count($columns), '?')).')';
+        $valuessql = implode(',', array_fill(0, count($chunk), $valuessql));
+
+        $params = array();
+        foreach ($chunk as $dataobject) {
+            foreach ($columns as $field => $column) {
+                $params[] = $this->normalise_value($column, $dataobject[$field]);
+            }
+        }
+
+        $sql = "INSERT INTO {$this->prefix}$table $fieldssql VALUES $valuessql";
+
+        list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
+        $rawsql = $this->emulate_bound_params($sql, $params);
+
+        $this->query_start($sql, $params, SQL_QUERY_INSERT);
+        $result = $this->mysqli->query($rawsql);
+        $this->query_end($result);
+    }
+
     /**
      * Import a record into a table, id field is required.
      * Safety checks are NOT carried out. Lobs are supported.
index 00f79d6..fed5c85 100644 (file)
@@ -406,7 +406,7 @@ class oci_native_moodle_database extends moodle_database {
         oci_free_statement($stmt);
         $records = array_map('strtolower', $records['TABLE_NAME']);
         foreach ($records as $tablename) {
-            if ($this->prefix !== '') {
+            if ($this->prefix !== false && $this->prefix !== '') {
                 if (strpos($tablename, $this->prefix) !== 0) {
                     continue;
                 }
@@ -888,19 +888,28 @@ class oci_native_moodle_database extends moodle_database {
 
     /**
      * Do NOT use in code, to be used by database_manager only!
-     * @param string $sql query
+     * @param string|array $sql query
      * @return bool true
-     * @throws dml_exception A DML specific exception is thrown for any errors.
+     * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
      */
     public function change_database_structure($sql) {
-        $this->reset_caches();
-
-        $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
-        $stmt = $this->parse_query($sql);
-        $result = oci_execute($stmt, $this->commit_status);
-        $this->query_end($result, $stmt);
-        oci_free_statement($stmt);
+        $this->get_manager(); // Includes DDL exceptions classes ;-)
+        $sqls = (array)$sql;
+
+        try {
+            foreach ($sqls as $sql) {
+                $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
+                $stmt = $this->parse_query($sql);
+                $result = oci_execute($stmt, $this->commit_status);
+                $this->query_end($result, $stmt);
+                oci_free_statement($stmt);
+            }
+        } catch (ddl_change_structure_exception $e) {
+            $this->reset_caches();
+            throw $e;
+        }
 
+        $this->reset_caches();
         return true;
     }
 
index 53fd8d5..29cdd65 100644 (file)
@@ -174,22 +174,34 @@ abstract class pdo_moodle_database extends moodle_database {
 
     /**
      * Do NOT use in code, to be used by database_manager only!
-     * @param string $sql query
-     * @return bool success
+     * @param string|array $sql query
+     * @return bool true
+     * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
      */
     public function change_database_structure($sql) {
-        $result = true;
-        $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
+        $this->get_manager(); // Includes DDL exceptions classes ;-)
+        $sqls = (array)$sql;
 
         try {
-            $this->pdb->exec($sql);
+            foreach ($sqls as $sql) {
+                $result = true;
+                $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
+
+                try {
+                    $this->pdb->exec($sql);
+                } catch (PDOException $ex) {
+                    $this->lastError = $ex->getMessage();
+                    $result = false;
+                }
+                $this->query_end($result);
+            }
+        } catch (ddl_change_structure_exception $e) {
             $this->reset_caches();
-        } catch (PDOException $ex) {
-            $this->lastError = $ex->getMessage();
-            $result = false;
+            throw $e;
         }
-        $this->query_end($result);
-        return $result;
+
+        $this->reset_caches();
+        return true;
     }
 
     public function delete_records_select($table, $select, array $params=null) {
index 2ee1635..717e632 100644 (file)
@@ -37,6 +37,7 @@ require_once(__DIR__.'/pgsql_native_moodle_temptables.php');
  */
 class pgsql_native_moodle_database extends moodle_database {
 
+    /** @var resource $pgsql database resource */
     protected $pgsql     = null;
     protected $bytea_oid = null;
 
@@ -320,7 +321,7 @@ class pgsql_native_moodle_database extends moodle_database {
         if ($result) {
             while ($row = pg_fetch_row($result)) {
                 $tablename = reset($row);
-                if ($this->prefix !== '') {
+                if ($this->prefix !== false && $this->prefix !== '') {
                     if (strpos($tablename, $this->prefix) !== 0) {
                         continue;
                     }
@@ -381,7 +382,7 @@ class pgsql_native_moodle_database extends moodle_database {
      * Returns detailed information about columns in table. This information is cached internally.
      * @param string $table name
      * @param bool $usecache
-     * @return array array of database_column_info objects indexed with column names
+     * @return database_column_info[] array of database_column_info objects indexed with column names
      */
     public function get_columns($table, $usecache=true) {
         if ($usecache) {
@@ -625,18 +626,35 @@ class pgsql_native_moodle_database extends moodle_database {
 
     /**
      * Do NOT use in code, to be used by database_manager only!
-     * @param string $sql query
+     * @param string|array $sql query
      * @return bool true
-     * @throws dml_exception A DML specific exception is thrown for any errors.
+     * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
      */
     public function change_database_structure($sql) {
-        $this->reset_caches();
+        $this->get_manager(); // Includes DDL exceptions classes ;-)
+        if (is_array($sql)) {
+            $sql = implode("\n;\n", $sql);
+        }
+        if (!$this->is_transaction_started()) {
+            // It is better to do all or nothing, this helps with recovery...
+            $sql = "BEGIN ISOLATION LEVEL SERIALIZABLE;\n$sql\n; COMMIT";
+        }
 
-        $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
-        $result = pg_query($this->pgsql, $sql);
-        $this->query_end($result);
+        try {
+            $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
+            $result = pg_query($this->pgsql, $sql);
+            $this->query_end($result);
+            pg_free_result($result);
+        } catch (ddl_change_structure_exception $e) {
+            if (!$this->is_transaction_started()) {
+                $result = @pg_query($this->pgsql, "ROLLBACK");
+                @pg_free_result($result);
+            }
+            $this->reset_caches();
+            throw $e;
+        }
 
-        pg_free_result($result);
+        $this->reset_caches();
         return true;
     }
 
@@ -919,6 +937,108 @@ class pgsql_native_moodle_database extends moodle_database {
 
     }
 
+    /**
+     * Insert multiple records into database as fast as possible.
+     *
+     * Order of inserts is maintained, but the operation is not atomic,
+     * use transactions if necessary.
+     *
+     * This method is intended for inserting of large number of small objects,
+     * do not use for huge objects with text or binary fields.
+     *
+     * @since 2.7
+     *
+     * @param string $table  The database table to be inserted into
+     * @param array|Traversable $dataobjects list of objects to be inserted, must be compatible with foreach
+     * @return void does not return new record ids
+     *
+     * @throws coding_exception if data objects have different structure
+     * @throws dml_exception A DML specific exception is thrown for any errors.
+     */
+    public function insert_records($table, $dataobjects) {
+        if (!is_array($dataobjects) and !($dataobjects instanceof Traversable)) {
+            throw new coding_exception('insert_records() passed non-traversable object');
+        }
+
+        // PostgreSQL does not seem to have problems with huge queries.
+        $chunksize = 500;
+        if (!empty($this->dboptions['bulkinsertsize'])) {
+            $chunksize = (int)$this->dboptions['bulkinsertsize'];
+        }
+
+        $columns = $this->get_columns($table, true);
+
+        // Make sure there are no nasty blobs!
+        foreach ($columns as $column) {
+            if ($column->binary) {
+                parent::insert_records($table, $dataobjects);
+                return;
+            }
+        }
+
+        $fields = null;
+        $count = 0;
+        $chunk = array();
+        foreach ($dataobjects as $dataobject) {
+            if (!is_array($dataobject) and !is_object($dataobject)) {
+                throw new coding_exception('insert_records() passed invalid record object');
+            }
+            $dataobject = (array)$dataobject;
+            if ($fields === null) {
+                $fields = array_keys($dataobject);
+                $columns = array_intersect_key($columns, $dataobject);
+                unset($columns['id']);
+            } else if ($fields !== array_keys($dataobject)) {
+                throw new coding_exception('All dataobjects in insert_records() must have the same structure!');
+            }
+
+            $count++;
+            $chunk[] = $dataobject;
+
+            if ($count === $chunksize) {
+                $this->insert_chunk($table, $chunk, $columns);
+                $chunk = array();
+                $count = 0;
+            }
+        }
+
+        if ($count) {
+            $this->insert_chunk($table, $chunk, $columns);
+        }
+    }
+
+    /**
+     * Insert records in chunks, no binary support, strict param types...
+     *
+     * Note: can be used only from insert_records().
+     *
+     * @param string $table
+     * @param array $chunk
+     * @param database_column_info[] $columns
+     */
+    protected function insert_chunk($table, array $chunk, array $columns) {
+        $i = 1;
+        $params = array();
+        $values = array();
+        foreach ($chunk as $dataobject) {
+            $vals = array();
+            foreach ($columns as $field => $column) {
+                $params[] = $this->normalise_value($column, $dataobject[$field]);
+                $vals[] = "\$".$i++;
+            }
+            $values[] = '('.implode(',', $vals).')';
+        }
+
+        $fieldssql = '('.implode(',', array_keys($columns)).')';
+        $valuessql = implode(',', $values);
+
+        $sql = "INSERT INTO {$this->prefix}$table $fieldssql VALUES $valuessql";
+        $this->query_start($sql, $params, SQL_QUERY_INSERT);
+        $result = pg_query_params($this->pgsql, $sql, $params);
+        $this->query_end($result);
+        pg_free_result($result);
+    }
+
     /**
      * Import a record into a table, id field is required.
      * Safety checks are NOT carried out. Lobs are supported.
index 7828eb7..fcfe446 100644 (file)
@@ -150,7 +150,7 @@ class sqlite3_pdo_moodle_database extends pdo_moodle_database {
         foreach ($rstables as $table) {
             $table = $table['name'];
             $table = strtolower($table);
-            if ($this->prefix !== '') {
+            if ($this->prefix !== false && $this->prefix !== '') {
                 if (strpos($table, $this->prefix) !== 0) {
                     continue;
                 }
index f6cab36..eb4569f 100644 (file)
@@ -380,7 +380,7 @@ class sqlsrv_native_moodle_database extends moodle_database {
         if ($result) {
             while ($row = sqlsrv_fetch_array($result)) {
                 $tablename = reset($row);
-                if ($this->prefix !== '') {
+                if ($this->prefix !== false && $this->prefix !== '') {
                     if (strpos($tablename, $this->prefix) !== 0) {
                         continue;
                     }
@@ -669,17 +669,26 @@ class sqlsrv_native_moodle_database extends moodle_database {
 
     /**
      * Do NOT use in code, to be used by database_manager only!
-     * @param string $sql query
+     * @param string|array $sql query
      * @return bool true
-     * @throws dml_exception A DML specific exception is thrown for any errors.
+     * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
      */
     public function change_database_structure($sql) {
-        $this->reset_caches();
-
-        $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
-        $result = sqlsrv_query($this->sqlsrv, $sql);
-        $this->query_end($result);
+        $this->get_manager(); // Includes DDL exceptions classes ;-)
+        $sqls = (array)$sql;
+
+        try {
+            foreach ($sqls as $sql) {
+                $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
+                $result = sqlsrv_query($this->sqlsrv, $sql);
+                $this->query_end($result);
+            }
+        } catch (ddl_change_structure_exception $e) {
+            $this->reset_caches();
+            throw $e;
+        }
 
+        $this->reset_caches();
         return true;
     }
 
index 869d6ba..223eaee 100644 (file)
@@ -2263,6 +2263,114 @@ class core_dml_testcase extends database_driver_testcase {
         }
     }
 
+    public function test_insert_records() {
+        $DB = $this->tdb;
+        $dbman = $DB->get_manager();
+
+        $table = $this->get_test_table();
+        $tablename = $table->getName();
+
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
+        $table->add_field('oneint', XMLDB_TYPE_INTEGER, '10', null, null, null, 100);
+        $table->add_field('onenum', XMLDB_TYPE_NUMBER, '10,2', null, null, null, 200);
+        $table->add_field('onechar', XMLDB_TYPE_CHAR, '100', null, null, null, 'onestring');
+        $table->add_field('onetext', XMLDB_TYPE_TEXT, 'big', null, null, null);
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $dbman->create_table($table);
+
+        $this->assertCount(0, $DB->get_records($tablename));
+
+        $record = new stdClass();
+        $record->id = '1';
+        $record->course = '1';
+        $record->oneint = null;
+        $record->onenum = '1.00';
+        $record->onechar = 'a';
+        $record->onetext = 'aaa';
+
+        $expected = array();
+        $records = array();
+        for ($i = 1; $i <= 2000; $i++) { // This may take a while, it should be higher than defaults in DML drivers.
+            $rec = clone($record);
+            $rec->id = (string)$i;
+            $rec->oneint = (string)$i;
+            $expected[$i] = $rec;
+            $rec = clone($rec);
+            unset($rec->id);
+            $records[$i] = $rec;
+        }
+
+        $DB->insert_records($tablename, $records);
+        $stored = $DB->get_records($tablename, array(), 'id ASC');
+        $this->assertEquals($expected, $stored);
+
+        // Test there can be some extra properties including id.
+        $count = $DB->count_records($tablename);
+        $rec1 = (array)$record;
+        $rec1['xxx'] = 1;
+        $rec2 = (array)$record;
+        $rec2['xxx'] = 2;
+
+        $records = array($rec1, $rec2);
+        $DB->insert_records($tablename, $records);
+        $this->assertEquals($count + 2, $DB->count_records($tablename));
+
+        // Test not all properties are necessary.
+        $rec1 = (array)$record;
+        unset($rec1['course']);
+        $rec2 = (array)$record;
+        unset($rec2['course']);
+
+        $records = array($rec1, $rec2);
+        $DB->insert_records($tablename, $records);
+
+        // Make sure no changes in data object structure are tolerated.
+        $rec1 = (array)$record;
+        unset($rec1['id']);
+        $rec2 = (array)$record;
+        unset($rec2['id']);
+
+        $records = array($rec1, $rec2);
+        $DB->insert_records($tablename, $records);
+
+        $rec2['xx'] = '1';
+        $records = array($rec1, $rec2);
+        try {
+            $DB->insert_records($tablename, $records);
+            $this->fail('coding_exception expected when insert_records receives different object data structures');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+
+        unset($rec2['xx']);
+        unset($rec2['course']);
+        $rec2['course'] = '1';
+        $records = array($rec1, $rec2);
+        try {
+            $DB->insert_records($tablename, $records);
+            $this->fail('coding_exception expected when insert_records receives different object data structures');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+
+        $records = 1;
+        try {
+            $DB->insert_records($tablename, $records);
+            $this->fail('coding_exception expected when insert_records receives non-traversable data');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+
+        $records = array(1);
+        try {
+            $DB->insert_records($tablename, $records);
+            $this->fail('coding_exception expected when insert_records receives non-objet record');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+    }
+
     public function test_import_record() {
         // All the information in this test is fetched from DB by get_recordset() so we
         // have such method properly tested against nulls, empties and friends...
index 4d9b7bb..5cdc3f2 100644 (file)
@@ -132,7 +132,7 @@ abstract class database_exporter {
         if ($this->check_schema and $errors = $this->manager->check_database_schema($this->schema)) {
             $details = '';
             foreach ($errors as $table=>$items) {
-                $details .= '<div>'.get_string('table').' '.$table.':';
+                $details .= '<div>'.get_string('tablex', 'dbtransfer', $table);
                 $details .= '<ul>';
                 foreach ($items as $item) {
                     $details .= '<li>'.$item.'</li>';
index 0ac6de5..0fb40ba 100644 (file)
@@ -456,8 +456,10 @@ class tgz_extractor {
                         'Failed to close output file: ' .  $this->currentfile);
             }
 
-            // Update modified time.
-            touch($this->currentfile, $this->currentmtime);
+            // At this point we should touch the file to set its modified
+            // time to $this->currentmtime. However, when extracting to the
+            // temp directory, cron will delete files more than a week old,
+            // so to avoid problems we leave all files at their current time.
         }
 
         if ($this->currentarchivepath === tgz_packer::ARCHIVE_INDEX_FILE) {
index 066b625..c00a0ef 100644 (file)
@@ -223,10 +223,16 @@ M.util.set_user_preference = function(name, value) {
 
 /**
  * Prints a confirmation dialog in the style of DOM.confirm().
- * @param object event A YUI DOM event or null if launched manually
- * @param string message The message to show in the dialog
- * @param string url The URL to forward to if YES is clicked. Disabled if fn is given
- * @param function fn A JS function to run if YES is clicked.
+ *
+ * @method show_confirm_dialog
+ * @param {EventFacade} e
+ * @param {Object} args
+ * @param {String} args.message The question to ask the user
+ * @param {Function} [args.callback] A callback to apply on confirmation.
+ * @param {Object} [args.scope] The scope to use when calling the callback.
+ * @param {Object} [args.callbackargs] Any arguments to pass to the callback.
+ * @param {String} [args.cancellabel] The label to use on the cancel button.
+ * @param {String} [args.continuelabel] The label to use on the continue button.
  */
 M.util.show_confirm_dialog = function(e, args) {
     var target = e.target;
@@ -234,47 +240,35 @@ M.util.show_confirm_dialog = function(e, args) {
         e.preventDefault();
     }
 
-    YUI().use('yui2-container', 'yui2-event', function(Y) {
-        var simpledialog = new Y.YUI2.widget.SimpleDialog('confirmdialog',
-            {width: '300px',
-              fixedcenter: true,
-              modal: true,
-              visible: false,
-              draggable: false
-            }
-        );
-
-        simpledialog.setHeader(M.str.admin.confirmation);
-        simpledialog.setBody(args.message);
-        simpledialog.cfg.setProperty('icon', Y.YUI2.widget.SimpleDialog.ICON_WARN);
-
-        var handle_cancel = function() {
-            simpledialog.hide();
-        };
-
-        var handle_yes = function() {
-            simpledialog.hide();
+    YUI().use('moodle-core-notification-confirm', function(Y) {
+        var confirmationDialogue = new M.core.confirm({
+            width: '300px',
+            center: true,
+            modal: true,
+            visible: false,
+            draggable: false,
+            title: M.util.get_string('confirmation', 'admin'),
+            noLabel: M.util.get_string('cancel', 'moodle'),
+            question: args.message
+        });
 
+        // The dialogue was submitted with a positive value indication.
+        confirmationDialogue.on('complete-yes', function(e) {
+            // Handle any callbacks.
             if (args.callback) {
-                // args comes from PHP, so callback will be a string, needs to be evaluated by JS
-                var callback = null;
-                if (Y.Lang.isFunction(args.callback)) {
-                    callback = args.callback;
-                } else {
-                    callback = eval('('+args.callback+')');
+                if (!Y.Lang.isFunction(args.callback)) {
+                    Y.log('Callbacks to show_confirm_dialog must now be functions. Please update your code to pass in a function instead.',
+                            'warn', 'M.util.show_confirm_dialog');
+                    return;
                 }
 
+                var scope = e.target;
                 if (Y.Lang.isObject(args.scope)) {
-                    var sc = args.scope;
-                } else {
-                    var sc = e.target;
+                    scope = args.scope;
                 }
 
-                if (args.callbackargs) {
-                    callback.apply(sc, args.callbackargs);
-                } else {
-                    callback.apply(sc);
-                }
+                var callbackargs = args.callbackargs || [];
+                args.callback.apply(scope, callbackargs);
                 return;
             }
 
@@ -288,9 +282,7 @@ M.util.show_confirm_dialog = function(e, args) {
                 window.location = targetancestor.get('href');
 
             } else if (target.test('input')) {
-                targetform = target.ancestor(function(node) { return node.get('tagName').toLowerCase() == 'form'; });
-                // We cannot use target.ancestor('form') on the previous line
-                // because of http://yuilibrary.com/projects/yui3/ticket/2531561
+                targetform = target.ancestor('form', true);
                 if (!targetform) {
                     return;
                 }
@@ -300,32 +292,26 @@ M.util.show_confirm_dialog = function(e, args) {
                 }
                 targetform.submit();
 
-            } else if (target.get('tagName').toLowerCase() == 'form') {
-                // We cannot use target.test('form') on the previous line because of
-                // http://yuilibrary.com/projects/yui3/ticket/2531561
+            } else if (target.test('form')) {
                 target.submit();
 
-            } else if (M.cfg.developerdebug) {
-                alert("Element of type " + target.get('tagName') + " is not supported by the M.util.show_confirm_dialog function. Use A, INPUT, or FORM");
+            } else {
+                Y.log("Element of type " + target.get('tagName') +
+                        " is not supported by the M.util.show_confirm_dialog function. Use A, INPUT, or FORM",
+                        'warn', 'javascript-static');
             }
-        };
+        }, this);
 
-        if (!args.cancellabel) {
-            args.cancellabel = M.str.moodle.cancel;
-        }
-        if (!args.continuelabel) {
-            args.continuelabel = M.str.moodle.yes;
+        if (args.cancellabel) {
+            confirmationDialogue.set('noLabel', args.cancellabel);
         }
 
-        var buttons = [
-            {text: args.cancellabel,   handler: handle_cancel, isDefault: true},
-            {text: args.continuelabel, handler: handle_yes}
-        ];
-
-        simpledialog.cfg.queueProperty('buttons', buttons);
+        if (args.continuelabel) {
+            confirmationDialogue.set('yesLabel', args.continuelabel);
+        }
 
-        simpledialog.render(document.body);
-        simpledialog.show();
+        confirmationDialogue.render()
+                .show();
     });
 };
 
index 312ac4e..39cfd10 100644 (file)
@@ -3210,8 +3210,8 @@ function require_logout() {
  */
 function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
     global $CFG, $PAGE, $SITE;
-    $issite = (is_object($courseorid) and $courseorid->id == SITEID)
-          or (!is_object($courseorid) and $courseorid == SITEID);
+    $issite = ((is_object($courseorid) and $courseorid->id == SITEID)
+          or (!is_object($courseorid) and $courseorid == SITEID));
     if ($issite && !empty($cm) && !($cm instanceof cm_info)) {
         // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
         // db queries so this is not really a performance concern, however it is obviously
@@ -3397,9 +3397,7 @@ function get_user_key($script, $userid, $instance=null, $iprestriction=null, $va
  * @return bool Always returns true
  */
 function update_user_login_times() {
-    global $USER, $DB, $CFG;
-
-    require_once($CFG->dirroot.'/user/lib.php');
+    global $USER, $DB;
 
     if (isguestuser()) {
         // Do not update guest access times/ips for performance.
@@ -3425,7 +3423,9 @@ function update_user_login_times() {
     $USER->lastaccess = $user->lastaccess = $now;
     $USER->lastip = $user->lastip = getremoteaddr();
 
-    user_update_user($user, false);
+    // Note: do not call user_update_user() here because this is part of the login process,
+    //       the login event means that these fields were updated.
+    $DB->update_record('user', $user);
     return true;
 }
 
@@ -4233,6 +4233,9 @@ function delete_user(stdClass $user) {
     // Remove users private keys.
     $DB->delete_records('user_private_key', array('userid' => $user->id));
 
+    // Remove users customised pages.
+    $DB->delete_records('my_pages', array('userid' => $user->id, 'private' => 1));
+
     // Force logout - may fail if file based sessions used, sorry.
     \core\session\manager::kill_user_sessions($user->id);
 
@@ -4335,16 +4338,24 @@ function authenticate_user_login($username, $password, $ignorelockout=false, &$f
         // Use manual if auth not set.
         $auth = empty($user->auth) ? 'manual' : $user->auth;
         if (!empty($user->suspended)) {
-            add_to_log(SITEID, 'login', 'error', 'index.php', $username);
-            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
             $failurereason = AUTH_LOGIN_SUSPENDED;
+
+            // Trigger login failed event.
+            $event = \core\event\user_login_failed::create(array('userid' => $user->id,
+                    'other' => array('username' => $username, 'reason' => $failurereason)));
+            $event->trigger();
+            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
             return false;
         }
         if ($auth=='nologin' or !is_enabled_auth($auth)) {
-            add_to_log(SITEID, 'login', 'error', 'index.php', $username);
-            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Disabled Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
             // Legacy way to suspend user.
             $failurereason = AUTH_LOGIN_SUSPENDED;
+
+            // Trigger login failed event.
+            $event = \core\event\user_login_failed::create(array('userid' => $user->id,
+                    'other' => array('username' => $username, 'reason' => $failurereason)));
+            $event->trigger();
+            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Disabled Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
             return false;
         }
         $auths = array($auth);
@@ -4352,16 +4363,27 @@ function authenticate_user_login($username, $password, $ignorelockout=false, &$f
     } else {
         // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user().
         if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id,  'deleted' => 1))) {
-            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Deleted Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
             $failurereason = AUTH_LOGIN_NOUSER;
+
+            // Trigger login failed event.
+            $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
+                    'reason' => $failurereason)));
+            $event->trigger();
+            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Deleted Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
             return false;
         }
 
         // Do not try to authenticate non-existent accounts when user creation is not disabled.
         if (!empty($CFG->authpreventaccountcreation)) {
-            add_to_log(SITEID, 'login', 'error', 'index.php', $username);
-            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Unknown user, can not create new accounts:  $username  ".$_SERVER['HTTP_USER_AGENT']);
             $failurereason = AUTH_LOGIN_NOUSER;
+
+            // Trigger login failed event.
+            $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
+                    'reason' => $failurereason)));
+            $event->trigger();
+
+            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Unknown user, can not create new accounts:  $username  ".
+                    $_SERVER['HTTP_USER_AGENT']);
             return false;
         }
 
@@ -4377,9 +4399,14 @@ function authenticate_user_login($username, $password, $ignorelockout=false, &$f
     } else if ($user->id) {
         // Verify login lockout after other ways that may prevent user login.
         if (login_is_lockedout($user)) {
-            add_to_log(SITEID, 'login', 'error', 'index.php', $username);
-            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Login lockout:  $username  ".$_SERVER['HTTP_USER_AGENT']);
             $failurereason = AUTH_LOGIN_LOCKOUT;
+
+            // Trigger login failed event.
+            $event = \core\event\user_login_failed::create(array('userid' => $user->id,
+                    'other' => array('username' => $username, 'reason' => $failurereason)));
+            $event->trigger();
+
+            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Login lockout:  $username  ".$_SERVER['HTTP_USER_AGENT']);
             return false;
         }
     } else {
@@ -4425,14 +4452,21 @@ function authenticate_user_login($username, $password, $ignorelockout=false, &$f
 
         if (empty($user->id)) {
             $failurereason = AUTH_LOGIN_NOUSER;
+            // Trigger login failed event.
+            $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
+                    'reason' => $failurereason)));
+            $event->trigger();
             return false;
         }
 
         if (!empty($user->suspended)) {
             // Just in case some auth plugin suspended account.
-            add_to_log(SITEID, 'login', 'error', 'index.php', $username);
-            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
             $failurereason = AUTH_LOGIN_SUSPENDED;
+            // Trigger login failed event.
+            $event = \core\event\user_login_failed::create(array('userid' => $user->id,
+                    'other' => array('username' => $username, 'reason' => $failurereason)));
+            $event->trigger();
+            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
             return false;
         }
 
@@ -4442,7 +4476,6 @@ function authenticate_user_login($username, $password, $ignorelockout=false, &$f
     }
 
     // Failed if all the plugins have failed.
-    add_to_log(SITEID, 'login', 'error', 'index.php', $username);
     if (debugging('', DEBUG_ALL)) {
         error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Failed Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
     }
@@ -4450,8 +4483,16 @@ function authenticate_user_login($username, $password, $ignorelockout=false, &$f
     if ($user->id) {
         login_attempt_failed($user);
         $failurereason = AUTH_LOGIN_FAILED;
+        // Trigger login failed event.
+        $event = \core\event\user_login_failed::create(array('userid' => $user->id,
+                'other' => array('username' => $username, 'reason' => $failurereason)));
+        $event->trigger();
     } else {
         $failurereason = AUTH_LOGIN_NOUSER;
+        // Trigger login failed event.
+        $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
+                'reason' => $failurereason)));
+        $event->trigger();
     }
 
     return false;
@@ -8857,6 +8898,10 @@ function get_performance_info() {
     $info['html'] .= '<span class="dbqueries">DB reads/writes: '.$info['dbqueries'].'</span> ';
     $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' ';
 
+    $info['dbtime'] = round($DB->perf_get_queries_time(), 5);
+    $info['html'] .= '<span class="dbtime">DB queries time: '.$info['dbtime'].' secs</span> ';
+    $info['txt'] .= 'db queries time: ' . $info['dbtime'] . 's ';
+
     if (function_exists('posix_times')) {
         $ptimes = posix_times();
         if (is_array($ptimes)) {
index 12c10c3..38c6fdb 100644 (file)
@@ -93,13 +93,19 @@ class confirm_action extends component_action {
      *
      * @param string $message The message to display to the user when they are shown
      *    the confirm dialogue.
-     * @param string $callback The method to call when the user confirms the action.
+     * @param string $callback Deprecated since 2.7
      * @param string $continuelabel The string to use for he continue button
      * @param string $cancellabel The string to use for the cancel button
      */
     public function __construct($message, $callback = null, $continuelabel = null, $cancellabel = null) {
+        if ($callback !== null) {
+            debugging('The callback argument to new confirm_action() has been deprecated.' .
+                    ' If you need to use a callback, please write Javascript to use moodle-core-notification-confirmation ' .
+                    'and attach to the provided events.',
+                    DEBUG_DEVELOPER);
+        }
         parent::__construct('click', 'M.util.show_confirm_dialog', array(
-                'message' => $message, 'callback' => $callback,
+                'message' => $message,
                 'continuelabel' => $continuelabel, 'cancellabel' => $cancellabel));
     }
 }
@@ -197,4 +203,4 @@ class popup_action extends component_action {
 
         return $jsoptions;
     }
-}
\ No newline at end of file
+}
index d43d7c7..1f39798 100644 (file)
@@ -539,9 +539,6 @@ class core_renderer extends renderer_base {
               <li><a href="http://www.contentquality.com/mynewtester/cynthia.exe?rptmode=0&amp;warnp2n3e=1&amp;url1=' . urlencode(qualified_me()) . '">WCAG 1 (2,3) Check</a></li>
             </ul></div>';
         }
-        if (!empty($CFG->additionalhtmlfooter)) {
-            $output .= "\n".$CFG->additionalhtmlfooter;
-        }
         return $output;
     }
 
@@ -563,15 +560,22 @@ class core_renderer extends renderer_base {
 
     /**
      * The standard tags (typically script tags that are not needed earlier) that
-     * should be output after everything else. Designed to be called in theme layout.php files.
+     * should be output after everything else. Designed to be called in theme layout.php files.
      *
      * @return string HTML fragment.
      */
     public function standard_end_of_body_html() {
+        global $CFG;
+
         // This function is normally called from a layout.php file in {@link core_renderer::header()}
         // but some of the content won't be known until later, so we return a placeholder
         // for now. This will be replaced with the real content in {@link core_renderer::footer()}.
-        return $this->unique_end_html_token;
+        $output = '';
+        if (!empty($CFG->additionalhtmlfooter)) {
+            $output .= "\n".$CFG->additionalhtmlfooter;
+        }
+        $output .= $this->unique_end_html_token;
+        return $output;
     }
 
     /**
index 3b72e6e..309698e 100644 (file)
@@ -81,6 +81,9 @@ define('K_CELL_HEIGHT_RATIO', 1.25);
 /** reduction factor for small font */
 define('K_SMALL_RATIO', 2/3);
 
+/** Throw exceptions from errors so they can be caught and recovered from. */
+define('K_TCPDF_THROW_EXCEPTION_ERROR', true);
+
 require_once(dirname(__FILE__).'/tcpdf/tcpdf.php');
 
 /**
index 34b086e..04bd791 100644 (file)
@@ -343,6 +343,34 @@ abstract class advanced_testcase extends PHPUnit_Framework_TestCase {
         $this->assertEquals($expected, $legacydata, $message);
     }
 
+    /**
+     * Assert that an event is not using event->contxet.
+     * While restoring context might not be valid and it should not be used by event url
+     * or description methods.
+     *
+     * @param \core\event\base $event the event object.
+     * @param string $message
+     * @return void
+     */
+    public function assertEventContextNotUsed(\core\event\base $event, $message = '') {
+        // Save current event->context and set it to false.
+        $eventcontext = phpunit_event_mock::testable_get_event_context($event);
+        phpunit_event_mock::testable_set_event_context($event, false);
+        if ($message === '') {
+            $message = 'Event should not use context property of event in any method.';
+        }
+
+        // Test event methods should not use event->context.
+        $event->get_url();
+        $event->get_description();
+        $event->get_legacy_eventname();
+        phpunit_event_mock::testable_get_legacy_eventdata($event);
+        phpunit_event_mock::testable_get_legacy_logdata($event);
+
+        // Restore event->context.
+        phpunit_event_mock::testable_set_event_context($event, $eventcontext);
+    }
+
     /**
      * Stores current time as the base for assertTimeCurrent().
      *
index 26b99d1..578f300 100644 (file)
@@ -37,6 +37,8 @@ abstract class phpunit_event_mock extends \core\event\base {
 
     /**
      * Returns the log data of the event.
+     *
+     * @param \core\event\base $event event to get legacy eventdata from.
      * @return array
      */
     public static function testable_get_legacy_eventdata($event) {
@@ -45,10 +47,31 @@ abstract class phpunit_event_mock extends \core\event\base {
 
     /**
      * Returns the log data of the event.
+     *
+     * @param \core\event\base $event event to get legacy logdata from.
      * @return array
      */
     public static function testable_get_legacy_logdata($event) {
         return $event->get_legacy_logdata();
     }
 
+    /**
+     * Returns event context.
+     *
+     * @param \core\event\base $event event to get context for.
+     * @return context event context
+     */
+    public static function testable_get_event_context($event) {
+        return $event->context;
+    }
+
+    /**
+     * Sets event context.
+     *
+     * @param \core\event\base $event event to set context for.
+     * @param context $context context to set.
+     */
+    public static function testable_set_event_context($event, $context) {
+        $event->context = $context;
+    }
 }
index 0e6c477..799e53d 100644 (file)
@@ -1255,7 +1255,7 @@ function disable_output_buffering() {
  */
 function redirect_if_major_upgrade_required() {
     global $CFG;
-    $lastmajordbchanges = 2013100400.02;
+    $lastmajordbchanges = 2014012400.00;
     if (empty($CFG->version) or (float)$CFG->version < $lastmajordbchanges or
             during_initial_install() or !empty($CFG->adminsetuppending)) {
         try {
index 657c72e..4e0058c 100644 (file)
@@ -797,7 +797,7 @@ class flexible_table {
             }
             return format_text($text, $format, $options);
         } else {
-            $eci =& $this->export_class_instance();
+            $eci = $this->export_class_instance();
             return $eci->format_text($text, $format, $options, $courseid);
         }
     }
@@ -1527,9 +1527,9 @@ class table_spreadsheet_export_format_parent extends table_default_export_format
         $filename = $filename.'.'.$this->fileextension;
         $this->define_workbook();
         // format types
-        $this->formatnormal =& $this->workbook->add_format();
+        $this->formatnormal = $this->workbook->add_format();
         $this->formatnormal->set_bold(0);
-        $this->formatheaders =& $this->workbook->add_format();
+        $this->formatheaders = $this->workbook->add_format();
         $this->formatheaders->set_bold(1);
         $this->formatheaders->set_align('center');
         // Sending HTTP headers
index 6a9f8d6..ac7cc06 100644 (file)
@@ -60,7 +60,7 @@ class core_accesslib_testcase extends advanced_testcase {
         $this->assertNotEmpty($ACCESSLIB_PRIVATE->rolepermissions);
         $this->assertNotEmpty($ACCESSLIB_PRIVATE->rolepermissions);
         $this->assertNotEmpty($ACCESSLIB_PRIVATE->accessdatabyuser);
-        accesslib_clear_all_caches(true);
+        accesslib_clear_all_caches_for_unit_testing();
         $this->assertEmpty($ACCESSLIB_PRIVATE->rolepermissions);
         $this->assertEmpty($ACCESSLIB_PRIVATE->rolepermissions);
         $this->assertEmpty($ACCESSLIB_PRIVATE->dirtycontexts);
@@ -2095,7 +2095,7 @@ class core_accesslib_testcase extends advanced_testcase {
         unassign_capability('moodle/site:accessallgroups', $allroles['teacher'], $frontpagecontext->id, true);
         unset($rc);
 
-        accesslib_clear_all_caches(false); // Must be done after assign_capability().
+        accesslib_clear_all_caches_for_unit_testing(); // Must be done after assign_capability().
 
 
         // Test role_assign(), role_unassign(), role_unassign_all() functions.
@@ -2112,7 +2112,7 @@ class core_accesslib_testcase extends advanced_testcase {
         $this->assertEquals(0, $DB->count_records('role_assignments', array('contextid'=>$context->id)));
         unset($context);
 
-        accesslib_clear_all_caches(false); // Just in case.
+        accesslib_clear_all_caches_for_unit_testing(); // Just in case.
 
 
         // Test has_capability(), get_users_by_capability(), role_switch(), reload_all_capabilities() and friends functions.
@@ -2173,7 +2173,7 @@ class core_accesslib_testcase extends advanced_testcase {
 
         assign_capability('mod/page:view', CAP_PREVENT, $allroles['guest'], $systemcontext, true);
 
-        accesslib_clear_all_caches(false); // Must be done after assign_capability().
+        accesslib_clear_all_caches_for_unit_testing(); /// Must be done after assign_capability().
 
         // Extra tests for guests and not-logged-in users because they can not be verified by cross checking
         // with get_users_by_capability() where they are ignored.
@@ -2296,7 +2296,7 @@ class core_accesslib_testcase extends advanced_testcase {
         unset($permissions);
         unset($roles);
 
-        accesslib_clear_all_caches(false); // Must be done after assign_capability().
+        accesslib_clear_all_caches_for_unit_testing(); // must be done after assign_capability().
 
         // Test time - let's set up some real user, just in case the logic for USER affects the others...
         $USER = $DB->get_record('user', array('id'=>$testusers[3]));
index be7666f..2a1a05e 100644 (file)
@@ -133,35 +133,104 @@ class core_authlib_testcase extends advanced_testcase {
         $user1 = $this->getDataGenerator()->create_user(array('username'=>'username1', 'password'=>'password1'));
         $user2 = $this->getDataGenerator()->create_user(array('username'=>'username2', 'password'=>'password2', 'suspended'=>1));
         $user3 = $this->getDataGenerator()->create_user(array('username'=>'username3', 'password'=>'password3', 'auth'=>'nologin'));
-
+        // Capture events.
+        $sink = $this->redirectEvents();
         $result = authenticate_user_login('username1', 'password1');
+        $events = $sink->get_events();
+        $sink->close();
+
+        // No event is triggred.
+        $this->assertEmpty($events);
         $this->assertInstanceOf('stdClass', $result);
         $this->assertEquals($user1->id, $result->id);
 
         $reason = null;
+        // Capture event.
+        $sink = $this->redirectEvents();
         $result = authenticate_user_login('username1', 'password1', false, $reason);
+        $events = $sink->get_events();
+        $sink->close();
+
+        // No event is triggred.
+        $this->assertEmpty($events);
         $this->assertInstanceOf('stdClass', $result);
         $this->assertEquals(AUTH_LOGIN_OK, $reason);
 
         $reason = null;
+        // Capture failed login event.
+        $sink = $this->redirectEvents();
         $result = authenticate_user_login('username1', 'nopass', false, $reason);
+        $events = $sink->get_events();
+        $sink->close();
+        $event = array_pop($events);
+
         $this->assertFalse($result);
         $this->assertEquals(AUTH_LOGIN_FAILED, $reason);
+        // Test Event.
+        $this->assertInstanceOf('\core\event\user_login_failed', $event);
+        $expectedlogdata = array(SITEID, 'login', 'error', 'index.php', 'username1');
+        $this->assertEventLegacyLogData($expectedlogdata, $event);
+        $eventdata = $event->get_data();
+        $this->assertSame($eventdata['other']['username'], 'username1');
+        $this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_FAILED);
+        $this->assertEventContextNotUsed($event);
 
         $reason = null;
+        // Capture failed login event.
+        $sink = $this->redirectEvents();
         $result = authenticate_user_login('username2', 'password2', false, $reason);
+        $events = $sink->get_events();
+        $sink->close();
+        $event = array_pop($events);
+
         $this->assertFalse($result);
         $this->assertEquals(AUTH_LOGIN_SUSPENDED, $reason);
+        // Test Event.
+        $this->assertInstanceOf('\core\event\user_login_failed', $event);
+        $expectedlogdata = array(SITEID, 'login', 'error', 'index.php', 'username2');
+        $this->assertEventLegacyLogData($expectedlogdata, $event);
+        $eventdata = $event->get_data();
+        $this->assertSame($eventdata['other']['username'], 'username2');
+        $this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_SUSPENDED);
+        $this->assertEventContextNotUsed($event);
 
         $reason = null;
+        // Capture failed login event.
+        $sink = $this->redirectEvents();
         $result = authenticate_user_login('username3', 'password3', false, $reason);
+        $events = $sink->get_events();
+        $sink->close();
+        $event = array_pop($events);
+
         $this->assertFalse($result);
         $this->assertEquals(AUTH_LOGIN_SUSPENDED, $reason);
+        // Test Event.
+        $this->assertInstanceOf('\core\event\user_login_failed', $event);
+        $expectedlogdata = array(SITEID, 'login', 'error', 'index.php', 'username3');
+        $this->assertEventLegacyLogData($expectedlogdata, $event);
+        $eventdata = $event->get_data();
+        $this->assertSame($eventdata['other']['username'], 'username3');
+        $this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_SUSPENDED);
+        $this->assertEventContextNotUsed($event);
 
         $reason = null;
+        // Capture failed login event.
+        $sink = $this->redirectEvents();
         $result = authenticate_user_login('username4', 'password3', false, $reason);
+        $events = $sink->get_events();
+        $sink->close();
+        $event = array_pop($events);
+
         $this->assertFalse($result);
         $this->assertEquals(AUTH_LOGIN_NOUSER, $reason);
+        // Test Event.
+        $this->assertInstanceOf('\core\event\user_login_failed', $event);
+        $expectedlogdata = array(SITEID, 'login', 'error', 'index.php', 'username4');
+        $this->assertEventLegacyLogData($expectedlogdata, $event);
+        $eventdata = $event->get_data();
+        $this->assertSame($eventdata['other']['username'], 'username4');
+        $this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_NOUSER);
+        $this->assertEventContextNotUsed($event);
 
         set_config('lockoutthreshold', 3);
 
index 5bf603d..d89c9c7 100644 (file)
@@ -321,7 +321,7 @@ class behat_hooks extends behat_base {
 
         // All the run screenshots in the same parent dir.
         if (!$screenshotsdirname = self::get_run_screenshots_dir()) {
-            $screenshotsdirname = self::$screenshotsdirname = date('Ymd_Hi');
+            $screenshotsdirname = self::$screenshotsdirname = date('Ymd_His');
 
             $dir = $CFG->behat_screenshots_path . DIRECTORY_SEPARATOR . $screenshotsdirname;
 
diff --git a/lib/tests/code_test.php b/lib/tests/code_test.php
deleted file mode 100644 (file)
index b206567..0000000
+++ /dev/null
@@ -1,76 +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/>.
-
-/**
- * Code quality unit tests that are fast enough to run each time.
- *
- * @package    core
- * @category   phpunit
- * @copyright  &copy; 2006 The Open University
- * @author     T.J.Hunt@open.ac.uk
- * @license    http://www.gnu.org/copyleft/gpl.html GNU Public License
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-class core_code_testcase extends advanced_testcase {
-    protected $badstrings;
-    protected $extensions_to_ignore = array('exe', 'gif', 'ico', 'jpg', 'png', 'ttf', 'log');
-    protected $ignore_folders = array();
-
-    public function test_dnc() {
-        global $CFG;
-
-        if ($CFG->ostype === 'UNIX') {
-            // Try it the faster way.
-            $oldcwd = getcwd();
-            chdir($CFG->dirroot);
-            $output = null;
-            $exclude = array();
-            foreach ($this->extensions_to_ignore as $ext) {
-                $exclude[] = '--exclude="*.'.$ext.'"';
-            }
-            $exclude = implode(' ', $exclude);
-            exec('grep -r '.$exclude.' DONOT'.'COMMIT .', $output, $code);
-            chdir($oldcwd);
-            // Return code 0 means found, return code 1 means NOT found, 127 is grep not found.
-            if ($code == 1) {
-                // Executed only if no file failed the test.
-                $this->assertTrue(true);
-                return;
-            }
-        }
-
-        $regexp = '/\.(' . implode('|', $this->extensions_to_ignore) . ')$/';
-        $this->badstrings = array();
-        $this->badstrings['DONOT' . 'COMMIT'] = 'DONOT' . 'COMMIT'; // If we put the literal string here, it fails the test!
-        $this->badstrings['trailing whitespace'] = "[\t ][\r\n]";
-        foreach ($this->badstrings as $description => $ignored) {
-            $this->allok[$description] = true;
-        }
-        $this->recurseFolders($CFG->dirroot, 'search_file_for_dnc', $regexp, true);
-        $this->assertTrue(true); // Executed only if no file failed the test.
-    }
-
-    protected function search_file_for_dnc($filepath) {
-        $content = file_get_contents($filepath);
-        foreach ($this->badstrings as $description => $badstring) {
-            if (stripos($content, $badstring) !== false) {
-                $this->fail("File $filepath contains $description.");
-            }
-        }
-    }
-}
index 63d67d6..23d7770 100644 (file)
@@ -812,7 +812,7 @@ class core_completionlib_testcase extends advanced_testcase {
         $this->assertInstanceOf('\core\event\course_module_completion_updated', $event);
         $this->assertEquals($forum->cmid, $event->get_record_snapshot('course_modules_completion', $event->objectid)->coursemoduleid);
         $this->assertEquals($current, $event->get_record_snapshot('course_modules_completion', $event->objectid));
-        $this->assertEquals(context_module::instance($forum->id), $event->get_context());
+        $this->assertEquals(context_module::instance($forum->cmid), $event->get_context());
         $this->assertEquals($USER->id, $event->userid);
         $this->assertEquals($this->user->id, $event->other['relateduserid']);
         $this->assertInstanceOf('moodle_url', $event->get_url());
index 61b3b65..b4b8180 100644 (file)
@@ -370,16 +370,18 @@ class core_coursecatlib_testcase extends advanced_testcase {
      * Test the countall function
      */
     public function test_count_all() {
-        // There should be just the default category.
-        $this->assertEquals(1, coursecat::count_all());
+        global $DB;
+        // Dont assume there is just one. An add-on might create a category as part of the install.
+        $numcategories = $DB->count_records('course_categories');
+        $this->assertEquals($numcategories, coursecat::count_all());
         $category1 = coursecat::create(array('name' => 'Cat1'));
         $category2 = coursecat::create(array('name' => 'Cat2', 'parent' => $category1->id));
         $category3 = coursecat::create(array('name' => 'Cat3', 'parent' => $category2->id, 'visible' => 0));
-        // Now we've got four.
-        $this->assertEquals(4, coursecat::count_all());
+        // Now we've got three more.
+        $this->assertEquals($numcategories + 3, coursecat::count_all());
         cache_helper::purge_by_event('changesincoursecat');
         // We should still have 4.
-        $this->assertEquals(4, coursecat::count_all());
+        $this->assertEquals($numcategories + 3, coursecat::count_all());
     }
 
     /**
index 859173e..7e5579f 100644 (file)
@@ -75,6 +75,7 @@ class core_event_content_viewed_testcase extends advanced_testcase {
         $result = $sink->get_events();
         $event = $result[1];
         $this->assertEventLegacyLogData(null, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -89,6 +90,7 @@ class core_event_content_viewed_testcase extends advanced_testcase {
         $pageevent = \core_tests\event\content_viewed::create();
         $pageevent->set_page_detail();
         $pageevent->trigger();
+        $this->assertEventContextNotUsed($pageevent);
     }
 }
 
index b129c78..3789ab7 100644 (file)
@@ -57,6 +57,7 @@ class core_event_course_module_instance_list_viewed_testcase extends advanced_te
         $this->assertEventLegacyLogData($legacydata, $event);
         $url = new moodle_url('/mod/unittests/index.php', array('id' => $course->id));
         $this->assertEquals($url, $event->get_url());
+        $this->assertEventContextNotUsed($event);
 
     }
 
index 7977642..28e7bd1 100644 (file)
@@ -64,6 +64,7 @@ class core_event_course_module_viewed_testcase extends advanced_testcase {
         $this->assertSame('feedback', $event->objecttable);
         $url = new moodle_url('/mod/feedback/view.php', array('id' => $cm->id));
         $this->assertEquals($url, $event->get_url());
+        $this->assertEventContextNotUsed($event);
 
     }
 
index a85276d..713fc14 100644 (file)
@@ -749,4 +749,16 @@ class core_event_testcase extends advanced_testcase {
 
         $this->assertSame($event->get_data(), $data);
     }
+
+    /**
+     * @expectedException PHPUnit_Framework_Error_Notice
+     */
+    public function test_context_not_used() {
+        $event = \core_tests\event\context_used_in_event::create(array('courseid' => 1, 'other' => array('sample' => 1, 'xx' => 10)));
+        $this->assertEventContextNotUsed($event);
+
+        $eventcontext = phpunit_event_mock::testable_get_event_context($event);
+        phpunit_event_mock::testable_set_event_context($event, null);
+        $this->assertEventContextNotUsed($event);
+    }
 }
index e298bcc..cd54a1c 100644 (file)
@@ -51,6 +51,7 @@ class core_events_testcase extends advanced_testcase {
         $this->assertEquals(context_coursecat::instance($category->id), $event->get_context());
         $expected = array(SITEID, 'category', 'add', 'editcategory.php?id=' . $category->id, $category->id);
         $this->assertEventLegacyLogData($expected, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -139,6 +140,7 @@ class core_events_testcase extends advanced_testcase {
         $this->assertEquals(context_coursecat::instance($category2->id), $event->get_context());
         $expected = array(SITEID, 'category', 'show', 'editcategory.php?id=' . $category2->id, $category2->id);
         $this->assertEventLegacyLogData($expected, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -170,5 +172,6 @@ class core_events_testcase extends advanced_testcase {
         $this->assertEquals(context_system::instance(), $event->get_context());
         $expected = array(SITEID, 'library', 'mailer', qualified_me(), 'ERROR: The email failed to send!');
         $this->assertEventLegacyLogData($expected, $event);
+        $this->assertEventContextNotUsed($event);
     }
 }
index a516184..a72532a 100644 (file)
@@ -258,3 +258,29 @@ class course_module_viewed extends \core\event\course_module_viewed {
 class course_module_viewed_noinit extends \core\event\course_module_viewed {
 }
 
+/**
+ * Event to test context used in event functions
+ */
+class context_used_in_event extends \core\event\base {
+    public function get_description() {
+        return $this->context->instanceid . " Description";
+    }
+
+    protected function init() {
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+        $this->context = \context_system::instance();
+    }
+
+    public function get_url() {
+        return new \moodle_url('/somepath/somefile.php', array('id' => $this->context->instanceid));
+    }
+
+    protected function get_legacy_eventdata() {
+        return array($this->data['courseid'], $this->context->instanceid);
+    }
+
+    protected function get_legacy_logdata() {
+        return array($this->data['courseid'], 'core_unittest', 'view', 'unittest.php?id=' . $this->context->instanceid);
+    }
+}
diff --git a/lib/tests/lock_config_test.php b/lib/tests/lock_config_test.php
new file mode 100644 (file)
index 0000000..63aef07
--- /dev/null
@@ -0,0 +1,76 @@
+<?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/>.
+
+/**
+ * lock unit tests
+ *
+ * @package    core
+ * @category   lock
+ * @copyright  2013 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Unit tests for our locking configuration.
+ *
+ * @package    core
+ * @category   lock
+ * @copyright  2013 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class lock_config_testcase extends advanced_testcase {
+
+    /**
+     * Tests the static parse charset method
+     * @return void
+     */
+    public function test_lock_config() {
+        global $CFG;
+        $original = null;
+        if (isset($CFG->lock_factory)) {
+            $original = $CFG->lock_factory;
+        }
+
+        // Test no configuration.
+        unset($CFG->lock_factory);
+
+        $factory = \core\lock\lock_config::get_lock_factory('cache');
+
+        $this->assertNotEmpty($factory, 'Get a default factory with no configuration');
+
+        $CFG->lock_factory = '\core\lock\file_lock_factory';
+
+        $factory = \core\lock\lock_config::get_lock_factory('cache');
+        $this->assertTrue($factory instanceof \core\lock\file_lock_factory,
+                          'Get a default factory with a set configuration');
+
+        $CFG->lock_factory = '\core\lock\db_record_lock_factory';
+
+        $factory = \core\lock\lock_config::get_lock_factory('cache');
+        $this->assertTrue($factory instanceof \core\lock\db_record_lock_factory,
+                          'Get a default factory with a changed configuration');
+
+        if ($original) {
+            $CFG->lock_factory = $original;
+        } else {
+            unset($CFG->lock_factory);
+        }
+    }
+}
+
diff --git a/lib/tests/lock_test.php b/lib/tests/lock_test.php
new file mode 100644 (file)
index 0000000..17c8e57
--- /dev/null
@@ -0,0 +1,111 @@
+<?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/>.
+
+/**
+ * lock unit tests
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2013 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Unit tests for our locking implementations.
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2013 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class lock_testcase extends advanced_testcase {
+
+    /**
+     * Some lock types will store data in the database.
+     */
+    protected function setUp() {
+        $this->resetAfterTest(true);
+    }
+
+    /**
+     * Run a suite of tests on a lock factory.
+     * @param \core\lock\lock_factory $lockfactory - A lock factory to test
+     */
+    protected function run_on_lock_factory(\core\lock\lock_factory $lockfactory) {
+
+        if ($lockfactory->is_available()) {
+            // This should work.
+            $lock1 = $lockfactory->get_lock('abc', 2);
+            $this->assertNotEmpty($lock1, 'Get a lock');
+
+            if ($lockfactory->supports_timeout()) {
+                if ($lockfactory->supports_recursion()) {
+                    $lock2 = $lockfactory->get_lock('abc', 2);
+                    $this->assertNotEmpty($lock2, 'Get a stacked lock');
+                    $this->assertTrue($lock2->release(), 'Release a stacked lock');
+                } else {
+                    // This should timeout.
+                    $lock2 = $lockfactory->get_lock('abc', 2);
+                    $this->assertFalse($lock2, 'Cannot get a stacked lock');
+                }
+            }
+            // Release the lock.
+            $this->assertTrue($lock1->release(), 'Release a lock');
+            // Get it again.
+            $lock3 = $lockfactory->get_lock('abc', 2);
+
+            $this->assertNotEmpty($lock3, 'Get a lock again');
+            // Release the lock again.
+            $this->assertTrue($lock3->release(), 'Release a lock again');
+            // Release the lock again (shouldn't hurt).
+            $this->assertFalse($lock3->release(), 'Release a lock that is not held');
+            if (!$lockfactory->supports_auto_release()) {
+                // Test that a lock can be claimed after the timeout period.
+                $lock4 = $lockfactory->get_lock('abc', 2, 2);
+                $this->assertNotEmpty($lock4, 'Get a lock');
+                sleep(3);
+
+                $lock5 = $lockfactory->get_lock('abc', 2, 2);
+                $this->assertNotEmpty($lock5, 'Get another lock after a timeout');
+                $this->assertTrue($lock5->release(), 'Release the lock');
+                $this->assertTrue($lock4->release(), 'Release the lock');
+            }
+        }
+    }
+
+    /**
+     * Tests the testable lock factories.
+     * @return void
+     */
+    public function test_locks() {
+        // Run the suite on the current configured default (may be non-core).
+        $defaultfactory = \core\lock\lock_config::get_lock_factory('default');
+        $this->run_on_lock_factory($defaultfactory);
+
+        // Manually create the core no-configuration factories.
+        $dblockfactory = new \core\lock\db_record_lock_factory('test');
+        $this->run_on_lock_factory($dblockfactory);
+
+        $filelockfactory = new \core\lock\file_lock_factory('test');
+        $this->run_on_lock_factory($filelockfactory);
+
+    }
+
+}
+