Merge branch 'wip-MDL-48437_MASTER' of git://github.com/jason-platts/moodle
authorDavid Monllao <davidm@moodle.com>
Tue, 10 Mar 2015 01:56:21 +0000 (09:56 +0800)
committerDavid Monllao <davidm@moodle.com>
Tue, 10 Mar 2015 01:56:21 +0000 (09:56 +0800)
256 files changed:
.gitignore
.jshintrc
Gruntfile.js [new file with mode: 0644]
admin/index.php
admin/registration/confirmregistration.php
admin/registration/index.php
admin/registration/renderer.php
admin/settings/courses.php
admin/settings/security.php
admin/tool/langimport/classes/controller.php
admin/tool/task/classes/edit_scheduled_task_form.php
admin/tool/task/tests/behat/manage_tasks.feature [new file with mode: 0644]
admin/webservice/forms.php
auth/db/auth.php
auth/db/config.html
auth/shibboleth/auth.php
auth/shibboleth/config.html
availability/classes/info.php
availability/classes/tree.php
availability/classes/tree_node.php
availability/condition/group/classes/condition.php
availability/condition/grouping/classes/condition.php
availability/upgrade.txt
backup/moodle2/backup_activity_task.class.php
backup/moodle2/backup_course_task.class.php
backup/moodle2/backup_root_task.class.php
backup/moodle2/backup_settingslib.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_root_task.class.php
backup/moodle2/restore_settingslib.php
backup/moodle2/restore_stepslib.php
backup/util/dbops/backup_controller_dbops.class.php
badges/backpackconnect.php
badges/tests/events_test.php [new file with mode: 0644]
blocks/course_overview/lang/en/block_course_overview.php
blocks/tests/behat/configure_block_throughout_site.feature
calendar/lib.php
config-dist.php
course/edit.php
course/editsection.php
course/externallib.php
course/format/topics/tests/behat/edit_delete_sections.feature
course/format/weeks/tests/behat/edit_delete_sections.feature
enrol/renderer.php
enrol/self/edit_form.php
enrol/users.php
enrol/users_forms.php
filter/mathjaxloader/db/upgrade.php
filter/mathjaxloader/settings.php
filter/mathjaxloader/upgrade.txt
filter/mathjaxloader/version.php
filter/mathjaxloader/yui/build/moodle-filter_mathjaxloader-loader/moodle-filter_mathjaxloader-loader-debug.js
filter/mathjaxloader/yui/build/moodle-filter_mathjaxloader-loader/moodle-filter_mathjaxloader-loader-min.js
filter/mathjaxloader/yui/build/moodle-filter_mathjaxloader-loader/moodle-filter_mathjaxloader-loader.js
filter/mathjaxloader/yui/src/loader/js/loader.js
filter/urltolink/filter.php
filter/urltolink/tests/filter_test.php
grade/export/key.php
grade/export/keymanager.php
grade/grading/form/guide/edit_form.php
grade/grading/form/guide/guideeditor.php
grade/grading/form/guide/lang/en/gradingform_guide.php
grade/import/csv/classes/load_data.php
grade/import/csv/tests/load_data_test.php
grade/import/key.php
grade/import/keymanager.php
grade/lib.php
grade/report/outcomes/index.php
group/externallib.php
group/tests/externallib_test.php
lang/en/admin.php
lang/en/backup.php
lang/en/badges.php
lang/en/enrol.php
lang/en/filters.php
lang/en/form.php
lang/en/hub.php
lang/en/moodle.php
lang/en/repository.php
lang/en/role.php
lang/en/webservice.php
lib/accesslib.php
lib/amd/build/first.min.js [new file with mode: 0644]
lib/amd/src/first.js [new file with mode: 0644]
lib/badgeslib.php
lib/behat/classes/behat_selectors.php
lib/behat/classes/util.php
lib/blocklib.php
lib/classes/event/badge_awarded.php [new file with mode: 0644]
lib/classes/plugininfo/portfolio.php
lib/classes/requirejs.php [new file with mode: 0644]
lib/classes/session/memcached.php
lib/classes/task/manager.php
lib/componentlib.class.php
lib/configonlylib.php
lib/db/install.xml
lib/db/services.php
lib/db/tasks.php
lib/db/upgrade.php
lib/dml/oci_native_moodle_database.php
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
lib/editor/atto/yui/src/editor/js/editor.js
lib/external/externallib.php
lib/external/tests/external_test.php
lib/filestorage/zip_archive.php
lib/formslib.php
lib/grade/grade_category.php
lib/javascript-static.js
lib/medialib.php
lib/moodlelib.php
lib/outputrequirementslib.php
lib/requirejs.php [new file with mode: 0644]
lib/requirejs/LICENSE [new file with mode: 0644]
lib/requirejs/jquery-private.js [new file with mode: 0644]
lib/requirejs/moodle-config.js [new file with mode: 0644]
lib/requirejs/require.js [new file with mode: 0644]
lib/requirejs/require.min.js [new file with mode: 0644]
lib/setup.php
lib/tests/behat/behat_forms.php
lib/tests/blocklib_test.php
lib/tests/medialib_test.php
lib/tests/scheduled_task_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/weblib.php
login/token.php
mdeploy.php
mdeploytest.php
message/index.php
message/lib.php
message/tests/behat/manage_contacts.feature
message/tests/messagelib_test.php
mod/assign/backup/moodle2/restore_assign_stepslib.php
mod/assign/lang/en/assign.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/tests/behat/grading_status.feature
mod/assign/tests/lib_test.php
mod/assign/tests/locallib_test.php
mod/choice/backup/moodle2/backup_choice_stepslib.php
mod/choice/db/install.xml
mod/choice/db/upgrade.php
mod/choice/lang/en/choice.php
mod/choice/lib.php
mod/choice/mod_form.php
mod/choice/renderer.php
mod/choice/tests/behat/allow_preview.feature [new file with mode: 0644]
mod/choice/tests/behat/block_editing.feature [new file with mode: 0644]
mod/choice/tests/behat/my_home.feature [new file with mode: 0644]
mod/choice/upgrade.txt [new file with mode: 0644]
mod/choice/version.php
mod/choice/view.php
mod/forum/classes/post_form.php
mod/forum/lib.php
mod/forum/post.php
mod/forum/tests/behat/discussion_subscriptions.feature
mod/forum/tests/behat/forum_subscriptions_default.feature [new file with mode: 0644]
mod/forum/tests/externallib_test.php
mod/forum/tests/lib_test.php
mod/glossary/lib.php
mod/lesson/backup/moodle2/backup_lesson_stepslib.php
mod/lesson/backup/moodle2/restore_lesson_stepslib.php
mod/lesson/classes/event/content_page_viewed.php [new file with mode: 0644]
mod/lesson/classes/event/page_created.php [new file with mode: 0644]
mod/lesson/classes/event/page_deleted.php [new file with mode: 0644]
mod/lesson/classes/event/page_updated.php [new file with mode: 0644]
mod/lesson/classes/event/question_answered.php [new file with mode: 0644]
mod/lesson/classes/event/question_viewed.php [new file with mode: 0644]
mod/lesson/continue.php
mod/lesson/db/install.xml
mod/lesson/db/upgrade.php
mod/lesson/editpage.php
mod/lesson/essay.php
mod/lesson/lang/en/deprecated.txt [new file with mode: 0644]
mod/lesson/lang/en/lesson.php
mod/lesson/lib.php
mod/lesson/locallib.php
mod/lesson/mod_form.php
mod/lesson/pagetypes/branchtable.php
mod/lesson/pagetypes/essay.php
mod/lesson/pagetypes/matching.php
mod/lesson/pagetypes/multichoice.php
mod/lesson/pagetypes/numerical.php
mod/lesson/pagetypes/shortanswer.php
mod/lesson/pagetypes/truefalse.php
mod/lesson/tests/behat/completion_condition_end_reached.feature
mod/lesson/tests/behat/lesson_student_resume.feature [new file with mode: 0644]
mod/lesson/tests/behat/lesson_with_subcluster.feature [new file with mode: 0644]
mod/lesson/tests/behat/time_limit.feature
mod/lesson/tests/events_test.php
mod/lesson/tests/generator/lib.php
mod/lesson/tests/generator_test.php
mod/lesson/timer.js
mod/lesson/version.php
mod/lesson/view.php
mod/lti/lang/en/lti.php
mod/quiz/lib.php
mod/quiz/report/statistics/statistics_question_table.php
mod/quiz/tests/behat/attempt.feature [new file with mode: 0644]
mod/quiz/tests/behat/behat_mod_quiz.php
mod/quiz/tests/lib_test.php
mod/scorm/datamodels/scorm_12.js
mod/scorm/datamodels/scormlib.php
mod/scorm/locallib.php
mod/scorm/player.php
mod/workshop/lang/en/workshop.php
package.json [new file with mode: 0644]
portfolio/googledocs/lang/en/portfolio_googledocs.php
portfolio/picasa/lang/en/portfolio_picasa.php
question/behaviour/interactive/behaviour.php
question/type/match/backup/moodle2/restore_qtype_match_plugin.class.php
question/type/multianswer/lang/en/qtype_multianswer.php
question/type/multianswer/question.php
question/type/multianswer/renderer.php
question/type/multianswer/tests/walkthrough_test.php
question/type/multichoice/backup/moodle2/restore_qtype_multichoice_plugin.class.php
question/type/randomsamatch/backup/moodle2/restore_qtype_randomsamatch_plugin.class.php
question/type/shortanswer/backup/moodle2/restore_qtype_shortanswer_plugin.class.php
report/log/classes/table_log.php
report/security/lang/en/report_security.php
report/security/locallib.php
repository/googledocs/lang/en/repository_googledocs.php
repository/picasa/lang/en/repository_picasa.php
repository/s3/lang/en/repository_s3.php
repository/s3/lib.php
repository/s3/tests/generator/lib.php
tag/user.php
theme/base/style/calendar.css
theme/base/style/message.css
theme/bootstrapbase/less/moodle.less
theme/bootstrapbase/less/moodle/calendar.less
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/less/moodle/message.less
theme/bootstrapbase/less/moodle/question.less
theme/bootstrapbase/less/moodle/responsive.less
theme/bootstrapbase/less/moodle/undo.less
theme/bootstrapbase/style/moodle.css
theme/clean/lang/en/theme_clean.php
theme/more/lang/en/theme_more.php
theme/yui_combo.php
user/edit.php
user/externallib.php
user/filters/checkbox.php
user/filters/cohort.php
user/filters/courserole.php
user/filters/lib.php
user/filters/profilefield.php
user/filters/select.php
user/filters/text.php
user/lib.php
user/tests/behat/filter_idnumber.feature [new file with mode: 0644]
user/tests/externallib_test.php
version.php

index 8b976bb..89ea0d6 100644 (file)
@@ -36,3 +36,4 @@ composer.lock
 # lib/yuilib/version/module/module-coverage.js
 /lib/yuilib/*/*/*-coverage.js
 atlassian-ide-plugin.xml
+/node_modules/
index 9eff324..8b8a806 100644 (file)
--- a/.jshintrc
+++ b/.jshintrc
@@ -34,7 +34,8 @@
     "passfail":     false,
     "plusplus":     false,
     "predef": [
-        "M"
+        "M",
+        "define"
     ],
     "proto":        false,
     "regexdash":    false,
diff --git a/Gruntfile.js b/Gruntfile.js
new file mode 100644 (file)
index 0000000..4c45234
--- /dev/null
@@ -0,0 +1,149 @@
+// 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/>.
+
+/**
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Grunt configuration
+ */
+
+module.exports = function(grunt) {
+    var path = require('path'),
+        tasks = {};
+
+    // Project configuration.
+    grunt.initConfig({
+        jshint: {
+            options: {jshintrc: '.jshintrc'},
+            files: ['**/amd/src/*.js']
+        },
+        uglify: {
+            dynamic_mappings: {
+                files: grunt.file.expandMapping(
+                    ['**/src/*.js', '!**/node_modules/**'],
+                    '',
+                    {
+                        cwd: process.env.PWD,
+                        rename: function(destBase, destPath) {
+                            destPath = destPath.replace('src', 'build');
+                            destPath = destPath.replace('.js', '.min.js');
+                            destPath = path.resolve(process.env.PWD, destPath);
+                            return destPath;
+                        }
+                    }
+                )
+            }
+        }
+    });
+
+    tasks.shifter = function() {
+       var  exec = require('child_process').spawn,
+            done = this.async(),
+            args = [],
+            options = {
+                recursive: true,
+                watch: false,
+                walk: false,
+                module: false
+            },
+            shifter;
+
+            // Determine the most appropriate options to run with based upon the current location.
+            if (path.basename(process.env.PWD) === 'src') {
+                // Detect whether we're in a src directory.
+                grunt.log.debug('In a src directory');
+                args.push('--walk');
+                options.walk = true;
+            } else if (path.basename(path.dirname(process.env.PWD)) === 'src') {
+                // Detect whether we're in a module directory.
+                grunt.log.debug('In a module directory');
+                options.module = true;
+            }
+
+            if (grunt.option('watch')) {
+                if (!options.walk && !options.module) {
+                    grunt.fail.fatal('Unable to watch unless in a src or module directory');
+                }
+
+                // It is not advisable to run with recursivity and watch - this
+                // leads to building the build directory in a race-like fashion.
+                grunt.log.debug('Detected a watch - disabling recursivity');
+                options.recursive = false;
+                args.push('--watch');
+            }
+
+            if (options.recursive) {
+                args.push('--recursive');
+            }
+
+            // Always ignore the node_modules directory.
+            args.push('--excludes', 'node_modules');
+
+            // Add the stderr option if appropriate
+            if (grunt.option('verbose')) {
+                args.push('--lint-stderr');
+            }
+
+            // Actually run shifter.
+            shifter = exec(process.cwd() + '/node_modules/shifter/bin/shifter', args, {
+                cwd: process.env.PWD,
+                stdio: 'inherit',
+                env: process.env
+            });
+
+            // Tidy up after exec.
+            shifter.on('exit', function (code) {
+                if (code) {
+                    grunt.fail.fatal('Shifter failed with code: ' + code);
+                } else {
+                    grunt.log.ok('Shifter build complete.');
+                    done();
+                }
+            });
+    };
+
+    tasks.startup = function() {
+        // Are we in a YUI directory?
+        if (path.basename(path.resolve(process.env.PWD, '../../')) == 'yui') {
+            grunt.task.run('shifter');
+        // Are we in an AMD directory?
+        } else if (path.basename(process.env.PWD) == 'amd') {
+            grunt.task.run('jshint');
+            grunt.task.run('uglify');
+        } else {
+            // Run them all!.
+            grunt.task.run('shifter');
+            grunt.task.run('jshint');
+            grunt.task.run('uglify');
+        }
+    };
+
+
+    // Register NPM tasks.
+    grunt.loadNpmTasks('grunt-contrib-uglify');
+    grunt.loadNpmTasks('grunt-contrib-jshint');
+
+    // Register the shifter task.
+    grunt.registerTask('shifter', 'Run Shifter against the current directory', tasks.shifter);
+
+    // Register the startup task.
+    grunt.registerTask('startup', 'Run the correct tasks for the current directory', tasks.startup);
+
+    // Register the default task.
+    grunt.registerTask('default', ['startup']);
+};
index 8e68e44..8df4edf 100644 (file)
@@ -88,7 +88,6 @@ core_component::get_core_subsystems();
 require_once($CFG->libdir.'/adminlib.php');    // various admin-only functions
 require_once($CFG->libdir.'/upgradelib.php');  // general upgrade/install related functions
 
-$id             = optional_param('id', '', PARAM_TEXT);
 $confirmupgrade = optional_param('confirmupgrade', 0, PARAM_BOOL);
 $confirmrelease = optional_param('confirmrelease', 0, PARAM_BOOL);
 $confirmplugins = optional_param('confirmplugincheck', 0, PARAM_BOOL);
@@ -525,11 +524,6 @@ if (empty($site->shortname)) {
     redirect('upgradesettings.php?return=site');
 }
 
-// Check if we are returning from moodle.org registration and if so, we mark that fact to remove reminders
-if (!empty($id) and $id == $CFG->siteidentifier) {
-    set_config('registered', time());
-}
-
 // setup critical warnings before printing admin tree block
 $insecuredataroot = is_dataroot_insecure(true);
 $SESSION->admin_critical_warning = ($insecuredataroot==INSECURE_DATAROOT_ERROR);
index e7c8288..d1a4e8b 100644 (file)
@@ -65,7 +65,7 @@ if (!empty($registeredhub) and $registeredhub->token == $token) {
     $registeredhub->hubname = $hubname;
     $registrationmanager->update_registeredhub($registeredhub);
 
-    //display notficiation message
+    // Display notification message.
     $notificationmessage = $OUTPUT->notification(
             get_string('registrationconfirmedon', 'hub', $hublink), 'notifysuccess');
     echo $notificationmessage;
index 42f1c07..8b33014 100644 (file)
@@ -22,9 +22,9 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL
  * @copyright  (C) 1999 onwards Martin Dougiamas  http://dougiamas.com
  *
- * On this page the administrator selects which hub he wants to register,
- * except for MOOCH. Admins can register with MOOCH with the top admin menu "Registration" link.
- * On this page the administrator can also unregister from any hubs, including MOOCH.
+ * On this page the administrator selects which hub he wants to register (except for moodle.net)
+ * Admins can register with moodle.net via the site admin menu "Registration" link.
+ * On this page the administrator can also unregister from any hubs including moodle.net.
  */
 
 require('../../config.php');
index 3ab5b36..f3aba40 100644 (file)
@@ -36,13 +36,18 @@ class core_register_renderer extends plugin_renderer_base {
      * @return string
      */
     public function moodleorg_registration_message() {
-        $moodleorgurl = html_writer::link('http://moodle.org', 'Moodle.org');
-        $moodleorgstatsurl = html_writer::link('http://moodle.org/stats', get_string('statsmoodleorg', 'admin'));
-        $moochurl = html_writer::link(HUB_MOODLEORGHUBURL, get_string('moodleorghubname', 'admin'));
-        $moodleorgregmsg = get_string('registermoodleorg', 'admin', $moodleorgurl);
+
+        $moodleorgstatslink = html_writer::link('http://moodle.net/stats',
+                                               get_string('statsmoodleorg', 'admin'),
+                                               array('target' => '_blank'));
+
+        $hublink = html_writer::link('https://moodle.net/mod/page/view.php?id=1',
+                                      get_string('moodleorghubname', 'admin'),
+                                      array('target' => '_blank'));
+
+        $moodleorgregmsg = get_string('registermoodleorg', 'admin', $hublink);
         $items = array(get_string('registermoodleorgli1', 'admin'),
-            get_string('registermoodleorgli2', 'admin', $moodleorgstatsurl),
-            get_string('registermoodleorgli3', 'admin', $moochurl));
+                       get_string('registermoodleorgli2', 'admin', $moodleorgstatslink));
         $moodleorgregmsg .= html_writer::alist($items);
         return $moodleorgregmsg;
     }
index 9e379a4..58b92b0 100644 (file)
@@ -196,6 +196,10 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
     $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_logs', new lang_string('generallogs','backup'), new lang_string('configgenerallogs','backup'), array('value'=>0, 'locked'=>0)));
     $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_histories', new lang_string('generalhistories','backup'), new lang_string('configgeneralhistories','backup'), array('value'=>0, 'locked'=>0)));
     $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_questionbank', new lang_string('generalquestionbank','backup'), new lang_string('configgeneralquestionbank','backup'), array('value'=>1, 'locked'=>0)));
+    $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_groups',
+            new lang_string('generalgroups', 'backup'), new lang_string('configgeneralgroups', 'backup'),
+            array('value' => 1, 'locked' => 0)));
+
     $ADMIN->add('backups', $temp);
 
     // Create a page for general import configuration and defaults.
@@ -271,6 +275,8 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
     $temp->add(new admin_setting_configcheckbox('backup/backup_auto_logs', new lang_string('generallogs', 'backup'), new lang_string('configgenerallogs', 'backup'), 0));
     $temp->add(new admin_setting_configcheckbox('backup/backup_auto_histories', new lang_string('generalhistories','backup'), new lang_string('configgeneralhistories','backup'), 0));
     $temp->add(new admin_setting_configcheckbox('backup/backup_auto_questionbank', new lang_string('generalquestionbank','backup'), new lang_string('configgeneralquestionbank','backup'), 1));
+    $temp->add(new admin_setting_configcheckbox('backup/backup_auto_groups', new lang_string('generalgroups', 'backup'),
+            new lang_string('configgeneralgroups', 'backup'), 1));
 
     //$temp->add(new admin_setting_configcheckbox('backup/backup_auto_messages', new lang_string('messages', 'message'), new lang_string('backupmessageshelp','message'), 0));
     //$temp->add(new admin_setting_configcheckbox('backup/backup_auto_blogs', new lang_string('blogs', 'blog'), new lang_string('backupblogshelp','blog'), 0));
index af1e6c8..66cae6b 100644 (file)
@@ -55,7 +55,8 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
 
     $temp->add(new admin_setting_configcheckbox('profilesforenrolledusersonly', new lang_string('profilesforenrolledusersonly','admin'),new lang_string('configprofilesforenrolledusersonly', 'admin'),'1'));
 
-    $temp->add(new admin_setting_configcheckbox('cronclionly', new lang_string('cronclionly', 'admin'), new lang_string('configcronclionly', 'admin'), 0));
+    $temp->add(new admin_setting_configcheckbox('cronclionly', new lang_string('cronclionly', 'admin'), new lang_string
+            ('configcronclionly', 'admin'), 1));
     $temp->add(new admin_setting_configpasswordunmask('cronremotepassword', new lang_string('cronremotepassword', 'admin'), new lang_string('configcronremotepassword', 'admin'), ''));
 
     $options = array(0=>get_string('no'), 3=>3, 5=>5, 7=>7, 10=>10, 20=>20, 30=>30, 50=>50, 100=>100);
index 06f815e..d3eee58 100644 (file)
@@ -156,7 +156,7 @@ class controller {
         $updateablelangs = array();
         foreach ($currentlangs as $clang) {
             if (!array_key_exists($clang, $md5array)) {
-                $noticeok[] = get_string('langpackupdateskipped', 'tool_langimport', $clang);
+                $this->info[] = get_string('langpackupdateskipped', 'tool_langimport', $clang);
                 continue;
             }
             $dest1 = $CFG->dataroot.'/lang/'.$clang;
@@ -175,36 +175,10 @@ class controller {
             }
         }
 
-        // Clean-up currently installed versions of the packs.
-        foreach ($neededlangs as $packindex => $pack) {
-            if ($pack == 'en') {
-                continue;
-            }
-
-            // Delete old directories.
-            $dest1 = $CFG->dataroot.'/lang/'.$pack;
-            $dest2 = $CFG->dirroot.'/lang/'.$pack;
-            if (file_exists($dest1)) {
-                if (!remove_dir($dest1)) {
-                    $noticeerror[] = 'Could not delete old directory '.$dest1.', update of '.$pack
-                        .' failed, please check permissions.';
-                    unset($neededlangs[$packindex]);
-                    continue;
-                }
-            }
-            if (file_exists($dest2)) {
-                if (!remove_dir($dest2)) {
-                    $noticeerror[] = 'Could not delete old directory '.$dest2.', update of '.$pack
-                        .' failed, please check permissions.';
-                    unset($neededlangs[$packindex]);
-                    continue;
-                }
-            }
-        }
-
         try {
             $updated = $this->install_languagepacks($neededlangs, true);
         } catch (\moodle_exception $e) {
+            $this->errors[] = 'An exception occurred while installing language packs: ' . $e->getMessage();
             return false;
         }
 
index 21d2518..145b59d 100644 (file)
@@ -58,27 +58,22 @@ class tool_task_edit_scheduled_task_form extends moodleform {
         $mform->addElement('text', 'minute', get_string('taskscheduleminute', 'tool_task'));
         $mform->setType('minute', PARAM_RAW);
         $mform->addHelpButton('minute', 'taskscheduleminute', 'tool_task');
-        $mform->addRule('minute', get_string('required'), 'required');
 
         $mform->addElement('text', 'hour', get_string('taskschedulehour', 'tool_task'));
         $mform->setType('hour', PARAM_RAW);
         $mform->addHelpButton('hour', 'taskschedulehour', 'tool_task');
-        $mform->addRule('hour', get_string('required'), 'required');
 
         $mform->addElement('text', 'day', get_string('taskscheduleday', 'tool_task'));
         $mform->setType('day', PARAM_RAW);
         $mform->addHelpButton('day', 'taskscheduleday', 'tool_task');
-        $mform->addRule('day', get_string('required'), 'required');
 
         $mform->addElement('text', 'month', get_string('taskschedulemonth', 'tool_task'));
         $mform->setType('month', PARAM_RAW);
         $mform->addHelpButton('month', 'taskschedulemonth', 'tool_task');
-        $mform->addRule('month', get_string('required'), 'required');
 
         $mform->addElement('text', 'dayofweek', get_string('taskscheduledayofweek', 'tool_task'));
         $mform->setType('dayofweek', PARAM_RAW);
         $mform->addHelpButton('dayofweek', 'taskscheduledayofweek', 'tool_task');
-        $mform->addRule('dayofweek', get_string('required'), 'required');
 
         $mform->addElement('advcheckbox', 'disabled', get_string('disabled', 'tool_task'));
         $mform->addHelpButton('disabled', 'disabled', 'tool_task');
diff --git a/admin/tool/task/tests/behat/manage_tasks.feature b/admin/tool/task/tests/behat/manage_tasks.feature
new file mode 100644 (file)
index 0000000..73fc27d
--- /dev/null
@@ -0,0 +1,53 @@
+@tool @tool_task @javascript
+Feature: Manage scheduled tasks
+  In order to configure scheduled tasks
+  As an admin
+  I need to be able to disable, enable, edit and reset to default scheduled tasks
+
+  Background:
+    Given I log in as "admin"
+    And I navigate to "Scheduled tasks" node in "Site administration > Server"
+
+  Scenario: Disable scheduled task
+    When I click on "Edit task schedule: Log table cleanup" "link" in the "Log table cleanup" "table_row"
+    Then I should see "Edit task schedule: Log table cleanup"
+    And I set the following fields to these values:
+      | disabled             | 1 |
+    And I press "Save changes"
+    Then I should see "Changes saved"
+    And I should see "Task disabled" in the "Log table cleanup" "table_row"
+
+  Scenario: Enable scheduled task
+    When I click on "Edit task schedule: Log table cleanup" "link" in the "Log table cleanup" "table_row"
+    Then I should see "Edit task schedule: Log table cleanup"
+    And I set the following fields to these values:
+      | disabled             | 0 |
+    And I press "Save changes"
+    Then I should see "Changes saved"
+    And I should not see "Task disabled" in the "Log table cleanup" "table_row"
+
+  Scenario: Edit scheduled task
+    When I click on "Edit task schedule: Log table cleanup" "link" in the "Log table cleanup" "table_row"
+    Then I should see "Edit task schedule: Log table cleanup"
+    And I set the following fields to these values:
+      | minute               | */5 |
+      | hour                 | 1   |
+      | day                  | 2   |
+      | month                | 3   |
+      | dayofweek            | 4   |
+    And I press "Save changes"
+    Then I should see "Changes saved"
+    And the following should exist in the "admintable" table:
+      | Component    | Minute | Hour | Day | Day of week | Month |
+      | Standard log | */5    | 1    | 2   | 4           | 3     |
+
+  Scenario: Reset scheduled task to default
+    When I click on "Edit task schedule: Log table cleanup" "link" in the "Log table cleanup" "table_row"
+    Then I should see "Edit task schedule: Log table cleanup"
+    And I set the following fields to these values:
+      | resettodefaults      | 1   |
+    And I press "Save changes"
+    Then I should see "Changes saved"
+    And the following should not exist in the "admintable" table:
+      | Name               | Component    | Minute | Hour | Day | Day of week | Month |
+      | Log table cleanup  | Standard log | */5    | 1    | 2   | 4           | 3     |
\ No newline at end of file
index 9465bf4..88e543f 100644 (file)
@@ -157,6 +157,13 @@ class external_service_form extends moodleform {
 
         $errors = parent::validation($data, $files);
 
+        // Add field validation check for duplicate name.
+        if ($webservice = $DB->get_record('external_services', array('name' => $data['name']))) {
+            if (empty($data['id']) || $webservice->id != $data['id']) {
+                $errors['name'] = get_string('nameexists', 'webservice');
+            }
+        }
+
         // Add field validation check for duplicate shortname.
         // Allow duplicated "empty" shortnames.
         if (!empty($data['shortname'])) {
index 3feed3e..9121c6e 100644 (file)
@@ -168,7 +168,15 @@ class auth_plugin_db extends auth_plugin_base {
      */
     function db_attributes() {
         $moodleattributes = array();
-        foreach ($this->userfields as $field) {
+        // If we have custom fields then merge them with user fields.
+        $customfields = $this->get_custom_user_profile_fields();
+        if (!empty($customfields) && !empty($this->userfields)) {
+            $userfields = array_merge($this->userfields, $customfields);
+        } else {
+            $userfields = $this->userfields;
+        }
+
+        foreach ($userfields as $field) {
             if (!empty($this->config->{"field_map_$field"})) {
                 $moodleattributes[$field] = $this->config->{"field_map_$field"};
             }
@@ -210,7 +218,7 @@ class auth_plugin_db extends auth_plugin_base {
                     $fields_obj = $rs->FetchObj();
                     $fields_obj = (object)array_change_key_case((array)$fields_obj , CASE_LOWER);
                     foreach ($selectfields as $localname=>$externalname) {
-                        $result[$localname] = core_text::convert($fields_obj->{$localname}, $this->config->extencoding, 'utf-8');
+                        $result[$localname] = core_text::convert($fields_obj->{strtolower($localname)}, $this->config->extencoding, 'utf-8');
                      }
                  }
                  $rs->Close();
@@ -603,6 +611,10 @@ class auth_plugin_db extends auth_plugin_base {
                 continue;
             }
             $nuvalue = $newuser->$key;
+            // Support for textarea fields.
+            if (isset($nuvalue['text'])) {
+                $nuvalue = $nuvalue['text'];
+            }
             if ($nuvalue != $value) {
                 $update[] = $this->config->{"field_map_$key"}."='".$this->ext_addslashes(core_text::convert($nuvalue, 'utf-8', $this->config->extencoding))."'";
             }
index 331ad04..8d04d04 100644 (file)
 
 <?php
 
-print_auth_lock_options($this->authtype, $user_fields, get_string('auth_dbextrafields', 'auth_db'), true, true);
+print_auth_lock_options($this->authtype, $user_fields, get_string('auth_dbextrafields', 'auth_db'), true, true, $this->get_custom_user_profile_fields());
 
 ?>
 </table>
index 02b3529..ddfe96f 100644 (file)
@@ -143,7 +143,8 @@ class auth_plugin_shibboleth extends auth_plugin_base {
         $configarray = (array) $this->config;
 
         $moodleattributes = array();
-        foreach ($this->userfields as $field) {
+        $userfields = array_merge($this->userfields, $this->get_custom_user_profile_fields());
+        foreach ($userfields as $field) {
             if (isset($configarray["field_map_$field"])) {
                 $moodleattributes[$field] = $configarray["field_map_$field"];
             }
index 9a1854f..101be91 100644 (file)
@@ -138,7 +138,7 @@ urn:mace:organization2:providerID, Example Organization 2, /Shibboleth.sso/WAYF/
 
 <?php
 
-print_auth_lock_options($this->authtype, $user_fields, '<!-- empty help -->', true, false);
+print_auth_lock_options($this->authtype, $user_fields, '<!-- empty help -->', true, false, $this->get_custom_user_profile_fields());
 
 ?>
 </table>
index 95363c9..afd1d40 100644 (file)
@@ -311,17 +311,25 @@ abstract class info {
      * @param int $courseid Target course id
      * @param \base_logger $logger Logger for any warnings
      * @param int $dateoffset Date offset to be added to any dates (0 = none)
+     * @param \base_task $task Restore task
      */
-    public function update_after_restore($restoreid, $courseid, \base_logger $logger, $dateoffset) {
+    public function update_after_restore($restoreid, $courseid, \base_logger $logger,
+            $dateoffset, \base_task $task) {
         $tree = $this->get_availability_tree();
         // Set static data for use by get_restore_date_offset function.
-        self::$restoreinfo = array('restoreid' => $restoreid, 'dateoffset' => $dateoffset);
+        self::$restoreinfo = array('restoreid' => $restoreid, 'dateoffset' => $dateoffset,
+                'task' => $task);
         $changed = $tree->update_after_restore($restoreid, $courseid, $logger,
                 $this->get_thing_name());
         if ($changed) {
             // Save modified data.
-            $structure = $tree->save();
-            $this->set_in_database(json_encode($structure));
+            if ($tree->is_empty()) {
+                // If the tree is empty, but the tree has changed, remove this condition.
+                $this->set_in_database(null);
+            } else {
+                $structure = $tree->save();
+                $this->set_in_database(json_encode($structure));
+            }
         }
     }
 
@@ -343,6 +351,24 @@ abstract class info {
         return self::$restoreinfo['dateoffset'];
     }
 
+    /**
+     * Gets the restore task (specifically, the task that calls the
+     * update_after_restore method) for the current restore.
+     *
+     * @param string $restoreid Restore identifier
+     * @return \base_task Restore task
+     * @throws coding_exception If not in a restore (or not in that restore)
+     */
+    public static function get_restore_task($restoreid) {
+        if (!self::$restoreinfo) {
+            throw new coding_exception('Only valid during restore');
+        }
+        if (self::$restoreinfo['restoreid'] !== $restoreid) {
+            throw new coding_exception('Data not available for that restore id');
+        }
+        return self::$restoreinfo['task'];
+    }
+
     /**
      * Obtains the name of the item (cm_info or section_info, at present) that
      * this is controlling availability of. Name should be formatted ready
index e6383ed..8849320 100644 (file)
@@ -663,10 +663,18 @@ class tree extends tree_node {
     public function update_after_restore($restoreid, $courseid,
             \base_logger $logger, $name) {
         $changed = false;
-        foreach ($this->children as $child) {
-            $thischanged = $child->update_after_restore($restoreid, $courseid,
-                    $logger, $name);
-            $changed = $changed || $thischanged;
+        foreach ($this->children as $index => $child) {
+            if ($child->include_after_restore($restoreid, $courseid, $logger, $name,
+                    info::get_restore_task($restoreid))) {
+                $thischanged = $child->update_after_restore($restoreid, $courseid,
+                        $logger, $name);
+                $changed = $changed || $thischanged;
+            } else {
+                unset($this->children[$index]);
+                unset($this->showchildren[$index]);
+                $this->showchildren = array_values($this->showchildren);
+                $changed = true;
+            }
         }
         return $changed;
     }
index 0c3e0e4..2b1ddc0 100644 (file)
@@ -82,13 +82,34 @@ abstract class tree_node {
      */
     public abstract function save();
 
+    /**
+     * Checks whether this node should be included after restore or not. The
+     * node may be removed depending on restore settings, which you can get from
+     * the $task object.
+     *
+     * By default nodes are still included after restore.
+     *
+     * @param string $restoreid Restore ID
+     * @param int $courseid ID of target course
+     * @param \base_logger $logger Logger for any warnings
+     * @param string $name Name of this item (for use in warning messages)
+     * @param \base_task $task Current restore task
+     * @return bool True if there was any change
+     */
+    public function include_after_restore($restoreid, $courseid, \base_logger $logger, $name,
+            \base_task $task) {
+        return true;
+    }
+
     /**
      * Updates this node after restore, returning true if anything changed.
      * The default behaviour is simply to return false. If there is a problem
      * with the update, $logger can be used to output a warning.
      *
      * Note: If you need information about the date offset, call
-     * \core_availability\info::get_restore_date_offset($restoreid).
+     * \core_availability\info::get_restore_date_offset($restoreid). For
+     * information on the restoring task and its settings, call
+     * \core_availability\info::get_restore_task($restoreid).
      *
      * @param string $restoreid Restore ID
      * @param int $courseid ID of target course
index 2445cdc..46e3934 100644 (file)
@@ -127,6 +127,23 @@ class condition extends \core_availability\condition {
         return $this->groupid ? '#' . $this->groupid : 'any';
     }
 
+    /**
+     * Include this condition only if we are including groups in restore, or
+     * if it's a generic 'same activity' one.
+     *
+     * @param int $restoreid The restore Id.
+     * @param int $courseid The ID of the course.
+     * @param base_logger $logger The logger being used.
+     * @param string $name Name of item being restored.
+     * @param base_task $task The task being performed.
+     *
+     * @return Integer groupid
+     */
+    public function include_after_restore($restoreid, $courseid, \base_logger $logger,
+            $name, \base_task $task) {
+        return !$this->groupid || $task->get_setting_value('groups');
+    }
+
     public function update_after_restore($restoreid, $courseid, \base_logger $logger, $name) {
         global $DB;
         if (!$this->groupid) {
index e0b5997..1fdd928 100644 (file)
@@ -158,6 +158,23 @@ class condition extends \core_availability\condition {
         }
     }
 
+    /**
+     * Include this condition only if we are including groups in restore, or
+     * if it's a generic 'same activity' one.
+     *
+     * @param int $restoreid The restore Id.
+     * @param int $courseid The ID of the course.
+     * @param base_logger $logger The logger being used.
+     * @param string $name Name of item being restored.
+     * @param base_task $task The task being performed.
+     *
+     * @return Integer groupid
+     */
+    public function include_after_restore($restoreid, $courseid, \base_logger $logger,
+            $name, \base_task $task) {
+        return !$this->groupingid || $task->get_setting_value('groups');
+    }
+
     public function update_after_restore($restoreid, $courseid, \base_logger $logger, $name) {
         global $DB;
         if (!$this->groupingid) {
index 1d2dc46..92d3b8d 100644 (file)
@@ -2,6 +2,13 @@ This files describes API changes in /availability/*.
 
 The information here is intended only for developers.
 
+=== 2.9 ===
+
+* Condition plugins can now implement a new include_after_restore function to
+  indicate that they should be removed during the restore process. (This is
+  implemented so that group and grouping conditions are removed if groups are
+  not restored.)
+
 === 2.8 ===
 
 * There is a new API function in the info_module/info_section objects (and
index e39c858..8eac169 100644 (file)
@@ -137,8 +137,10 @@ abstract class backup_activity_task extends backup_task {
         // activity and from its related course_modules record and availability
         $this->add_step(new backup_module_structure_step('module_info', 'module.xml'));
 
-        // Annotate the groups used in already annotated groupings
-        $this->add_step(new backup_annotate_groups_from_groupings('annotate_groups'));
+        // Annotate the groups used in already annotated groupings if groups are to be backed up.
+        if ($this->get_setting_value('groups')) {
+            $this->add_step(new backup_annotate_groups_from_groupings('annotate_groups'));
+        }
 
         // Here we add all the common steps for any activity and, in the point of interest
         // we call to define_my_steps() is order to get the particular ones inserted in place.
index 5ed54e9..c7b64b4 100644 (file)
@@ -83,15 +83,18 @@ class backup_course_task extends backup_task {
         // Annotate enrolment custom fields.
         $this->add_step(new backup_enrolments_execution_step('annotate_enrol_custom_fields'));
 
-        // Annotate all the groups and groupings belonging to the course
-        $this->add_step(new backup_annotate_course_groups_and_groupings('annotate_course_groups'));
+        // Annotate all the groups and groupings belonging to the course. This can be optional.
+        if ($this->get_setting_value('groups')) {
+            $this->add_step(new backup_annotate_course_groups_and_groupings('annotate_course_groups'));
+        }
 
         // Annotate the groups used in already annotated groupings (note this may be
         // unnecessary now that we are annotating all the course groups and groupings in the
-        // step above. But we keep it working in case we decide, someday, to introduce one
-        // setting to transform the step above into an optional one. This is here to support
-        // course->defaultgroupingid
-        $this->add_step(new backup_annotate_groups_from_groupings('annotate_groups_from_groupings'));
+        // step above). This is here to support course->defaultgroupingid.
+        // This may not be required to annotate if groups are not being backed up.
+        if ($this->get_setting_value('groups')) {
+            $this->add_step(new backup_annotate_groups_from_groupings('annotate_groups_from_groupings'));
+        }
 
         // Annotate the question_categories belonging to the course context (conditionally).
         if ($this->get_setting_value('questionbank')) {
index 0a2df8a..45e792d 100644 (file)
@@ -159,5 +159,9 @@ class backup_root_task extends backup_task {
         $questionbank = new backup_generic_setting('questionbank', base_setting::IS_BOOLEAN, true);
         $questionbank->set_ui(new backup_setting_ui_checkbox($questionbank, get_string('rootsettingquestionbank', 'backup')));
         $this->add_setting($questionbank);
+
+        $groups = new backup_groups_setting('groups', base_setting::IS_BOOLEAN, true);
+        $groups->set_ui(new backup_setting_ui_checkbox($groups, get_string('rootsettinggroups', 'backup')));
+        $this->add_setting($groups);
     }
 }
index b3704ab..f799e4a 100644 (file)
@@ -65,6 +65,15 @@ class backup_filename_setting extends backup_generic_setting {
  */
 class backup_users_setting extends backup_generic_setting {}
 
+/**
+ * root setting to control if backup will include group information depends on @backup_users_setting
+ *
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2014 Matt Sammarco
+ */
+class backup_groups_setting extends backup_generic_setting {
+}
+
 /**
  * root setting to control if backup will include activities or no.
  * A lot of other settings (_included at activity levels)
index 78507d2..c761eba 100644 (file)
@@ -1149,8 +1149,10 @@ class backup_groups_structure_step extends backup_structure_step {
 
     protected function define_structure() {
 
-        // To know if we are including users
-        $users = $this->get_setting_value('users');
+        // To know if we are including users.
+        $userinfo = $this->get_setting_value('users');
+        // To know if we are including groups and groupings.
+        $groupinfo = $this->get_setting_value('groups');
 
         // Define each element separated
 
@@ -1190,27 +1192,29 @@ class backup_groups_structure_step extends backup_structure_step {
 
         // Define sources
 
-        $group->set_source_sql("
-            SELECT g.*
-              FROM {groups} g
-              JOIN {backup_ids_temp} bi ON g.id = bi.itemid
-             WHERE bi.backupid = ?
-               AND bi.itemname = 'groupfinal'", array(backup::VAR_BACKUPID));
-
-        // This only happens if we are including users
-        if ($users) {
-            $member->set_source_table('groups_members', array('groupid' => backup::VAR_PARENTID));
+        // This only happens if we are including groups/groupings.
+        if ($groupinfo) {
+            $group->set_source_sql("
+                SELECT g.*
+                  FROM {groups} g
+                  JOIN {backup_ids_temp} bi ON g.id = bi.itemid
+                 WHERE bi.backupid = ?
+                   AND bi.itemname = 'groupfinal'", array(backup::VAR_BACKUPID));
+
+            $grouping->set_source_sql("
+                SELECT g.*
+                  FROM {groupings} g
+                  JOIN {backup_ids_temp} bi ON g.id = bi.itemid
+                 WHERE bi.backupid = ?
+                   AND bi.itemname = 'groupingfinal'", array(backup::VAR_BACKUPID));
+            $groupinggroup->set_source_table('groupings_groups', array('groupingid' => backup::VAR_PARENTID));
+
+            // This only happens if we are including users.
+            if ($userinfo) {
+                $member->set_source_table('groups_members', array('groupid' => backup::VAR_PARENTID));
+            }
         }
 
-        $grouping->set_source_sql("
-            SELECT g.*
-              FROM {groupings} g
-              JOIN {backup_ids_temp} bi ON g.id = bi.itemid
-             WHERE bi.backupid = ?
-               AND bi.itemname = 'groupingfinal'", array(backup::VAR_BACKUPID));
-
-        $groupinggroup->set_source_table('groupings_groups', array('groupingid' => backup::VAR_PARENTID));
-
         // Define id annotations (as final)
 
         $member->annotate_ids('userfinal', 'userid');
@@ -1241,7 +1245,7 @@ class backup_users_structure_step extends backup_structure_step {
         // To know if we are including role assignments
         $roleassignments = $this->get_setting_value('role_assignments');
 
-        // Define each element separated
+        // Define each element separate.
 
         $users = new backup_nested_element('users');
 
index 73384df..922e203 100644 (file)
@@ -249,5 +249,22 @@ class restore_root_task extends restore_task {
         // The restore does not process the grade histories when some activities are ignored.
         // So let's define a dependency to prevent false expectations from our users.
         $activities->add_dependency($gradehistories);
+
+        // Define groups and groupings.
+        $defaultvalue = false;
+        $changeable = false;
+        if (isset($rootsettings['groups']) && $rootsettings['groups']) { // Only enabled when available.
+            $defaultvalue = true;
+            $changeable = true;
+        } else if (!isset($rootsettings['groups'])) {
+            // It is likely this is an older backup that does not contain information on the group setting,
+            // in which case groups should be restored and this setting can be changed.
+            $defaultvalue = true;
+            $changeable = true;
+        }
+        $groups = new restore_groups_setting('groups', base_setting::IS_BOOLEAN, $defaultvalue);
+        $groups->set_ui(new backup_setting_ui_checkbox($groups, get_string('rootsettinggroups', 'backup')));
+        $groups->get_ui()->set_changeable($changeable);
+        $this->add_setting($groups);
     }
 }
index 9568637..3ac0e2b 100644 (file)
@@ -43,6 +43,15 @@ class restore_generic_setting extends root_backup_setting {}
  */
 class restore_users_setting extends restore_generic_setting {}
 
+/**
+ * root setting to control if restore will create groups/grouping information. Depends on @restore_users_setting
+ *
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2014 Matt Sammarco
+ */
+class restore_groups_setting extends restore_generic_setting {
+}
+
 /**
  * root setting to control if restore will create role assignments
  * or no (any level), depends of @restore_users_setting
index d0b9a3a..d779cfa 100644 (file)
@@ -683,7 +683,7 @@ class restore_update_availability extends restore_execution_step {
             if (!is_null($section->availability)) {
                 $info = new \core_availability\info_section($section);
                 $info->update_after_restore($this->get_restoreid(),
-                        $this->get_courseid(), $this->get_logger(), $dateoffset);
+                        $this->get_courseid(), $this->get_logger(), $dateoffset, $this->task);
             }
         }
         $rs->close();
@@ -703,7 +703,7 @@ class restore_update_availability extends restore_execution_step {
             if (!is_null($cm->availability)) {
                 $info = new \core_availability\info_module($cm);
                 $info->update_after_restore($this->get_restoreid(),
-                        $this->get_courseid(), $this->get_logger(), $dateoffset);
+                        $this->get_courseid(), $this->get_logger(), $dateoffset, $this->task);
             }
         }
         $rs->close();
@@ -937,10 +937,13 @@ class restore_groups_structure_step extends restore_structure_step {
 
         $paths = array(); // Add paths here
 
-        $paths[] = new restore_path_element('group', '/groups/group');
-        $paths[] = new restore_path_element('grouping', '/groups/groupings/grouping');
-        $paths[] = new restore_path_element('grouping_group', '/groups/groupings/grouping/grouping_groups/grouping_group');
-
+        // Do not include group/groupings information if not requested.
+        $groupinfo = $this->get_setting_value('groups');
+        if ($groupinfo) {
+            $paths[] = new restore_path_element('group', '/groups/group');
+            $paths[] = new restore_path_element('grouping', '/groups/groupings/grouping');
+            $paths[] = new restore_path_element('grouping_group', '/groups/groupings/grouping/grouping_groups/grouping_group');
+        }
         return $paths;
     }
 
@@ -1069,7 +1072,7 @@ class restore_groups_members_structure_step extends restore_structure_step {
 
         $paths = array(); // Add paths here
 
-        if ($this->get_setting_value('users')) {
+        if ($this->get_setting_value('groups') && $this->get_setting_value('users')) {
             $paths[] = new restore_path_element('group', '/groups/group');
             $paths[] = new restore_path_element('member', '/groups/group/group_members/group_member');
         }
index e7456ce..06aa423 100644 (file)
@@ -574,7 +574,8 @@ abstract class backup_controller_dbops extends backup_dbops {
             'backup_auto_userscompletion'    => 'userscompletion',
             'backup_auto_logs'               => 'logs',
             'backup_auto_histories'          => 'grade_histories',
-            'backup_auto_questionbank'       => 'questionbank'
+            'backup_auto_questionbank'       => 'questionbank',
+            'backup_auto_groups'             => 'groups'
         );
         $plan = $controller->get_plan();
         foreach ($settings as $config => $settingname) {
@@ -612,7 +613,8 @@ abstract class backup_controller_dbops extends backup_dbops {
             'backup_general_userscompletion'    => 'userscompletion',
             'backup_general_logs'               => 'logs',
             'backup_general_histories'          => 'grade_histories',
-            'backup_general_questionbank'       => 'questionbank'
+            'backup_general_questionbank'       => 'questionbank',
+            'backup_general_groups'             => 'groups'
         );
         $plan = $controller->get_plan();
         foreach ($settings as $config=>$settingname) {
index e7616ec..382749a 100644 (file)
@@ -49,7 +49,7 @@ $assertion = filter_input(
 // Audience is the site url scheme + host + port only.
 $wwwparts = parse_url($CFG->wwwroot);
 $audience = $wwwparts['scheme'] . '://' . $wwwparts['host'];
-$audience .= isset($wwwparts['port']) ? $wwwparts['port'] : '';
+$audience .= isset($wwwparts['port']) ? ':' . $wwwparts['port'] : '';
 $params = 'assertion=' . urlencode($assertion) . '&audience=' .
            urlencode($audience);
 
diff --git a/badges/tests/events_test.php b/badges/tests/events_test.php
new file mode 100644 (file)
index 0000000..833cf09
--- /dev/null
@@ -0,0 +1,58 @@
+<?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/>.
+/**
+ * Badge events tests.
+ *
+ * @package    core_badges
+ * @copyright  2015 onwards Simey Lameze <simey@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+defined('MOODLE_INTERNAL') || die();
+global $CFG;
+require_once($CFG->dirroot . '/badges/tests/badgeslib_test.php');
+
+/**
+ * Badge events tests class.
+ *
+ * @package    core_badges
+ * @copyright  2015 onwards Simey Lameze <simey@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_badges_events_testcase extends core_badges_badgeslib_testcase {
+
+    /**
+     * Test badge awarded event.
+     */
+    public function test_badge_awarded() {
+
+        $systemcontext = context_system::instance();
+
+        $sink = $this->redirectEvents();
+
+        $badge = new badge($this->badgeid);
+        $badge->issue($this->user->id, true);
+        $badge->is_issued($this->user->id);
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+        $this->assertInstanceOf('\core\event\badge_awarded', $event);
+        $this->assertEquals($this->badgeid, $event->objectid);
+        $this->assertEquals($this->user->id, $event->relateduserid);
+        $this->assertEquals($systemcontext, $event->get_context());
+
+        $sink->close();
+    }
+}
\ No newline at end of file
index a78eaa7..ec9c063 100644 (file)
@@ -25,7 +25,7 @@
 $string['activityoverview'] = 'You have {$a}s that need attention';
 $string['alwaysshowall'] = 'Always show all';
 $string['collapseall'] = 'Collapse all course lists';
-$string['configotherexpanded'] = 'If enabled, other courses will be expanded by default unless overriden by user preferences.';
+$string['configotherexpanded'] = 'If enabled, other courses will be expanded by default unless overridden by user preferences.';
 $string['configpreservestates'] = 'If enabled, the collapsed/expanded states set by the user are stored and used on each load.';
 $string['course_overview:addinstance'] = 'Add a new course overview block';
 $string['course_overview:myaddinstance'] = 'Add a new course overview block to My home';
index b71303c..b0e0e57 100644 (file)
@@ -56,3 +56,16 @@ Feature: Add and configure blocks throughout the site
     And I follow "Course 1"
     And I follow "Turn editing on"
     Then I should see "Assign roles in Search forums block"
+
+  @javascript
+  Scenario: Blocks can safely be customised
+    Given I log in as "admin"
+    And I click on "My home" "link" in the "Navigation" "block"
+    And I press "Customise this page"
+    And I add the "HTML" block
+    And I configure the "(new HTML block)" block
+    And I set the following fields to these values:
+      | Block title | Foo " onload="document.getElementsByTagName('body')[0].remove()" alt="
+      | Content     | Example
+    When I press "Save changes"
+    Then I should see "Course overview"
index 4882914..ca52015 100644 (file)
@@ -2953,11 +2953,12 @@ function calendar_add_icalendar_event($event, $courseid, $subscriptionid, $timez
         $description = '';
     } else {
         $description = $event->properties['DESCRIPTION'][0]->value;
+        $description = clean_param($description, PARAM_NOTAGS);
         $description = str_replace('\n', '<br />', $description);
         $description = str_replace('\\', '', $description);
         $description = preg_replace('/\s+/', ' ', $description);
     }
-    $eventrecord->description = clean_param($description, PARAM_NOTAGS);
+    $eventrecord->description = $description;
 
     // Probably a repeating event with RRULE etc. TODO: skip for now.
     if (empty($event->properties['DTSTART'][0]->value)) {
index 4338d7e..fc59406 100644 (file)
@@ -242,7 +242,7 @@ $CFG->admin = 'admin';
 //      $CFG->session_memcached_save_path = '127.0.0.1:11211';
 //      $CFG->session_memcached_prefix = 'memc.sess.key.';
 //      $CFG->session_memcached_acquire_lock_timeout = 120;
-//      $CFG->session_memcached_lock_expire = 7200;       // Ignored if memcached extension <= 2.1.0
+//      $CFG->session_memcached_lock_expire = 7200;       // Ignored if PECL memcached is below version 2.2.0
 //
 //   Memcache session handler (requires memcached server and memcache extension):
 //      $CFG->session_handler_class = '\core\session\memcache';
@@ -507,7 +507,7 @@ $CFG->admin = 'admin';
 //      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\\db_record_lock_factory" - DB locking based on table rows.
 //
 // "\\core\\lock\\postgres_lock_factory" - DB locking based on postgres advisory locks.
 //
index bbe8992..6f41d19 100644 (file)
@@ -169,7 +169,7 @@ if ($editform->is_cancelled()) {
                 if ($plugin = enrol_get_plugin($instance->enrol)) {
                     if ($plugin->get_manual_enrol_link($instance)) {
                         // We know that the ajax enrol UI will have an option to enrol.
-                        $courseurl = new moodle_url('/enrol/users.php', array('id' => $course->id));
+                        $courseurl = new moodle_url('/enrol/users.php', array('id' => $course->id, 'newcourse' => 1));
                         break;
                     }
                 }
index b1e01db..76b49eb 100644 (file)
@@ -66,8 +66,8 @@ if ($deletesection) {
             echo $OUTPUT->box_start('noticebox');
             $optionsyes = array('id' => $id, 'confirm' => 1, 'delete' => 1, 'sesskey' => sesskey());
             $deleteurl = new moodle_url('/course/editsection.php', $optionsyes);
-            $formcontinue = new single_button($deleteurl, get_string('yes'));
-            $formcancel = new single_button($cancelurl, get_string('no'), 'get');
+            $formcontinue = new single_button($deleteurl, get_string('continue'));
+            $formcancel = new single_button($cancelurl, get_string('cancel'), 'get');
             echo $OUTPUT->confirm(get_string('confirmdeletesection', '',
                 get_section_name($course, $sectioninfo)), $formcontinue, $formcancel);
             echo $OUTPUT->box_end();
index ff3dbfa..0399d4f 100644 (file)
@@ -77,11 +77,14 @@ class core_course_external extends external_api {
         //retrieve the course
         $course = $DB->get_record('course', array('id' => $params['courseid']), '*', MUST_EXIST);
 
-        //check course format exist
-        if (!file_exists($CFG->dirroot . '/course/format/' . $course->format . '/lib.php')) {
-            throw new moodle_exception('cannotgetcoursecontents', 'webservice', '', null, get_string('courseformatnotfound', 'error', '', $course->format));
-        } else {
-            require_once($CFG->dirroot . '/course/format/' . $course->format . '/lib.php');
+        if ($course->id != SITEID) {
+            // Check course format exist.
+            if (!file_exists($CFG->dirroot . '/course/format/' . $course->format . '/lib.php')) {
+                throw new moodle_exception('cannotgetcoursecontents', 'webservice', '', null,
+                                            get_string('courseformatnotfound', 'error', $course->format));
+            } else {
+                require_once($CFG->dirroot . '/course/format/' . $course->format . '/lib.php');
+            }
         }
 
         // now security checks
index 81e8a25..8d29e64 100644 (file)
@@ -42,8 +42,8 @@ Feature: Sections can be edited and deleted in topics format
 
   Scenario: Deleting the last section in topics format
     When I click on "Delete topic" "link" in the "li#section-5" "css_element"
-    Then I should see "Are you absolutely sure you want to delete \"Topic 5\"? All activities will be also deleted"
-    And I press "Yes"
+    Then I should see "Are you absolutely sure you want to completely delete \"Topic 5\" and all the activities it contains?"
+    And I press "Continue"
     And I should not see "Topic 5"
     And I navigate to "Edit settings" node in "Course administration"
     And I expand all fieldsets
@@ -51,7 +51,7 @@ Feature: Sections can be edited and deleted in topics format
 
   Scenario: Deleting the middle section in topics format
     When I click on "Delete topic" "link" in the "li#section-4" "css_element"
-    And I press "Yes"
+    And I press "Continue"
     Then I should not see "Topic 5"
     And I should not see "Test chat name"
     And I should see "Test choice name" in the "li#section-4" "css_element"
@@ -63,7 +63,7 @@ Feature: Sections can be edited and deleted in topics format
     When I follow "Reduce the number of sections"
     Then I should see "Orphaned activities (section 5)" in the "li#section-5" "css_element"
     And I click on "Delete topic" "link" in the "li#section-5" "css_element"
-    And I press "Yes"
+    And I press "Continue"
     And I should not see "Topic 5"
     And I should not see "Orphaned activities"
     And "li#section-5" "css_element" should not exist
@@ -77,7 +77,7 @@ Feature: Sections can be edited and deleted in topics format
     And "li#section-5.orphaned" "css_element" should exist
     And "li#section-4.orphaned" "css_element" should not exist
     And I click on "Delete topic" "link" in the "li#section-1" "css_element"
-    And I press "Yes"
+    And I press "Continue"
     And I should not see "Test book name"
     And I should see "Orphaned activities (section 4)" in the "li#section-4" "css_element"
     And "li#section-5" "css_element" should not exist
index ae1524c..e477cf8 100644 (file)
@@ -44,8 +44,8 @@ Feature: Sections can be edited and deleted in weeks format
   Scenario: Deleting the last section in weeks format
     Given I should see "29 May - 4 June" in the "li#section-5" "css_element"
     When I click on "Delete week" "link" in the "li#section-5" "css_element"
-    Then I should see "Are you absolutely sure you want to delete \"29 May - 4 June\"? All activities will be also deleted"
-    And I press "Yes"
+    Then I should see "Are you absolutely sure you want to completely delete \"29 May - 4 June\" and all the activities it contains?"
+    And I press "Continue"
     And I should not see "29 May - 4 June"
     And I navigate to "Edit settings" node in "Course administration"
     And I expand all fieldsets
@@ -54,7 +54,7 @@ Feature: Sections can be edited and deleted in weeks format
   Scenario: Deleting the middle section in weeks format
     Given I should see "29 May - 4 June" in the "li#section-5" "css_element"
     When I click on "Delete week" "link" in the "li#section-4" "css_element"
-    And I press "Yes"
+    And I press "Continue"
     Then I should not see "29 May - 4 June"
     And I should not see "Test chat name"
     And I should see "Test choice name" in the "li#section-4" "css_element"
@@ -66,7 +66,7 @@ Feature: Sections can be edited and deleted in weeks format
     When I follow "Reduce the number of sections"
     Then I should see "Orphaned activities (section 5)" in the "li#section-5" "css_element"
     And I click on "Delete week" "link" in the "li#section-5" "css_element"
-    And I press "Yes"
+    And I press "Continue"
     And I should not see "29 May - 4 June"
     And I should not see "Orphaned activities"
     And "li#section-5" "css_element" should not exist
@@ -80,7 +80,7 @@ Feature: Sections can be edited and deleted in weeks format
     And "li#section-5.orphaned" "css_element" should exist
     And "li#section-4.orphaned" "css_element" should not exist
     And I click on "Delete week" "link" in the "li#section-1" "css_element"
-    And I press "Yes"
+    And I press "Continue"
     And I should not see "Test book name"
     And I should see "Orphaned activities (section 4)" in the "li#section-4" "css_element"
     And "li#section-5" "css_element" should not exist
index b1cd895..72b0cb2 100644 (file)
@@ -45,7 +45,7 @@ class core_enrol_renderer extends plugin_renderer_base {
         $buttons = $table->get_manual_enrol_buttons();
         $buttonhtml = '';
         if (count($buttons) > 0) {
-            $buttonhtml .= html_writer::start_tag('div', array('class' => 'enrol_user_buttons'));
+            $buttonhtml .= html_writer::start_tag('div', array('class' => 'enrol_user_buttons enrol-users-page-action'));
             foreach ($buttons as $button) {
                 $buttonhtml .= $this->render($button);
             }
index a1e3a80..b035bd3 100644 (file)
@@ -39,8 +39,10 @@ class enrol_self_edit_form extends moodleform {
 
         $mform->addElement('header', 'header', get_string('pluginname', 'enrol_self'));
 
-        $mform->addElement('text', 'name', get_string('custominstancename', 'enrol'));
+        $nameattribs = array('size' => '20', 'maxlength' => '255');
+        $mform->addElement('text', 'name', get_string('custominstancename', 'enrol'), $nameattribs);
         $mform->setType('name', PARAM_TEXT);
+        $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'server');
 
         $options = array(ENROL_INSTANCE_ENABLED  => get_string('yes'),
                          ENROL_INSTANCE_DISABLED => get_string('no'));
@@ -52,11 +54,13 @@ class enrol_self_edit_form extends moodleform {
         $mform->addHelpButton('customint6', 'newenrols', 'enrol_self');
         $mform->disabledIf('customint6', 'status', 'eq', ENROL_INSTANCE_DISABLED);
 
-        $mform->addElement('passwordunmask', 'password', get_string('password', 'enrol_self'));
+        $passattribs = array('size' => '20', 'maxlength' => '50');
+        $mform->addElement('passwordunmask', 'password', get_string('password', 'enrol_self'), $passattribs);
         $mform->addHelpButton('password', 'password', 'enrol_self');
         if (empty($instance->id) and $plugin->get_config('requirepassword')) {
             $mform->addRule('password', get_string('required'), 'required', null, 'client');
         }
+        $mform->addRule('password', get_string('maximumchars', '', 50), 'maxlength', 50, 'server');
 
         $options = array(1 => get_string('yes'),
                          0 => get_string('no'));
index 99e5133..ac2b68d 100644 (file)
@@ -35,10 +35,11 @@ $search  = optional_param('search', '', PARAM_RAW);
 $role    = optional_param('role', 0, PARAM_INT);
 $fgroup  = optional_param('filtergroup', 0, PARAM_INT);
 $status  = optional_param('status', -1, PARAM_INT);
+$newcourse = optional_param('newcourse', false, PARAM_BOOL);
 
 // When users reset the form, redirect back to first page without other params.
 if (optional_param('resetbutton', '', PARAM_RAW) !== '') {
-    redirect('users.php?id=' . $id);
+    redirect('users.php?id=' . $id . '&newcourse=' . $newcourse);
 }
 
 $course = $DB->get_record('course', array('id'=>$id), '*', MUST_EXIST);
@@ -54,7 +55,7 @@ $PAGE->set_pagelayout('admin');
 
 $manager = new course_enrolment_manager($PAGE, $course, $filter, $role, $search, $fgroup, $status);
 $table = new course_enrolment_users_table($manager, $PAGE);
-$PAGE->set_url('/enrol/users.php', $manager->get_url_params()+$table->get_url_params());
+$PAGE->set_url('/enrol/users.php', $manager->get_url_params()+$table->get_url_params()+array('newcourse' => $newcourse));
 navigation_node::override_active_url(new moodle_url('/enrol/users.php', array('id' => $id)));
 
 // Check if there is an action to take
@@ -217,7 +218,7 @@ if (!has_capability('moodle/course:viewhiddenuserfields', $context)) {
     }
 }
 
-$filterform = new enrol_users_filter_form('users.php', array('manager' => $manager, 'id' => $id),
+$filterform = new enrol_users_filter_form('users.php', array('manager' => $manager, 'id' => $id, 'newcourse' => $newcourse),
         'get', '', array('id' => 'filterform'));
 $filterform->set_data(array('search' => $search, 'ifilter' => $filter, 'role' => $role, 'filtergroup' => $fgroup));
 
@@ -240,4 +241,8 @@ $PAGE->set_heading($PAGE->title);
 echo $OUTPUT->header();
 echo $OUTPUT->heading(get_string('enrolledusers', 'enrol'));
 echo $renderer->render_course_enrolment_users_table($table, $filterform);
+if ($newcourse == 1) {
+    echo $OUTPUT->single_button(new moodle_url('/course/view.php', array('id' => $id)),
+    get_string('proceedtocourse', 'enrol'), 'GET', array('class' => 'enrol-users-page-action'));
+}
 echo $OUTPUT->footer();
index a854d80..ab55530 100644 (file)
@@ -190,5 +190,7 @@ class enrol_users_filter_form extends moodleform {
         // Add hidden fields required by page.
         $mform->addElement('hidden', 'id', $this->_customdata['id']);
         $mform->setType('id', PARAM_INT);
+        $mform->addElement('hidden', 'newcourse', $this->_customdata['newcourse']);
+        $mform->setType('newcourse', PARAM_BOOL);
     }
 }
index acab499..34f27b4 100644 (file)
@@ -47,5 +47,64 @@ function xmldb_filter_mathjaxloader_upgrade($oldversion) {
     // Moodle v2.8.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2015021200) {
+
+        $httpurl = get_config('filter_mathjaxloader', 'httpurl');
+        // Don't change the config if it has been manually changed to something besides the default setting value.
+        if ($httpurl === "http://cdn.mathjax.org/mathjax/2.3-latest/MathJax.js") {
+            set_config('httpurl', 'http://cdn.mathjax.org/mathjax/2.5-latest/MathJax.js', 'filter_mathjaxloader');
+        }
+
+        $httpsurl = get_config('filter_mathjaxloader', 'httpsurl');
+        // Don't change the config if it has been manually changed to something besides the default setting value.
+        if ($httpsurl === "https://cdn.mathjax.org/mathjax/2.3-latest/MathJax.js") {
+            set_config('httpsurl', 'https://cdn.mathjax.org/mathjax/2.5-latest/MathJax.js', 'filter_mathjaxloader');
+        }
+
+        upgrade_plugin_savepoint(true, 2015021200, 'filter', 'mathjaxloader');
+    }
+
+    if ($oldversion < 2015021700) {
+
+        $oldconfig = get_config('filter_mathjaxloader', 'mathjaxconfig');
+        $olddefault = 'MathJax.Hub.Config({
+    config: ["MMLorHTML.js", "Safe.js"],
+    jax: ["input/TeX","input/MathML","output/HTML-CSS","output/NativeMML"],
+    extensions: ["tex2jax.js","mml2jax.js","MathMenu.js","MathZoom.js"],
+    TeX: {
+        extensions: ["AMSmath.js","AMSsymbols.js","noErrors.js","noUndefined.js"]
+    },
+    menuSettings: {
+        zoom: "Double-Click",
+        mpContext: true,
+        mpMouse: true
+    },
+    errorSettings: { message: ["!"] },
+    skipStartupTypeset: true,
+    messageStyle: "none"
+});
+';
+        $newdefault = '
+MathJax.Hub.Config({
+    config: ["Accessible.js", "Safe.js"],
+    errorSettings: { message: ["!"] },
+    skipStartupTypeset: true,
+    messageStyle: "none"
+});
+';
+
+        // Ignore white space changes.
+        $oldconfig = trim(preg_replace('/\s+/', ' ', $oldconfig));
+        $olddefault = trim(preg_replace('/\s+/', ' ', $olddefault));
+
+        // Update the default config for mathjax only if it has not been customised.
+
+        if ($oldconfig == $olddefault) {
+            set_config('mathjaxconfig', $newdefault, 'filter_mathjaxloader');
+        }
+
+        upgrade_plugin_savepoint(true, 2015021700, 'filter', 'mathjaxloader');
+    }
+
     return true;
 }
index 978587c..2c681f0 100644 (file)
@@ -33,14 +33,14 @@ if ($ADMIN->fulltree) {
     $item = new admin_setting_configtext('filter_mathjaxloader/httpurl',
                                          new lang_string('httpurl', 'filter_mathjaxloader'),
                                          new lang_string('httpurl_help', 'filter_mathjaxloader'),
-                                         'http://cdn.mathjax.org/mathjax/2.3-latest/MathJax.js',
+                                         'http://cdn.mathjax.org/mathjax/2.5-latest/MathJax.js',
                                          PARAM_RAW);
     $settings->add($item);
 
     $item = new admin_setting_configtext('filter_mathjaxloader/httpsurl',
                                          new lang_string('httpsurl', 'filter_mathjaxloader'),
                                          new lang_string('httpsurl_help', 'filter_mathjaxloader'),
-                                         'https://cdn.mathjax.org/mathjax/2.3-latest/MathJax.js',
+                                         'https://cdn.mathjax.org/mathjax/2.5-latest/MathJax.js',
                                          PARAM_RAW);
     $settings->add($item);
 
@@ -52,17 +52,7 @@ if ($ADMIN->fulltree) {
 
     $default = '
 MathJax.Hub.Config({
-    config: ["MMLorHTML.js", "Safe.js"],
-    jax: ["input/TeX","input/MathML","output/HTML-CSS","output/NativeMML"],
-    extensions: ["tex2jax.js","mml2jax.js","MathMenu.js","MathZoom.js"],
-    TeX: {
-        extensions: ["AMSmath.js","AMSsymbols.js","noErrors.js","noUndefined.js"]
-    },
-    menuSettings: {
-        zoom: "Double-Click",
-        mpContext: true,
-        mpMouse: true
-    },
+    config: ["Accessible.js", "Safe.js"],
     errorSettings: { message: ["!"] },
     skipStartupTypeset: true,
     messageStyle: "none"
index ca95016..87db75e 100644 (file)
@@ -1,3 +1,14 @@
+=== 2.9 ===
+
+* Update to the latest version of MathJax setting "httpurl" and "httpsurl" to:
+  http://cdn.mathjax.org/mathjax/2.5-latest/MathJax.js
+
+  and
+
+  https://cdn.mathjax.org/mathjax/2.5-latest/MathJax.js
+
+=== Before 2.9 ===
+
 Setting "httpsurl" default changed from:
 
 https://c328740.ssl.cf1.rackcdn.com/mathjax/2.3-latest/MathJax.js
index b825781..a47e1cd 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version  = 2014111000;
+$plugin->version  = 2015021700;
 $plugin->requires = 2014110400;  // Requires this Moodle version
 $plugin->component= 'filter_mathjaxloader';
index 11b4a9d..83b6d8d 100644 (file)
Binary files a/filter/mathjaxloader/yui/build/moodle-filter_mathjaxloader-loader/moodle-filter_mathjaxloader-loader-debug.js and b/filter/mathjaxloader/yui/build/moodle-filter_mathjaxloader-loader/moodle-filter_mathjaxloader-loader-debug.js differ
index ccebf0c..809018e 100644 (file)
Binary files a/filter/mathjaxloader/yui/build/moodle-filter_mathjaxloader-loader/moodle-filter_mathjaxloader-loader-min.js and b/filter/mathjaxloader/yui/build/moodle-filter_mathjaxloader-loader/moodle-filter_mathjaxloader-loader-min.js differ
index 11b4a9d..83b6d8d 100644 (file)
Binary files a/filter/mathjaxloader/yui/build/moodle-filter_mathjaxloader-loader/moodle-filter_mathjaxloader-loader.js and b/filter/mathjaxloader/yui/build/moodle-filter_mathjaxloader-loader/moodle-filter_mathjaxloader-loader.js differ
index aad01b3..835a864 100644 (file)
@@ -111,14 +111,20 @@ M.filter_mathjaxloader = M.filter_mathjaxloader || {
     contentUpdated: function(event) {
         var self = this;
         Y.use('mathjax', function() {
+            if (typeof window.MathJax === "undefined") {
+                return;
+            }
+            var processdelay = window.MathJax.Hub.processSectionDelay;
+            // Set the process section delay to 0 when updating the formula.
+            window.MathJax.Hub.processSectionDelay = 0;
             self._setLocale();
             event.nodes.each(function (node) {
                 node.all('.filter_mathjaxloader_equation').each(function(node) {
-                    if (typeof window.MathJax !== "undefined") {
-                        window.MathJax.Hub.Queue(["Typeset", window.MathJax.Hub, node.getDOMNode()]);
-                    }
+                    window.MathJax.Hub.Queue(["Typeset", window.MathJax.Hub, node.getDOMNode()]);
                 });
             });
+            // Set the delay back to normal after processing.
+            window.MathJax.Hub.processSectionDelay = processdelay;
         });
     }
 };
index 538d03e..354dbe1 100644 (file)
@@ -134,10 +134,9 @@ class filter_urltolink extends moodle_text_filter {
 
         // Lookbehind assertions.
         // Is not HTML attribute or CSS URL property. Unfortunately legit text like "url(http://...)" will not be a link.
-        $lookbehindstart = "(?<!=[\"']|\burl\([\"' ]|\burl\()";
         $lookbehindend = "(?<![]),.;])";
 
-        $regex = "$lookbehindstart$urlstart((?:$domainsegment\.)+$domainsegment|$numericip)" .
+        $regex = "$urlstart((?:$domainsegment\.)+$domainsegment|$numericip)" .
                 "($port?$path$querystring?$fragment?)$lookbehindend";
         if ($unicoderegexp) {
             $regex = '#' . $regex . '#ui';
@@ -145,7 +144,38 @@ class filter_urltolink extends moodle_text_filter {
             $regex = '#' . preg_replace(array('\pLl', '\PL'), 'a-z', $regex) . '#i';
         }
 
-        $text = preg_replace($regex, '<a href="http$1://$2$3$4" class="_blanktarget">$0</a>', $text);
+        // Locate any HTML tags.
+        $matches = preg_split('/(<[^<|>]*>)/i', $text, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
+
+        // Iterate through the tokenized text to handle chunks (html and content).
+        foreach ($matches as $idx => $chunk) {
+            // Nothing to do. We skip completely any html chunk.
+            if (strpos(trim($chunk), '<') === 0) {
+                continue;
+            }
+
+            // Nothing to do. We skip any content chunk having any of these attributes.
+            if (preg_match('#(background=")|(action=")|(style="background)|(href=")|(src=")|(url [(])#', $chunk)) {
+                continue;
+            }
+
+            // Arrived here, we want to process every word in this chunk.
+            $text = $chunk;
+            $words = explode(' ', $text);
+
+            foreach ($words as $idx2 => $word) {
+                // ReDoS protection. Stop processing if a word is too large.
+                if (strlen($word) < 4096) {
+                    $words[$idx2] = preg_replace($regex, '<a href="http$1://$2$3$4" class="_blanktarget">$0</a>', $word);
+                }
+            }
+            $text = implode(' ', $words);
+
+            // Copy the result back to the array.
+            $matches[$idx] = $text;
+        }
+
+        $text = implode('', $matches);
 
         if (!empty($ignoretags)) {
             $ignoretags = array_reverse($ignoretags); /// Reversed so "progressive" str_replace() will solve some nesting problems.
index 58bd4a3..0ae7f89 100644 (file)
@@ -29,9 +29,13 @@ global $CFG;
 require_once($CFG->dirroot . '/filter/urltolink/filter.php'); // Include the code to test
 
 
-class filter_urltolink_testcase extends basic_testcase {
+class filter_urltolink_filter_testcase extends basic_testcase {
 
     function get_convert_urls_into_links_test_cases() {
+        // Create a 4095 and 4096 long URLs.
+        $superlong4095 = str_pad('http://www.superlong4095.com?this=something', 4095, 'a');
+        $superlong4096 = str_pad('http://www.superlong4096.com?this=something', 4096, 'a');
+
         $texts = array (
             //just a url
             'http://moodle.org - URL' => '<a href="http://moodle.org" class="_blanktarget">http://moodle.org</a> - URL',
@@ -130,6 +134,7 @@ class filter_urltolink_testcase extends basic_testcase {
             '<td background="http://moodle.org">&nbsp;</td>' => '<td background="http://moodle.org">&nbsp;</td>',
             '<td background="www.moodle.org">&nbsp;</td>' => '<td background="www.moodle.org">&nbsp;</td>',
             '<form name="input" action="http://moodle.org/submit.asp" method="get">'=>'<form name="input" action="http://moodle.org/submit.asp" method="get">',
+            '<input type="submit" value="Go to http://moodle.org">' => '<input type="submit" value="Go to http://moodle.org">',
             '<td background="https://www.moodle.org">&nbsp;</td>' => '<td background="https://www.moodle.org">&nbsp;</td>',
             // CSS URLs.
             '<table style="background-image: url(\'http://moodle.org/pic.jpg\');">' => '<table style="background-image: url(\'http://moodle.org/pic.jpg\');">',
@@ -148,6 +153,27 @@ class filter_urltolink_testcase extends basic_testcase {
             //Encoded URLs in the query
             'URL: http://127.0.0.1/path/to?param=value_with%28parenthesis%29&param2=1' => 'URL: <a href="http://127.0.0.1/path/to?param=value_with%28parenthesis%29&param2=1" class="_blanktarget">http://127.0.0.1/path/to?param=value_with%28parenthesis%29&param2=1</a>',
             'URL: www.localhost.com/path/to?param=value_with%28parenthesis%29&param2=1' => 'URL: <a href="http://www.localhost.com/path/to?param=value_with%28parenthesis%29&param2=1" class="_blanktarget">www.localhost.com/path/to?param=value_with%28parenthesis%29&param2=1</a>',
+            // Test URL less than 4096 characters in size is converted to link.
+            'URL: ' . $superlong4095 => 'URL: <a href="' . $superlong4095 . '" class="_blanktarget">' . $superlong4095 . '</a>',
+            // Test URL equal to or greater than 4096 characters in size is not converted to link.
+            'URL: ' . $superlong4096 => 'URL: ' . $superlong4096,
+            // Testing URL within a span tag.
+            'URL: <span style="kasd"> my link to http://google.com </span>' => 'URL: <span style="kasd"> my link to <a href="http://google.com" class="_blanktarget">http://google.com</a> </span>',
+            // Nested tags test.
+            '<b><i>www.google.com</i></b>' => '<b><i><a href="http://www.google.com" class="_blanktarget">www.google.com</a></i></b>',
+            '<input type="submit" value="Go to http://moodle.org">' => '<input type="submit" value="Go to http://moodle.org">',
+            // Test realistic content.
+            '<p><span style="color: rgb(37, 37, 37); font-family: sans-serif; line-height: 22.3999996185303px;">Lorem ipsum amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut http://google.com aliquip ex ea <a href="http://google.com">commodo consequat</a>. Duis aute irure in reprehenderit in excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia https://docs.google.com/document/d/BrokenLinkPleaseAyacDHc_Ov8aoskoSVQsfmLHP_jYAkRMk/edit?usp=sharing https://docs.google.com/document/d/BrokenLinkPleaseAyacDHc_Ov8aoskoSVQsfmLHP_jYAkRMk/edit?usp=sharing mollit anim id est laborum.</span><br></p>'
+            =>
+            '<p><span style="color: rgb(37, 37, 37); font-family: sans-serif; line-height: 22.3999996185303px;">Lorem ipsum amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut <a href="http://google.com" class="_blanktarget">http://google.com</a> aliquip ex ea <a href="http://google.com">commodo consequat</a>. Duis aute irure in reprehenderit in excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia <a href="https://docs.google.com/document/d/BrokenLinkPleaseAyacDHc_Ov8aoskoSVQsfmLHP_jYAkRMk/edit?usp=sharing" class="_blanktarget">https://docs.google.com/document/d/BrokenLinkPleaseAyacDHc_Ov8aoskoSVQsfmLHP_jYAkRMk/edit?usp=sharing</a> <a href="https://docs.google.com/document/d/BrokenLinkPleaseAyacDHc_Ov8aoskoSVQsfmLHP_jYAkRMk/edit?usp=sharing" class="_blanktarget">https://docs.google.com/document/d/BrokenLinkPleaseAyacDHc_Ov8aoskoSVQsfmLHP_jYAkRMk/edit?usp=sharing</a> mollit anim id est laborum.</span><br></p>',
+            // Test some broken html.
+            '5 < 10 www.google.com <a href="hi.com">im a link</a>' => '5 < 10 <a href="http://www.google.com" class="_blanktarget">www.google.com</a> <a href="hi.com">im a link</a>',
+            'h3 (www.styles.com/h3) < h1 (www.styles.com/h1)' => 'h3 (<a href="http://www.styles.com/h3" class="_blanktarget">www.styles.com/h3</a>) < h1 (<a href="http://www.styles.com/h1" class="_blanktarget">www.styles.com/h1</a>)',
+            '<p>text www.moodle.org&lt;/p> text' => '<p>text <a href="http://www.moodle.org" class="_blanktarget">www.moodle.org</a>&lt;/p> text',
+            // Some more urls.
+            '<link rel="search" type="application/opensearchdescription+xml" href="/osd.jsp" title="Peer review - Moodle Tracker"/>' => '<link rel="search" type="application/opensearchdescription+xml" href="/osd.jsp" title="Peer review - Moodle Tracker"/>',
+            '<a href="https://docs.moodle.org/dev/Main_Page"></a><span>www.google.com</span><span class="placeholder"></span>' => '<a href="https://docs.moodle.org/dev/Main_Page"></a><span><a href="http://www.google.com" class="_blanktarget">www.google.com</a></span><span class="placeholder"></span>',
+            'http://nolandforzombies.com <a href="zombiesFTW.com">Zombies FTW</a> http://aliens.org' => '<a href="http://nolandforzombies.com" class="_blanktarget">http://nolandforzombies.com</a> <a href="zombiesFTW.com">Zombies FTW</a> <a href="http://aliens.org" class="_blanktarget">http://aliens.org</a>',
             //URLs in Javascript. Commented out as part of MDL-21183
             //'var url="http://moodle.org";'=>'var url="http://moodle.org";',
             //'var url = "http://moodle.org";'=>'var url = "http://moodle.org";',
index 093f605..2922900 100644 (file)
@@ -25,6 +25,7 @@
 
 require_once('../../config.php');
 require_once('key_form.php');
+require_once($CFG->dirroot.'/grade/lib.php');
 
 /// get url variables
 $courseid = optional_param('courseid', 0, PARAM_INT);
@@ -62,6 +63,12 @@ require_login($course);
 $context = context_course::instance($course->id);
 require_capability('moodle/grade:export', $context);
 
+// Check if the user has at least one grade publishing capability.
+$plugins = grade_helper::get_plugins_export($course->id);
+if (!isset($plugins['keymanager'])) {
+    print_error('nopermissions');
+}
+
 // extra security check
 if (!empty($key->userid) and $USER->id != $key->userid) {
     print_error('notownerofkey');
index 697f1bb..aca4e87 100644 (file)
@@ -39,6 +39,12 @@ $context = context_course::instance($id);
 
 require_capability('moodle/grade:export', $context);
 
+// Check if the user has at least one grade publishing capability.
+$plugins = grade_helper::get_plugins_export($course->id);
+if (!isset($plugins['keymanager'])) {
+    print_error('nopermissions');
+}
+
 print_grade_page_head($course->id, 'export', 'keymanager', get_string('keymanager', 'grades'));
 
 $stredit   = get_string('edit');
index 937f734..5437bee 100644 (file)
@@ -51,9 +51,11 @@ class gradingform_guide_editguide extends moodleform {
         $form->setType('returnurl', PARAM_LOCALURL);
 
         // Name.
-        $form->addElement('text', 'name', get_string('name', 'gradingform_guide'), array('size'=>52));
+        $form->addElement('text', 'name', get_string('name', 'gradingform_guide'),
+            array('size' => 52, 'maxlength' => 255));
         $form->addRule('name', get_string('required'), 'required');
         $form->setType('name', PARAM_TEXT);
+        $form->addRule('name', null, 'maxlength', 255, 'client');
 
         // Description.
         $options = gradingform_guide_controller::description_form_field_options($this->_customdata['context']);
index da430ec..902834c 100644 (file)
@@ -197,6 +197,10 @@ class moodlequickform_guideeditor extends HTML_QuickForm_input {
                     $errors['err_noshortname'] = 1;
                     $criterion['error_description'] = true;
                 }
+                if (strlen(trim($criterion['shortname'])) > 255) {
+                    $errors['err_shortnametoolong'] = 1;
+                    $criterion['error_description'] = true;
+                }
                 if (!strlen(trim($criterion['maxscore']))) {
                     $errors['err_nomaxscore'] = 1;
                     $criterion['error_description'] = true;
index 6bbfb40..a36eabc 100644 (file)
@@ -54,6 +54,7 @@ $string['err_nodescription'] = 'Student description can not be empty';
 $string['err_nodescriptionmarkers'] = 'Marker description can not be empty';
 $string['err_nomaxscore'] = 'Criterion max score can not be empty';
 $string['err_noshortname'] = 'Criterion name can not be empty';
+$string['err_shortnametoolong'] = 'Criterion name must be less than 256 characters';
 $string['err_scoreinvalid'] = 'The score given to {$a->criterianame} is not valid, the max score is: {$a->maxscore}';
 $string['gradingof'] = '{$a} grading';
 $string['guidemappingexplained'] = 'WARNING: Your marking guide has a maximum grade of <b>{$a->maxscore} points</b>┬ábut the maximum grade set in your activity is {$a->modulegrade}  The maximum score set in your marking guide will be scaled to the maximum grade in the module.<br />
index 801dec3..a099f59 100644 (file)
@@ -180,7 +180,7 @@ class gradeimport_csv_load_data {
      * @param array $header The column headers from the CSV file.
      * @param int $key Current row identifier.
      * @param string $value The value for this row (final grade).
-     * @return array new grades that are ready for commiting to the gradebook.
+     * @return stdClass new grade that is ready for commiting to the gradebook.
      */
     protected function import_new_grade_item($header, $key, $value) {
         global $DB, $USER;
@@ -199,14 +199,16 @@ class gradeimport_csv_load_data {
         $newgrade = new stdClass();
         $newgrade->newgradeitem = $this->newgradeitems[$key];
 
-        // If the user has a grade for this grade item.
-        if (trim($value) != '-') {
-            // Instead of omitting the grade we could insert one with finalgrade set to 0.
-            // We do not have access to grade item min grade.
+        $trimmed = trim($value);
+        if ($trimmed === '' or $trimmed == '-') {
+            // Blank or dash grade means null, ie "no grade".
+            $newgrade->finalgrade = null;
+        } else {
+            // We have an actual grade.
             $newgrade->finalgrade = $value;
-            $newgrades[] = $newgrade;
         }
-        return $newgrades;
+        $this->newgrades[] = $newgrade;
+        return $newgrade;
     }
 
     /**
@@ -385,7 +387,7 @@ class gradeimport_csv_load_data {
                 }
             break;
             case 'new':
-                $this->newgrades = $this->import_new_grade_item($header, $key, $value);
+                $this->import_new_grade_item($header, $key, $value);
             break;
             case 'feedback':
                 if ($feedbackgradeid) {
index 68a1521..07a1596 100644 (file)
@@ -41,9 +41,9 @@ require_once($CFG->libdir . '/grade/tests/fixtures/lib.php');
 class gradeimport_csv_load_data_testcase extends grade_base_testcase {
 
     /** @var string $oktext Text to be imported. This data should have no issues being imported. */
-    protected $oktext = '"First name",Surname,"ID number",Institution,Department,"Email address","Assignment: Assignment for grape group", "Feedback: Assignment for grape group","Course total"
-Anne,Able,,"Moodle HQ","Rock on!",student7@mail.com,56.00,"We welcome feedback",56.00
-Bobby,Bunce,,"Moodle HQ","Rock on!",student5@mail.com,75.00,,75.00';
+    protected $oktext = '"First name",Surname,"ID number",Institution,Department,"Email address","Assignment: Assignment for grape group", "Feedback: Assignment for grape group","Assignment: Second new grade item","Course total"
+Anne,Able,,"Moodle HQ","Rock on!",student7@mail.com,56.00,"We welcome feedback",,56.00
+Bobby,Bunce,,"Moodle HQ","Rock on!",student5@mail.com,75.00,,45.0,75.00';
 
     /** @var string $badtext Text to be imported. This data has an extra column and should not succeed in being imported. */
     protected $badtext = '"First name",Surname,"ID number",Institution,Department,"Email address","Assignment: Assignment for grape group","Course total"
@@ -109,6 +109,7 @@ Bobby,Bunce,,"Moodle HQ","Rock on!",student5@mail.com,75.00,,75.00,{exportdate}'
                 'student7@mail.com',
                 56.00,
                 'We welcome feedback',
+                '',
                 56.00
             ),
             array(
@@ -120,6 +121,7 @@ Bobby,Bunce,,"Moodle HQ","Rock on!",student5@mail.com,75.00,,75.00,{exportdate}'
                 'student5@mail.com',
                 75.00,
                 '',
+                45.0,
                 75.00
             )
         );
@@ -133,6 +135,7 @@ Bobby,Bunce,,"Moodle HQ","Rock on!",student5@mail.com,75.00,,75.00,{exportdate}'
             'Email address',
             'Assignment: Assignment for grape group',
             'Feedback: Assignment for grape group',
+            'Assignment: Second new grade item',
             'Course total'
         );
         // Check that general data is returned as expected.
@@ -371,6 +374,14 @@ Bobby,Bunce,,"Moodle HQ","Rock on!",student5@mail.com,75.00,,75.00,{exportdate}'
         // Check that the final grade is the same as the one inserted.
         $this->assertEquals($testarray[0][6], $newgrades[0]->finalgrade);
 
+        $newgrades = $testobject->test_map_user_data_with_value('new', $testarray[0][8], $this->columns, $map, $key,
+                $this->courseid, $map[$key], $verbosescales);
+        // Check that the final grade is the same as the one inserted.
+        // The testobject should now contain 2 new grade items.
+        $this->assertEquals(2, count($newgrades));
+        // Because this grade item is empty, the value for final grade should be null.
+        $this->assertNull($newgrades[1]->finalgrade);
+
         $feedback = $testobject->test_map_user_data_with_value('feedback', $testarray[0][7], $this->columns, $map, $key,
                 $this->courseid, $map[$key], $verbosescales);
         // Expected result.
index 96d191b..ed5d931 100644 (file)
@@ -25,6 +25,7 @@
 
 require_once('../../config.php');
 require_once('key_form.php');
+require_once($CFG->dirroot.'/grade/lib.php');
 
 /// get url variables
 $courseid = optional_param('courseid', 0, PARAM_INT);
@@ -62,6 +63,12 @@ require_login($course);
 $context = context_course::instance($course->id);
 require_capability('moodle/grade:import', $context);
 
+// Check if the user has at least one grade publishing capability.
+$plugins = grade_helper::get_plugins_import($course->id);
+if (!isset($plugins['keymanager'])) {
+    print_error('nopermissions');
+}
+
 // extra security check
 if (!empty($key->userid) and $USER->id != $key->userid) {
     print_error('notownerofkey');
index e09dc54..8c93b7e 100644 (file)
@@ -39,6 +39,12 @@ $context = context_course::instance($id);
 
 require_capability('moodle/grade:import', $context);
 
+// Check if the user has at least one grade publishing capability.
+$plugins = grade_helper::get_plugins_import($course->id);
+if (!isset($plugins['keymanager'])) {
+    print_error('nopermissions');
+}
+
 print_grade_page_head($course->id, 'import', 'keymanager', get_string('keymanager', 'grades'));
 
 $stredit   = get_string('edit');
index 7a6d09b..43b6d35 100644 (file)
@@ -1248,10 +1248,22 @@ class grade_structure {
         global $CFG, $OUTPUT;
         require_once $CFG->libdir.'/filelib.php';
 
+        $outputstr = '';
+
+        // Object holding pix_icon information before instantiation.
+        $icon = new stdClass();
+        $icon->attributes = array(
+            'class' => 'item itemicon'
+        );
+        $icon->component = 'moodle';
+
+        $none = true;
         switch ($element['type']) {
             case 'item':
             case 'courseitem':
             case 'categoryitem':
+                $none = false;
+
                 $is_course   = $element['object']->is_course_item();
                 $is_category = $element['object']->is_category_item();
                 $is_scale    = $element['object']->gradetype == GRADE_TYPE_SCALE;
@@ -1259,71 +1271,68 @@ class grade_structure {
                 $is_outcome  = !empty($element['object']->outcomeid);
 
                 if ($element['object']->is_calculated()) {
-                    $strcalc = get_string('calculatedgrade', 'grades');
-                    return '<img src="'.$OUTPUT->pix_url('i/calc') . '" class="icon itemicon" title="'.
-                            s($strcalc).'" alt="'.s($strcalc).'"/>';
+                    $icon->pix = 'i/calc';
+                    $icon->title = s(get_string('calculatedgrade', 'grades'));
 
                 } else if (($is_course or $is_category) and ($is_scale or $is_value)) {
                     if ($category = $element['object']->get_item_category()) {
                         $aggrstrings = grade_helper::get_aggregation_strings();
                         $stragg = $aggrstrings[$category->aggregation];
+
+                        $icon->pix = 'i/calc';
+                        $icon->title = s($stragg);
+
                         switch ($category->aggregation) {
                             case GRADE_AGGREGATE_MEAN:
                             case GRADE_AGGREGATE_MEDIAN:
                             case GRADE_AGGREGATE_WEIGHTED_MEAN:
                             case GRADE_AGGREGATE_WEIGHTED_MEAN2:
                             case GRADE_AGGREGATE_EXTRACREDIT_MEAN:
-                                return '<img src="'.$OUTPUT->pix_url('i/agg_mean') . '" ' .
-                                        'class="icon itemicon" title="'.s($stragg).'" alt="'.s($stragg).'"/>';
+                                $icon->pix = 'i/agg_mean';
+                                break;
                             case GRADE_AGGREGATE_SUM:
-                                return '<img src="'.$OUTPUT->pix_url('i/agg_sum') . '" ' .
-                                        'class="icon itemicon" title="'.s($stragg).'" alt="'.s($stragg).'"/>';
-                            default:
-                                return '<img src="'.$OUTPUT->pix_url('i/calc') . '" ' .
-                                        'class="icon itemicon" title="'.s($stragg).'" alt="'.s($stragg).'"/>';
+                                $icon->pix = 'i/agg_sum';
+                                break;
                         }
                     }
 
                 } else if ($element['object']->itemtype == 'mod') {
-                    //prevent outcomes being displaying the same icon as the activity they are attached to
+                    // Prevent outcomes displaying the same icon as the activity they are attached to.
                     if ($is_outcome) {
-                        $stroutcome = s(get_string('outcome', 'grades'));
-                        return '<img src="'.$OUTPUT->pix_url('i/outcomes') . '" ' .
-                            'class="icon itemicon" title="'.$stroutcome.
-                            '" alt="'.$stroutcome.'"/>';
+                        $icon->pix = 'i/outcomes';
+                        $icon->title = s(get_string('outcome', 'grades'));
                     } else {
-                        $strmodname = get_string('modulename', $element['object']->itemmodule);
-                        return '<img src="'.$OUTPUT->pix_url('icon',
-                            $element['object']->itemmodule) . '" ' .
-                            'class="icon itemicon" title="' .s($strmodname).
-                            '" alt="' .s($strmodname).'"/>';
+                        $icon->pix = 'icon';
+                        $icon->component = $element['object']->itemmodule;
+                        $icon->title = s(get_string('modulename', $element['object']->itemmodule));
                     }
                 } else if ($element['object']->itemtype == 'manual') {
                     if ($element['object']->is_outcome_item()) {
-                        $stroutcome = get_string('outcome', 'grades');
-                        return '<img src="'.$OUTPUT->pix_url('i/outcomes') . '" ' .
-                                'class="icon itemicon" title="'.s($stroutcome).
-                                '" alt="'.s($stroutcome).'"/>';
+                        $icon->pix = 'i/outcomes';
+                        $icon->title = s(get_string('outcome', 'grades'));
                     } else {
-                        $strmanual = get_string('manualitem', 'grades');
-                        return '<img src="'.$OUTPUT->pix_url('i/manual_item') . '" '.
-                                'class="icon itemicon" title="'.s($strmanual).
-                                '" alt="'.s($strmanual).'"/>';
+                        $icon->pix = 'i/manual_item';
+                        $icon->title = s(get_string('manualitem', 'grades'));
                     }
                 }
                 break;
 
             case 'category':
-                $strcat = get_string('category', 'grades');
-                return '<img src="'.$OUTPUT->pix_url('i/folder') . '" class="icon itemicon" ' .
-                        'title="'.s($strcat).'" alt="'.s($strcat).'" />';
+                $none = false;
+                $icon->pix = 'i/folder';
+                $icon->title = s(get_string('category', 'grades'));
+                break;
         }
 
-        if ($spacerifnone) {
-            return $OUTPUT->spacer().' ';
+        if ($none) {
+            if ($spacerifnone) {
+                $outputstr = $OUTPUT->spacer() . ' ';
+            }
         } else {
-            return '';
+            $outputstr = $OUTPUT->pix_icon($icon->pix, $icon->title, $icon->component, $icon->attributes);
         }
+
+        return $outputstr;
     }
 
     /**
@@ -2838,8 +2847,9 @@ abstract class grade_helper {
                 $importplugins[$plugin] = new grade_plugin_info($plugin, $url, $pluginstr);
             }
 
-
-            if ($CFG->gradepublishing) {
+            // Show key manager if grade publishing is enabled and the user has xml publishing capability.
+            // XML is the only grade import plugin that has publishing feature.
+            if ($CFG->gradepublishing && has_capability('gradeimport/xml:publish', $context)) {
                 $url = new moodle_url('/grade/import/keymanager.php', array('id'=>$courseid));
                 $importplugins['keymanager'] = new grade_plugin_info('keymanager', $url, get_string('keymanager', 'grades'));
             }
@@ -2866,17 +2876,24 @@ abstract class grade_helper {
         }
         $context = context_course::instance($courseid);
         $exportplugins = array();
+        $canpublishgrades = 0;
         if (has_capability('moodle/grade:export', $context)) {
             foreach (core_component::get_plugin_list('gradeexport') as $plugin => $plugindir) {
                 if (!has_capability('gradeexport/'.$plugin.':view', $context)) {
                     continue;
                 }
+                // All the grade export plugins has grade publishing capabilities.
+                if (has_capability('gradeexport/'.$plugin.':publish', $context)) {
+                    $canpublishgrades++;
+                }
+
                 $pluginstr = get_string('pluginname', 'gradeexport_'.$plugin);
                 $url = new moodle_url('/grade/export/'.$plugin.'/index.php', array('id'=>$courseid));
                 $exportplugins[$plugin] = new grade_plugin_info($plugin, $url, $pluginstr);
             }
 
-            if ($CFG->gradepublishing) {
+            // Show key manager if grade publishing is enabled and the user has at least one grade publishing capability.
+            if ($CFG->gradepublishing && $canpublishgrades != 0) {
                 $url = new moodle_url('/grade/export/keymanager.php', array('id'=>$courseid));
                 $exportplugins['keymanager'] = new grade_plugin_info('keymanager', $url, get_string('keymanager', 'grades'));
             }
index 7ffc08e..cc809ab 100644 (file)
@@ -74,7 +74,7 @@ foreach ($outcomes as $outcomeid => $outcome) {
                       FROM {grade_grades}
                      WHERE itemid = ?".
                      $hidesuspendedsql.
-                  "GROUP BY itemid";
+                  " GROUP BY itemid";
             $info = $DB->get_records_sql($sql, $params);
 
             if (!$info) {
index f02b748..45eadb3 100644 (file)
@@ -1154,6 +1154,115 @@ class core_group_external extends external_api {
         return null;
     }
 
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 2.9
+     */
+    public static function get_course_user_groups_parameters() {
+        return new external_function_parameters(
+            array(
+                'courseid' => new external_value(PARAM_INT, 'id of course'),
+                'userid' => new external_value(PARAM_INT, 'id of user'),
+                'groupingid' => new external_value(PARAM_INT, 'returns only groups in the specified grouping', VALUE_DEFAULT, 0)
+            )
+        );
+    }
+
+    /**
+     * Get all groups in the specified course for the specified user.
+     *
+     * @throws moodle_exception
+     * @param int $courseid id of course.
+     * @param int $userid id of user.
+     * @param int $groupingid optional returns only groups in the specified grouping.
+     * @return array of group objects (id, name, description, format) and possible warnings.
+     * @since Moodle 2.9
+     */
+    public static function get_course_user_groups($courseid, $userid, $groupingid = 0) {
+        global $USER;
+
+        // Warnings array, it can be empty at the end but is mandatory.
+        $warnings = array();
+
+        $params = array(
+            'courseid' => $courseid,
+            'userid' => $userid,
+            'groupingid' => $groupingid
+        );
+        $params = self::validate_parameters(self::get_course_user_groups_parameters(), $params);
+        $courseid = $params['courseid'];
+        $userid = $params['userid'];
+        $groupingid = $params['groupingid'];
+
+        // Validate course and user. get_course throws an exception if the course does not exists.
+        $course = get_course($courseid);
+        $user = core_user::get_user($userid, 'id', MUST_EXIST);
+
+        // Security checks.
+        $context = context_course::instance($course->id);
+        self::validate_context($context);
+
+         // Check if we have permissions for retrieve the information.
+        if ($user->id != $USER->id) {
+            if (!has_capability('moodle/course:managegroups', $context)) {
+                throw new moodle_exception('accessdenied', 'admin');
+            }
+            // Validate if the user is enrolled in the course.
+            if (!is_enrolled($context, $user->id)) {
+                // We return a warning because the function does not fail for not enrolled users.
+                $warning['item'] = 'course';
+                $warning['itemid'] = $course->id;
+                $warning['warningcode'] = '1';
+                $warning['message'] = "User $user->id is not enrolled in course $course->id";
+                $warnings[] = $warning;
+            }
+        }
+
+        $usergroups = array();
+        if (empty($warnings)) {
+            $groups = groups_get_all_groups($course->id, $user->id, 0, 'g.id, g.name, g.description, g.descriptionformat');
+
+            foreach ($groups as $group) {
+                list($group->description, $group->descriptionformat) =
+                    external_format_text($group->description, $group->descriptionformat,
+                            $context->id, 'group', 'description', $group->id);
+                $usergroups[] = (array)$group;
+            }
+        }
+
+        $results = array(
+            'groups' => $usergroups,
+            'warnings' => $warnings
+        );
+        return $results;
+    }
+
+    /**
+     * Returns description of method result value.
+     *
+     * @return external_description A single structure containing groups and possible warnings.
+     * @since Moodle 2.9
+     */
+    public static function get_course_user_groups_returns() {
+        return new external_single_structure(
+            array(
+                'groups' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'id' => new external_value(PARAM_INT, 'group record id'),
+                            'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'),
+                            'description' => new external_value(PARAM_RAW, 'group description text'),
+                            'descriptionformat' => new external_format_value('description')
+                        )
+                    )
+                ),
+                'warnings' => new external_warnings(),
+            )
+        );
+    }
+
 }
 
 /**
index d3ca01e..a65d930 100644 (file)
@@ -285,4 +285,92 @@ class core_group_externallib_testcase extends externallib_advanced_testcase {
             $this->assertEquals($dbgroup->courseid, $groupcourseid);
         }
     }
+
+    /**
+     * Test get_groups
+     */
+    public function test_get_course_user_groups() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $student1 = self::getDataGenerator()->create_user();
+        $student2 = self::getDataGenerator()->create_user();
+        $teacher = self::getDataGenerator()->create_user();
+
+        $course = self::getDataGenerator()->create_course();
+        $emptycourse = self::getDataGenerator()->create_course();
+
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($student1->id, $course->id, $studentrole->id);
+        $this->getDataGenerator()->enrol_user($student2->id, $course->id, $studentrole->id);
+
+        $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
+        $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
+        $this->getDataGenerator()->enrol_user($teacher->id, $emptycourse->id, $teacherrole->id);
+
+        $group1data = array();
+        $group1data['courseid'] = $course->id;
+        $group1data['name'] = 'Group Test 1';
+        $group1data['description'] = 'Group Test 1 description';
+        $group2data = array();
+        $group2data['courseid'] = $course->id;
+        $group2data['name'] = 'Group Test 2';
+        $group2data['description'] = 'Group Test 2 description';
+        $group1 = self::getDataGenerator()->create_group($group1data);
+        $group2 = self::getDataGenerator()->create_group($group2data);
+
+        groups_add_member($group1->id, $student1->id);
+        groups_add_member($group1->id, $student2->id);
+        groups_add_member($group2->id, $student1->id);
+
+        $this->setUser($student1);
+
+        $groups = core_group_external::get_course_user_groups($course->id, $student1->id);
+        $groups = external_api::clean_returnvalue(core_group_external::get_course_user_groups_returns(), $groups);
+        // Check that I see my groups.
+        $this->assertCount(2, $groups['groups']);
+
+        $this->setUser($student2);
+        $groups = core_group_external::get_course_user_groups($course->id, $student2->id);
+        $groups = external_api::clean_returnvalue(core_group_external::get_course_user_groups_returns(), $groups);
+        // Check that I see my groups.
+        $this->assertCount(1, $groups['groups']);
+
+        $this->assertEquals($group1data['name'], $groups['groups'][0]['name']);
+        $this->assertEquals($group1data['description'], $groups['groups'][0]['description']);
+
+        $this->setUser($teacher);
+        $groups = core_group_external::get_course_user_groups($course->id, $student1->id);
+        $groups = external_api::clean_returnvalue(core_group_external::get_course_user_groups_returns(), $groups);
+        // Check that a teacher can see student groups.
+        $this->assertCount(2, $groups['groups']);
+
+        $groups = core_group_external::get_course_user_groups($course->id, $student2->id);
+        $groups = external_api::clean_returnvalue(core_group_external::get_course_user_groups_returns(), $groups);
+        // Check that a teacher can see student groups.
+        $this->assertCount(1, $groups['groups']);
+
+        // Check permissions.
+        $this->setUser($student1);
+        try {
+            $groups = core_group_external::get_course_user_groups($course->id, $student2->id);
+        } catch (moodle_exception $e) {
+            $this->assertEquals('accessdenied', $e->errorcode);
+        }
+
+        try {
+            $groups = core_group_external::get_course_user_groups($emptycourse->id, $student2->id);
+        } catch (moodle_exception $e) {
+            $this->assertEquals('requireloginerror', $e->errorcode);
+        }
+
+        $this->setUser($teacher);
+        // Check warnings.
+        $groups = core_group_external::get_course_user_groups($emptycourse->id, $student1->id);
+        $groups = external_api::clean_returnvalue(core_group_external::get_course_user_groups_returns(), $groups);
+        $this->assertCount(1, $groups['warnings']);
+
+    }
+
 }
index fd3471d..69dbb49 100644 (file)
@@ -161,7 +161,7 @@ $string['configcourserequestnotify'] = 'Type username of user to be notified whe
 $string['configcourserequestnotify2'] = 'Users who will be notified when a course is requested. Only users who can approve course requests are listed here.';
 $string['configcoursesperpage'] = 'Enter the number of courses to be displayed per page in a course listing.';
 $string['configcourseswithsummarieslimit'] = 'The maximum number of courses to display in a course listing including summaries before falling back to a simpler listing.';
-$string['configcronclionly'] = 'If this is set, then the cron script can only be run from the command line instead of via the web.  This overrides the cron password setting below.';
+$string['configcronclionly'] = 'If this is set, then the cron script can only be run from the command line instead of via the web. This overrides the cron password setting below. Please note that, running cron from web can expose secure information to site users.';
 $string['configcronremotepassword'] = 'This means that the cron.php script cannot be run from a web browser without supplying the password using the following form of URL:<pre>
     http://site.example.com/admin/cron.php?password=opensesame
 </pre>If this is left empty, no password is required.';
@@ -800,7 +800,7 @@ $string['performance'] = 'Performance';
 $string['pgcluster'] = 'PostgreSQL Cluster';
 $string['pgclusterdescription'] = 'PostgreSQL version/cluster parameter for command line operations. If you only have one postgresql on your system or you are not sure what this is, leave this blank.';
 $string['phpfloatproblem'] = 'Detected unexpected problem in handling of PHP float numbers - {$a}';
-$string['pleaserefreshregistration'] = 'Your site has been registered. Registration last updated {$a}. You can manually update your registration at any time.';
+$string['pleaserefreshregistration'] = 'Your site is registered. Registration last updated {$a}.<br />The \'Site registration\' scheduled task keeps your registration up to date. You can also manually update your registration at any time.';
 $string['pleaserefreshregistrationunknown'] = 'Your site has been registered but the registration date is unknown. Please update your registration using the \'Update registration\' button or ensure that the \'Site registration\' scheduled task is enabled so your registration is automatically updated.';
 $string['plugin'] = 'Plugin';
 $string['plugins'] = 'Plugins';
@@ -897,11 +897,10 @@ $string['quizattemptsupgradedmessage'] = 'In Moodle 2.1 there was a major upgrad
 $string['recaptchaprivatekey'] = 'ReCAPTCHA private key';
 $string['recaptchapublickey'] = 'ReCAPTCHA public key';
 $string['register'] = 'Register your site';
-$string['registermoodleorg'] = 'When you register your site with {$a}';
+$string['registermoodleorg'] = 'When you register your site';
 $string['registermoodleorgli1'] = 'You are added to a low-volume mailing list for important notifications such as security alerts and new releases of Moodle.';
 $string['registermoodleorgli2'] = 'Statistics about your site will be added to the {$a} of the worldwide Moodle community.';
-$string['registermoodleorgli3'] = 'Your site is also registered with Moodle.net ({$a}), allowing users with the publish courses capability (by default only managers) the option of publishing courses to Moodle.net.';
-$string['registerwithmoodleorg'] = 'Register with Moodle.org';
+$string['registerwithmoodleorg'] = 'Register your site';
 $string['registration'] = 'Registration';
 $string['registration_help'] = 'Registering your site with Moodle.org is recommended in order to receive security alert notifications, to contribute <a href="http://moodle.org/stats">Moodle usage statistics</a> and to be able to share courses on <a href="http://moodle.net/">Moodle.net</a>.';
 $string['registrationwarning'] = 'Your site is not yet registered.';
index 6e68819..d8fd647 100644 (file)
@@ -87,6 +87,7 @@ $string['configgeneralfilters'] = 'Sets the default for including filters in a b
 $string['configgeneralhistories'] = 'Sets the default for including user history within a backup.';
 $string['configgenerallogs'] = 'If enabled logs will be included in backups by default.';
 $string['configgeneralquestionbank'] = 'If enabled the question bank will be included in backups by default. PLEASE NOTE: Disabling this setting will disable the backup of activities which use the question bank, such as the quiz.';
+$string['configgeneralgroups'] = 'Sets the default for including groups and groupings in a backup.';
 $string['configgeneralroleassignments'] = 'If enabled by default roles assignments will also be backed up.';
 $string['configgeneraluserscompletion'] = 'If enabled user completion information will be included in backups by default.';
 $string['configgeneralusers'] = 'Sets the default for whether to include users in backups.';
@@ -138,6 +139,7 @@ $string['generalhistories'] = 'Include histories';
 $string['generalgradehistories'] = 'Include histories';
 $string['generallogs'] = 'Include logs';
 $string['generalquestionbank'] = 'Include question bank';
+$string['generalgroups'] = 'Include groups and groupings';
 $string['generalroleassignments'] = 'Include role assignments';
 $string['generalsettings'] = 'General backup settings';
 $string['generaluserscompletion'] = 'Include user completion information';
@@ -235,6 +237,7 @@ $string['rootsettinguserscompletion'] = 'Include user completion details';
 $string['rootsettingquestionbank'] = 'Include question bank';
 $string['rootsettinglogs'] = 'Include course logs';
 $string['rootsettinggradehistories'] = 'Include grade history';
+$string['rootsettinggroups'] = 'Include groups and groupings';
 $string['rootsettingimscc1'] = 'Convert to IMS Common Cartridge 1.0';
 $string['rootsettingimscc11'] = 'Convert to IMS Common Cartridge 1.1';
 $string['sitecourseformatwarning'] = 'This is a front page backup, note that they can only be restored on the front page';
index 21cf94e..ce03b75 100644 (file)
@@ -246,6 +246,7 @@ $string['error:requesttimeout'] = 'The connection request timed out before it co
 $string['error:requesterror'] = 'The connection request failed (error code {$a}).';
 $string['error:save'] = 'Cannot save the badge.';
 $string['error:userdeleted'] = '{$a->user} (This user no longer exists in {$a->site})';
+$string['eventbadgeawarded'] = 'Badge awarded';
 $string['evidence'] = 'Evidence';
 $string['existingrecipients'] = 'Existing badge recipients';
 $string['expired'] = 'Expired';
index 8feb0f2..2da9eee 100644 (file)
@@ -104,6 +104,7 @@ $string['periodend'] = 'until {$a}';
 $string['periodnone'] = 'enrolled {$a}';
 $string['periodstart'] = 'from {$a}';
 $string['periodstartend'] = 'from {$a->start} until {$a->end}';
+$string['proceedtocourse'] = 'Proceed to course content';
 $string['recovergrades'] = 'Recover user\'s old grades if possible';
 $string['rolefromthiscourse'] = '{$a->role} (Assigned in this course)';
 $string['rolefrommetacourse'] = '{$a->role} (Inherited from parent course)';
index 6cc2acf..95e361e 100644 (file)
@@ -34,9 +34,11 @@ $string['categoryrole'] = 'Category role';
 $string['contains'] = 'contains';
 $string['content'] = 'Content';
 $string['contentandheadings'] = 'Content and headings';
+$string['coursecategory'] = 'course category';
 $string['courserole'] = 'Course role';
 $string['courserolelabel'] = '{$a->label} is {$a->rolename} in {$a->coursename} from {$a->categoryname}';
 $string['courserolelabelerror'] = '{$a->label} error: course {$a->coursename} does not exist';
+$string['coursevalue'] = 'course value';
 $string['datelabelisafter'] = '{$a->label} is after {$a->after}';
 $string['datelabelisbefore'] = '{$a->label} is before {$a->before}';
 $string['datelabelisbetween'] = '{$a->label} is between {$a->after} and {$a->before}';
@@ -62,6 +64,7 @@ $string['isempty'] = 'is empty';
 $string['isequalto'] = 'is equal to';
 $string['isnotdefined'] = 'isn\'t defined';
 $string['isnotequalto'] = 'isn\'t equal to';
+$string['limiterfor'] = '{$a} field limiter';
 $string['neveraccessed'] = 'Never accessed';
 $string['nevermodified'] = 'Never modified';
 $string['newfilter'] = 'New filter';
@@ -69,6 +72,8 @@ $string['nofiltersenabled'] = 'No filter plugins have been enabled on this site.
 $string['off'] = 'Off';
 $string['offbutavailable'] = 'Off, but available';
 $string['on'] = 'On';
+$string['profilefilterfield'] = 'Profile field name';
+$string['profilefilterlimiter'] = 'Profile field operator';
 $string['profilelabel'] = '{$a->label}: {$a->profile} {$a->operator} {$a->value}';
 $string['profilelabelnovalue'] = '{$a->label}: {$a->profile} {$a->operator}';
 $string['removeall'] = 'Remove all filters';
@@ -78,3 +83,4 @@ $string['startswith'] = 'starts with';
 $string['tablenosave'] = 'Changes in table above are saved automatically.';
 $string['textlabel'] = '{$a->label} {$a->operator} {$a->value}';
 $string['textlabelnovalue'] = '{$a->label} {$a->operator}';
+$string['valuefor'] = '{$a} value';
index ecd7bd7..734279e 100644 (file)
@@ -32,7 +32,7 @@ $string['err_alphanumeric'] = 'You must enter only letters or numbers here.';
 $string['err_email'] = 'You must enter a valid email address here.';
 $string['err_lettersonly'] = 'You must enter only letters here.';
 $string['err_maxfiles'] = 'You must not attach more than {$a} files here.';
-$string['err_maxlength'] = 'You must enter not more than {$a->format} characters here.';
+$string['err_maxlength'] = 'You must enter no more than {$a->format} characters here.';
 $string['err_minlength'] = 'You must enter at least {$a->format} characters here.';
 $string['err_nonzero'] = 'You must enter a number not starting with a 0 here.';
 $string['err_nopunctuation'] = 'You must enter no punctuation characters here.';
index 753ebe4..02d240b 100644 (file)
@@ -159,12 +159,12 @@ $string['registeredcourses'] = 'Registered courses';
 $string['registeredsites'] = 'Registered sites';
 $string['registrationinfo'] = 'Registration information';
 $string['registeredmoodleorg'] = 'Moodle.org ({$a})';
-$string['registeredon'] = 'Hubs with which you are registered';
+$string['registeredon'] = 'Where your site is registered';
 $string['registermoochtips'] = 'In order to register with Moodle.net, your site must be registered with Moodle.org.';
 $string['registersite'] = 'Register with {$a}';
 $string['registerwith'] = 'Register with a hub';
 $string['registrationconfirmed'] = 'Site registration confirmed';
-$string['registrationconfirmedon'] = 'You are now registered on the hub {$a}. You are now able to publish courses to this hub, using the "Publish" link in course administration menus.';
+$string['registrationconfirmedon'] = 'Thank you for registering your site. Registration information will be kept up to date by the \'Site registration\' scheduled task.';
 $string['registrationupdated'] = 'Registration has been updated.';
 $string['registrationupdatedfailed'] = 'Registration update failed.';
 $string['removefromhub'] = 'Remove from hub';
index b9a5d7b..4b1272c 100644 (file)
@@ -270,7 +270,7 @@ $string['complete'] = 'Complete';
 $string['completereport'] = 'Complete report';
 $string['configuration'] = 'Configuration';
 $string['confirm'] = 'Confirm';
-$string['confirmdeletesection'] = 'Are you absolutely sure you want to delete "{$a}"? All activities will be also deleted';
+$string['confirmdeletesection'] = 'Are you absolutely sure you want to completely delete "{$a}" and all the activities it contains?';
 $string['confirmed'] = 'Your registration has been confirmed';
 $string['confirmednot'] = 'Your registration has not yet been confirmed!';
 $string['confirmcheckfull'] = 'Are you absolutely sure you want to confirm {$a} ?';
@@ -375,7 +375,7 @@ $string['courserequestintro'] = 'Use this form to request a course to be created
 $string['courserequestreason'] = 'Reasons for wanting this course';
 $string['courserequestsuccess'] = 'Your course request has been saved successfully. You will be sent an email to inform you whether your request was approved.';
 $string['courserequestsupport'] = 'Supporting information to help the administrator evaluate this request';
-$string['courserequestwarning'] = 'The user requesting this course will be automatically enrolled using the "{$a}" role';
+$string['courserequestwarning'] = 'The user requesting this course will be automatically enrolled and assigned the role of {$a}.';
 $string['courserestore'] = 'Course restore';
 $string['courses'] = 'Courses';
 $string['coursesectionsummaries'] = 'Course section summaries';
@@ -1568,7 +1568,7 @@ $string['rssarticles'] = 'Number of RSS recent articles';
 $string['rsserror'] = 'Error reading RSS data';
 $string['rsserrorauth'] = 'Your RSS link does not contain a valid authentication token.';
 $string['rsserrorguest'] = 'This feed uses guest access to access the data, but guest does not have permission to read the data. Visit the original location that this feed comes from (URL) as a valid user and get a new RSS link from there.';
-$string['rsskeyshelp'] = 'To ensure security and privacy, RSS feed URLs contain a special token that identifies the user they are for. This prevents other users from accessing areas of Moodle they shouldn\'t have access to via RSS feeds.</p><p>This token is automatically created the first time you access an area of Moodle that produces an RSS feed. If you feel that your RSS feed token has been compromised in some way you can request a new one by clicking the Reset link here. Please note that your current RSS feed URLs will then become invalid.';
+$string['rsskeyshelp'] = '<p>To ensure security and privacy, RSS feed URLs contain a special token that identifies the user they are for. This prevents other users from accessing areas of the site where they are not allowed.</p><p>The token is automatically created the first time you access an area that produces an RSS feed. If you think that your RSS feed token has been compromised, you can request a new one by clicking the reset link. Please note that your current RSS feed URLs will then become invalid.</p>';
 $string['rsstype'] = 'RSS feed for this activity';
 $string['saveandnext'] = 'Save and show next';
 $string['savedat'] = 'Saved at:';
index 6e7352b..cc3eb9b 100644 (file)
@@ -61,16 +61,16 @@ $string['close'] = 'Close';
 $string['commonrepositorysettings'] = 'Common repository settings';
 $string['configallowexternallinks'] = 'This option enables all users to choose whether or not external media is copied into Moodle or not. If this is off then media is always copied into Moodle (this is usually best for overall data integrity and security).  If this is on then users can choose each time they add media to a text.';
 $string['configcacheexpire'] = 'The amount of time that file listings are cached locally (in seconds) when browsing external repositories.';
-$string['configgetfiletimeout'] = 'Timeout in seconds for downloading the external file into moodle.';
+$string['configgetfiletimeout'] = 'Timeout in seconds for downloading an external file into Moodle.';
 $string['configsaved'] = 'Configuration saved!';
 $string['configsyncfiletimeout'] = 'Timeout in seconds for syncronising the external file size.';
 $string['configsyncimagetimeout'] = 'Timeout in seconds for downloading an image file from external repository during syncronisation.';
-$string['confirmdelete'] = 'Are you sure you want to delete this repository - {$a}? If you choose "Continue and download", file references to external contents will be downloaded to moodle, but it could take long time to process.';
+$string['confirmdelete'] = 'Are you sure you want to delete the repository {$a}? If you choose "Continue and download", file references to external contents will be downloaded to Moodle. This could take a long time to process.';
 $string['confirmdeletefile'] = 'Are you sure you want to delete this file?';
 $string['confirmrenamefile'] = 'Are you sure you want to rename/move this file? There are {$a} alias/shortcut files that use this file as their source. If you proceed then those aliases will be converted to true copies.';
 $string['confirmdeletefilewithhref'] = 'Are you sure you want to delete this file? There are {$a} alias/shortcut files that use this file as their source. If you proceed then those aliases will be converted to true copies.';
 $string['confirmdeletefolder'] = 'Are you sure you want to delete this folder? All files and subfolders will be deleted.';
-$string['confirmremove'] = 'Are you sure you want to remove this repository plugin, its options and <strong>all of its instances</strong> - {$a}? If you choose "Continue and download", file references to external contents will be downloaded to moodle, but it could take long time to process.';
+$string['confirmremove'] = 'Are you sure you want to remove this repository plugin, its options and <strong>all of its instances</strong> - {$a}? If you choose "Continue and download", file references to external contents will be downloaded to Moodle. This could take a long time to process.';
 $string['confirmrenamefolder'] = ' Are you sure you want to move/rename this folder? Any alias/shortcut files that reference files in this folder will be converted into true copies.';
 $string['continueuninstall'] = 'Continue';
 $string['continueuninstallanddownload'] = 'Continue and download';
@@ -240,7 +240,7 @@ $string['usenonjsfilemanager'] = 'Open file manager in new window';
 $string['usenonjsfilepicker'] = 'Open file picker in new window';
 $string['unzipped'] = 'Unzipped successfully';
 $string['wrongcontext'] = 'You cannot access to this context';
-$string['xhtmlerror'] = 'You are probably using XHTML strict header, some YUI Component doesn\'t work in this mode, please turn it off in moodle';
+$string['xhtmlerror'] = 'You are probably using an XHTML strict header. Certain YUI components don\'t work in this mode; please turn it off.';
 $string['ziped'] = 'Compress folder successfully';
 
 // Deprecated since Moodle 2.8.
index 3bc7777..87e1007 100644 (file)
@@ -170,10 +170,10 @@ $string['defaultx'] = 'Default: {$a}';
 $string['defineroles'] = 'Define roles';
 $string['deletecourseoverrides'] = 'Delete all overrides in course';
 $string['deletelocalroles'] = 'Delete all local role assignments';
-$string['deleterolesure'] = 'Are you sure that you want to delete role "{$a->name} ({$a->shortname})"?</p><p>Currently this role is assigned to {$a->count} users.';
+$string['deleterolesure'] = '<p>Are you sure that you want to delete role "{$a->name} ({$a->shortname})"?</p><p>Currently this role is assigned to {$a->count} users.</p>';
 $string['deletexrole'] = 'Delete {$a} role';
 $string['duplicaterole'] = 'Duplicate role';
-$string['duplicaterolesure'] = 'Are you sure that you want to duplicate role "{$a->name} ({$a->shortname})"?</p>';
+$string['duplicaterolesure'] = '<p>Are you sure that you want to duplicate role "{$a->name} ({$a->shortname})"?</p>';
 $string['editingrolex'] = 'Editing role \'{$a}\'';
 $string['editrole'] = 'Edit role';
 $string['editxrole'] = 'Edit {$a} role';
index 7617216..b059127 100644 (file)
@@ -126,6 +126,7 @@ $string['missingusername'] = 'Missing username';
 $string['missingversionfile'] = 'Coding error: version.php file is missing for the component {$a}';
 $string['mobilewsdisabled'] = 'Disabled';
 $string['mobilewsenabled'] = 'Enabled';
+$string['nameexists'] = 'This name is already in use by another service';
 $string['nocapabilitytouseparameter'] = 'The user does not have the required capability to use the parameter {$a}';
 $string['nofunctions'] = 'This service has no functions.';
 $string['norequiredcapability'] = 'No required capability';
index fb679a9..1c0f8b0 100644 (file)
@@ -6447,14 +6447,17 @@ class context_user extends context {
     protected static function create_level_instances() {
         global $DB;
 
-        $sql = "INSERT INTO {context} (contextlevel, instanceid)
-                SELECT ".CONTEXT_USER.", u.id
+        $sql = "SELECT ".CONTEXT_USER.", u.id
                   FROM {user} u
                  WHERE u.deleted = 0
                        AND NOT EXISTS (SELECT 'x'
                                          FROM {context} cx
                                         WHERE u.id = cx.instanceid AND cx.contextlevel=".CONTEXT_USER.")";
-        $DB->execute($sql);
+        $contextdata = $DB->get_recordset_sql($sql);
+        foreach ($contextdata as $context) {
+            context::insert_context_record(CONTEXT_USER, $context->id, null);
+        }
+        $contextdata->close();
     }
 
     /**
@@ -6655,13 +6658,16 @@ class context_coursecat extends context {
     protected static function create_level_instances() {
         global $DB;
 
-        $sql = "INSERT INTO {context} (contextlevel, instanceid)
-                SELECT ".CONTEXT_COURSECAT.", cc.id
+        $sql = "SELECT ".CONTEXT_COURSECAT.", cc.id
                   FROM {course_categories} cc
                  WHERE NOT EXISTS (SELECT 'x'
                                      FROM {context} cx
                                     WHERE cc.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSECAT.")";
-        $DB->execute($sql);
+        $contextdata = $DB->get_recordset_sql($sql);
+        foreach ($contextdata as $context) {
+            context::insert_context_record(CONTEXT_COURSECAT, $context->id, null);
+        }
+        $contextdata->close();
     }
 
     /**
@@ -6878,13 +6884,16 @@ class context_course extends context {
     protected static function create_level_instances() {
         global $DB;
 
-        $sql = "INSERT INTO {context} (contextlevel, instanceid)
-                SELECT ".CONTEXT_COURSE.", c.id
+        $sql = "SELECT ".CONTEXT_COURSE.", c.id
                   FROM {course} c
                  WHERE NOT EXISTS (SELECT 'x'
                                      FROM {context} cx
                                     WHERE c.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSE.")";
-        $DB->execute($sql);
+        $contextdata = $DB->get_recordset_sql($sql);
+        foreach ($contextdata as $context) {
+            context::insert_context_record(CONTEXT_COURSE, $context->id, null);
+        }
+        $contextdata->close();
     }
 
     /**
@@ -7132,13 +7141,16 @@ class context_module extends context {
     protected static function create_level_instances() {
         global $DB;
 
-        $sql = "INSERT INTO {context} (contextlevel, instanceid)
-                SELECT ".CONTEXT_MODULE.", cm.id
+        $sql = "SELECT ".CONTEXT_MODULE.", cm.id
                   FROM {course_modules} cm
                  WHERE NOT EXISTS (SELECT 'x'
                                      FROM {context} cx
                                     WHERE cm.id = cx.instanceid AND cx.contextlevel=".CONTEXT_MODULE.")";
-        $DB->execute($sql);
+        $contextdata = $DB->get_recordset_sql($sql);
+        foreach ($contextdata as $context) {
+            context::insert_context_record(CONTEXT_MODULE, $context->id, null);
+        }
+        $contextdata->close();
     }
 
     /**
@@ -7349,13 +7361,16 @@ class context_block extends context {
     protected static function create_level_instances() {
         global $DB;
 
-        $sql = "INSERT INTO {context} (contextlevel, instanceid)
-                SELECT ".CONTEXT_BLOCK.", bi.id
+        $sql = "SELECT ".CONTEXT_BLOCK.", bi.id
                   FROM {block_instances} bi
                  WHERE NOT EXISTS (SELECT 'x'
                                      FROM {context} cx
                                     WHERE bi.id = cx.instanceid AND cx.contextlevel=".CONTEXT_BLOCK.")";
-        $DB->execute($sql);
+        $contextdata = $DB->get_recordset_sql($sql);
+        foreach ($contextdata as $context) {
+            context::insert_context_record(CONTEXT_BLOCK, $context->id, null);
+        }
+        $contextdata->close();
     }
 
     /**
diff --git a/lib/amd/build/first.min.js b/lib/amd/build/first.min.js
new file mode 100644 (file)
index 0000000..9dc3a61
Binary files /dev/null and b/lib/amd/build/first.min.js differ
diff --git a/lib/amd/src/first.js b/lib/amd/src/first.js
new file mode 100644 (file)
index 0000000..22188f3
--- /dev/null
@@ -0,0 +1,26 @@
+// 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 an empty module, that is required before all other modules.
+ * Because every module is returned from a request for any other module, this
+ * forces the loading of all modules with a single request.
+ *
+ * @module     core/first
+ * @package    core
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(function() { });
index 80b57ce..bf5b4e1 100644 (file)
@@ -398,6 +398,15 @@ class badge {
         $result = $DB->insert_record('badge_issued', $issued, true);
 
         if ($result) {
+            // Trigger badge awarded event.
+            $eventdata = array (
+                'context' => $this->get_context(),
+                'objectid' => $this->id,
+                'relateduserid' => $userid,
+                'other' => array('dateexpire' => $issued->dateexpire, 'badgeissuedid' => $result)
+            );
+            \core\event\badge_awarded::create($eventdata)->trigger();
+
             // Lock the badge, so that its criteria could not be changed any more.
             if ($this->status == BADGE_STATUS_ACTIVE) {
                 $this->set_status(BADGE_STATUS_ACTIVE_LOCKED);
index cd518cf..5fd807e 100644 (file)
@@ -39,45 +39,47 @@ class behat_selectors {
      * @var Allowed types when using text selectors arguments.
      */
     protected static $allowedtextselectors = array(
-        'dialogue' => 'dialogue',
-        'block' => 'block',
-        'section' => 'section',
         'activity' => 'activity',
-        'region' => 'region',
-        'table_row' => 'table_row',
+        'block' => 'block',
+        'css_element' => 'css_element',
+        'dialogue' => 'dialogue',
+        'fieldset' => 'fieldset',
         'list_item' => 'list_item',
+        'question' => 'question',
+        'region' => 'region',
+        'section' => 'section',
         'table' => 'table',
-        'fieldset' => 'fieldset',
-        'css_element' => 'css_element',
-        'xpath_element' => 'xpath_element'
+        'table_row' => 'table_row',
+        'xpath_element' => 'xpath_element',
     );
 
     /**
      * @var Allowed types when using selector arguments.
      */
     protected static $allowedselectors = array(
-        'dialogue' => 'dialogue',
-        'block' => 'block',
-        'section' => 'section',
         'activity' => 'activity',
-        'region' => 'region',
-        'table_row' => 'table_row',
-        'list_item' => 'list_item',
-        'link' => 'link',
+        'block' => 'block',
         'button' => 'button',
-        'link_or_button' => 'link_or_button',
-        'select' => 'select',
         'checkbox' => 'checkbox',
-        'radio' => 'radio',
+        'css_element' => 'css_element',
+        'dialogue' => 'dialogue',
+        'field' => 'field',
+        'fieldset' => 'fieldset',
         'file' => 'file',
         'filemanager' => 'filemanager',
+        'link' => 'link',
+        'link_or_button' => 'link_or_button',
+        'list_item' => 'list_item',
         'optgroup' => 'optgroup',
         'option' => 'option',
+        'question' => 'question',
+        'radio' => 'radio',
+        'region' => 'region',
+        'section' => 'section',
+        'select' => 'select',
         'table' => 'table',
-        'field' => 'field',
-        'fieldset' => 'fieldset',
+        'table_row' => 'table_row',
         'text' => 'text',
-        'css_element' => 'css_element',
         'xpath_element' => 'xpath_element'
     );
 
@@ -90,8 +92,14 @@ class behat_selectors {
      * @var XPaths for moodle elements.
      */
     protected static $moodleselectors = array(
-        'text' => <<<XPATH
-//*[contains(., %locator%)][count(./descendant::*[contains(., %locator%)]) = 0]
+        'activity' => <<<XPATH
+//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][normalize-space(.) = %locator% ]
+XPATH
+        , 'block' => <<<XPATH
+//div[contains(concat(' ', normalize-space(@class), ' '), ' block ') and
+    (contains(concat(' ', normalize-space(@class), ' '), concat(' ', %locator%, ' ')) or
+     descendant::h2[normalize-space(.) = %locator%] or
+     @aria-label = %locator%)]
 XPATH
         , 'dialogue' => <<<XPATH
 //div[contains(concat(' ', normalize-space(@class), ' '), ' moodle-dialogue ') and
@@ -101,11 +109,19 @@ XPATH
 //div[contains(concat(' ', normalize-space(@class), ' '), ' yui-dialog ') and
     normalize-space(descendant::div[@class='hd']) = %locator%]
 XPATH
-        , 'block' => <<<XPATH
-//div[contains(concat(' ', normalize-space(@class), ' '), ' block ') and
-    (contains(concat(' ', normalize-space(@class), ' '), concat(' ', %locator%, ' ')) or
-     descendant::h2[normalize-space(.) = %locator%] or
-     @aria-label = %locator%)]
+        , 'filemanager' => <<<XPATH
+//div[contains(concat(' ', normalize-space(@class), ' '), ' ffilemanager ')]
+    /descendant::input[@id = //label[contains(normalize-space(string(.)), %locator%)]/@for]
+XPATH
+        , 'list_item' => <<<XPATH
+.//li[contains(normalize-space(.), %locator%)]
+XPATH
+        , 'question' => <<<XPATH
+//div[contains(concat(' ', normalize-space(@class), ' '), ' que ')]
+    [contains(div[@class='content']/div[@class='formulation'], %locator%)]
+XPATH
+        , 'region' => <<<XPATH
+//*[self::div | self::section | self::aside | self::header | self::footer][./@id = %locator%]
 XPATH
         , 'section' => <<<XPATH
 //li[contains(concat(' ', normalize-space(@class), ' '), ' section ')][./descendant::*[self::h3]
@@ -114,24 +130,14 @@ XPATH
 //div[contains(concat(' ', normalize-space(@class), ' '), ' sitetopic ')]
     [./descendant::*[self::h2][normalize-space(.) = %locator%] or %locator% = 'frontpage']
 XPATH
-        , 'activity' => <<<XPATH
-//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][normalize-space(.) = %locator% ]
-XPATH
-        , 'region' => <<<XPATH
-//*[self::div | self::section | self::aside | self::header | self::footer][./@id = %locator%]
+        , 'table' => <<<XPATH
+.//table[(./@id = %locator% or contains(.//caption, %locator%) or contains(concat(' ', normalize-space(@class), ' '), %locator% ))]
 XPATH
         , 'table_row' => <<<XPATH
 .//tr[contains(normalize-space(.), %locator%)]
 XPATH
-        , 'list_item' => <<<XPATH
-.//li[contains(normalize-space(.), %locator%)]
-XPATH
-        , 'filemanager' => <<<XPATH
-//div[contains(concat(' ', normalize-space(@class), ' '), ' ffilemanager ')]
-    /descendant::input[@id = //label[contains(normalize-space(string(.)), %locator%)]/@for]
-XPATH
-        , 'table' => <<<XPATH
-.//table[(./@id = %locator% or contains(.//caption, %locator%) or contains(concat(' ', normalize-space(@class), ' '), %locator% ))]
+        , 'text' => <<<XPATH
+//*[contains(., %locator%)][count(./descendant::*[contains(., %locator%)]) = 0]
 XPATH
     );
 
index ee1f112..95f42fe 100644 (file)
@@ -111,6 +111,9 @@ class behat_util extends testing_util {
         // Disable some settings that are not wanted on test sites.
         set_config('noemailever', 1);
 
+        // Enable web cron.
+        set_config('cronclionly', 0);
+
         // Keeps the current version of database and dataroot.
         self::store_versions_hash();
 
index 630d592..5d1fb24 100644 (file)
@@ -750,19 +750,23 @@ class block_manager {
     }
 
     /**
-     * Convenience method, calls add_block repeatedly for all the blocks in $blocks.
+     * Convenience method, calls add_block repeatedly for all the blocks in $blocks. Optionally, a starting weight
+     * can be used to decide the starting point that blocks are added in the region, the weight is passed to {@link add_block}
+     * and incremented by the position of the block in the $blocks array
      *
      * @param array $blocks array with array keys the region names, and values an array of block names.
-     * @param string $pagetypepattern optional. Passed to @see add_block()
-     * @param string $subpagepattern optional. Passed to @see add_block()
+     * @param string $pagetypepattern optional. Passed to {@link add_block()}
+     * @param string $subpagepattern optional. Passed to {@link add_block()}
+     * @param boolean $showinsubcontexts optional. Passed to {@link add_block()}
+     * @param integer $weight optional. Determines the starting point that the blocks are added in the region.
      */
     public function add_blocks($blocks, $pagetypepattern = NULL, $subpagepattern = NULL, $showinsubcontexts=false, $weight=0) {
+        $initialweight = $weight;
         $this->add_regions(array_keys($blocks), false);
         foreach ($blocks as $region => $regionblocks) {
-            $weight = 0;
-            foreach ($regionblocks as $blockname) {
+            foreach ($regionblocks as $offset => $blockname) {
+                $weight = $initialweight + $offset;
                 $this->add_block($blockname, $region, $weight, $showinsubcontexts, $pagetypepattern, $subpagepattern);
-                $weight += 1;
             }
         }
     }
@@ -2123,12 +2127,12 @@ function blocks_find_block($blockid, $blocksarray) {
 
 // Functions for programatically adding default blocks to pages ================
 
-/**
- * Parse a list of default blocks. See config-dist for a description of the format.
- *
- * @param string $blocksstr
- * @return array
- */
+ /**
 * Parse a list of default blocks. See config-dist for a description of the format.
 *
+  * @param string $blocksstr Determines the starting point that the blocks are added in the region.
+  * @return array the parsed list of default blocks
 */
 function blocks_parse_default_blocks_list($blocksstr) {
     $blocks = array();
     $bits = explode(':', $blocksstr);
@@ -2139,7 +2143,7 @@ function blocks_parse_default_blocks_list($blocksstr) {
         }
     }
     if (!empty($bits)) {
-        $rightbits =trim(array_shift($bits));
+        $rightbits = trim(array_shift($bits));
         if ($rightbits != '') {
             $blocks[BLOCK_POS_RIGHT] = explode(',', $rightbits);
         }
diff --git a/lib/classes/event/badge_awarded.php b/lib/classes/event/badge_awarded.php
new file mode 100644 (file)
index 0000000..4c9e021
--- /dev/null
@@ -0,0 +1,97 @@
+<?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/>.
+
+/**
+ * Badge awarded event.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      - int expiredate: Badge expire timestamp.
+ *      - int badgeissuedid: Badge issued ID.
+ * }
+ *
+ * @package    core
+ * @copyright  2015 James Ballard
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event triggered after a badge is awarded to a user.
+ *
+ * @package    core
+ * @since      Moodle 2.9
+ * @copyright  2015 James Ballard
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class badge_awarded extends base {
+
+    /**
+     * Set basic properties for the event.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'badge';
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventbadgeawarded', 'badges');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->relateduserid' has been awarded the badge with id '".$this->objectid."'.";
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/badges/overview.php', array('id' => $this->objectid));
+    }
+
+    /**
+     * Custom validations.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+
+        if (!isset($this->objectid)) {
+            throw new \coding_exception('The \'objectid\' must be set.');
+        }
+    }
+}
index 4ae156d..78e94f4 100644 (file)
@@ -54,4 +54,41 @@ class portfolio extends base {
     public static function get_manage_url() {
         return new moodle_url('/admin/portfolio.php');
     }
-}
+
+    /**
+     * Defines if there should be a way to uninstall the plugin via the administration UI.
+     * @return boolean
+     */
+    public function is_uninstall_allowed() {
+        return true;
+    }
+
+    /**
+     * Pre-uninstall hook.
+     * This is intended for disabling of plugin, some DB table purging, etc.
+     */
+    public function uninstall_cleanup() {
+        global $DB;
+
+        // Get all instances of this portfolio.
+        $count = $DB->count_records('portfolio_instance', array('plugin' => $this->name));
+        if ($count > 0) {
+            // This portfolio is in use, get the it's ID.
+            $rec = $DB->get_record('portfolio_instance', array('plugin' => $this->name));
+
+            // Remove all records from portfolio_instance_config.
+            $DB->delete_records('portfolio_instance_config', array('instance' => $rec->id));
+            // Remove all records from portfolio_instance_user.
+            $DB->delete_records('portfolio_instance_user', array('instance' => $rec->id));
+            // Remove all records from portfolio_log.
+            $DB->delete_records('portfolio_log', array('portfolio' => $rec->id));
+            // Remove all records from portfolio_tempdata.
+            $DB->delete_records('portfolio_tempdata', array('instance' => $rec->id));
+
+            // Remove the record from the portfolio_instance table.
+            $DB->delete_records('portfolio_instance', array('id' => $rec->id));
+        }
+
+        parent::uninstall_cleanup();
+    }
+}
\ No newline at end of file
diff --git a/lib/classes/requirejs.php b/lib/classes/requirejs.php
new file mode 100644 (file)
index 0000000..9894dc5
--- /dev/null
@@ -0,0 +1,130 @@
+<?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/>.
+
+/**
+ * RequireJS helper functions.
+ *
+ * @package    core
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Collection of requirejs related methods.
+ *
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_requirejs {
+
+    /**
+     * Check a single module exists and return the full path to it.
+     *
+     * The expected location for amd modules is:
+     *  <componentdir>/amd/src/modulename.js
+     *
+     * @param string $component The component determines the folder the js file should be in.
+     * @param string $jsfilename The filename for the module (with the js extension).
+     * @param boolean $debug If true, returns the paths to the original (unminified) source files.
+     * @return array $files An array of mappings from module names to file paths.
+     *                      Empty array if the file does not exist.
+     */
+    public static function find_one_amd_module($component, $jsfilename, $debug = false) {
+        $jsfileroot = core_component::get_component_directory($component);
+        if (!$jsfileroot) {
+            return array();
+        }
+
+        $module = str_replace('.js', '', $jsfilename);
+
+        $srcdir = $jsfileroot . '/amd/build';
+        $minpart = '.min';
+        if ($debug) {
+            $srcdir = $jsfileroot . '/amd/src';
+            $minpart = '';
+        }
+
+        $filename = $srcdir . '/' . $module . $minpart . '.js';
+        if (!file_exists($filename)) {
+            return array();
+        }
+
+        $fullmodulename = $component . '/' . $module;
+        return array($fullmodulename => $filename);
+    }
+
+    /**
+     * Scan the source for AMD modules and return them all.
+     *
+     * The expected location for amd modules is:
+     *  <componentdir>/amd/src/modulename.js
+     *
+     * @param boolean $debug If true, returns the paths to the original (unminified) source files.
+     * @return array $files An array of mappings from module names to file paths.
+     */
+    public static function find_all_amd_modules($debug = false) {
+        global $CFG;
+
+        $jsdirs = array();
+        $jsfiles = array();
+
+        $dir = $CFG->libdir . '/amd';
+        if (!empty($dir) && is_dir($dir)) {
+            $jsdirs['core'] = $dir;
+        }
+        $subsystems = core_component::get_core_subsystems();
+        foreach ($subsystems as $subsystem => $dir) {
+            if (!empty($dir) && is_dir($dir . '/amd')) {
+                $jsdirs[$subsystem] = $dir . '/amd';
+            }
+        }
+        $plugintypes = core_component::get_plugin_types();
+        foreach ($plugintypes as $type => $dir) {
+            $plugins = core_component::get_plugin_list_with_file($type, 'amd', false);
+            foreach ($plugins as $plugin => $dir) {
+                if (!empty($dir) && is_dir($dir)) {
+                    $jsdirs[$type . '_' . $plugin] = $dir;
+                }
+            }
+        }
+
+        foreach ($jsdirs as $component => $dir) {
+            $srcdir = $dir . '/build';
+            if ($debug) {
+                $srcdir = $dir . '/src';
+            }
+            $items = new RecursiveDirectoryIterator($srcdir);
+            foreach ($items as $item) {
+                $extension = $item->getExtension();
+                if ($extension === 'js') {
+                    $filename = str_replace('.min', '', $item->getBaseName('.js'));
+                    // We skip lazy loaded modules.
+                    if (strpos($filename, '-lazy') === false) {
+                        $modulename = $component . '/' . $filename;
+                        $jsfiles[$modulename] = $item->getRealPath();
+                    }
+                }
+                unset($item);
+            }
+            unset($items);
+        }
+
+        return $jsfiles;
+    }
+
+}
index f5722db..8be9a2d 100644 (file)
@@ -44,7 +44,7 @@ class memcached extends handler {
     protected $acquiretimeout = 120;
     /**
      * @var int $lockexpire how long to wait before expiring the lock so that other requests
-     * may continue execution, ignored if memcached <= 2.1.0.
+     * may continue execution, ignored if PECL memcached is below version 2.2.0.
      */
     protected $lockexpire = 7200;
 
@@ -86,7 +86,7 @@ class memcached extends handler {
      * @return bool success
      */
     public function start() {
-        // NOTE: memcached <= 2.1.0 expires session locks automatically after max_execution_time,
+        // NOTE: memcached before 2.2.0 expires session locks automatically after max_execution_time,
         //       this leads to major difference compared to other session drivers that timeout
         //       and stop the second request execution instead.
 
@@ -119,7 +119,7 @@ class memcached extends handler {
         ini_set('memcached.sess_prefix', $this->prefix);
         ini_set('memcached.sess_locking', '1'); // Locking is required!
 
-        // Try to configure lock and expire timeouts - ignored if memcached <=2.1.0.
+        // Try to configure lock and expire timeouts - ignored if memcached is before version 2.2.0.
         ini_set('memcached.sess_lock_max_wait', $this->acquiretimeout);
         ini_set('memcached.sess_lock_expire', $this->lockexpire);
     }
index 5c6bef2..36772ee 100644 (file)
@@ -85,53 +85,31 @@ class manager {
      */
     public static function reset_scheduled_tasks_for_component($componentname) {
         global $DB;
-        $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
-
-        if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10, 60)) {
-            throw new \moodle_exception('locktimeout');
-        }
         $tasks = self::load_default_scheduled_tasks_for_component($componentname);
 
-        $tasklocks = array();
         foreach ($tasks as $taskid => $task) {
             $classname = get_class($task);
             if (strpos($classname, '\\') !== 0) {
                 $classname = '\\' . $classname;
             }
 
-            // For tasks, the first run should also follow the schedule.
-            $task->set_next_run_time($task->get_next_scheduled_time());
-
-            // If there is an existing task with a custom schedule, do not override it.
-            $currenttask = self::get_scheduled_task($classname);
-            if ($currenttask && $currenttask->is_customised()) {
-                $tasks[$taskid] = $currenttask;
-            }
-
-            if (!$lock = $cronlockfactory->get_lock($classname, 10, 60)) {
-                // Could not get all the locks required - release all locks and fail.
-                foreach ($tasklocks as $tasklock) {
-                    $tasklock->release();
+            if ($currenttask = self::get_scheduled_task($classname)) {
+                if ($currenttask->is_customised()) {
+                    // If there is an existing task with a custom schedule, do not override it.
+                    continue;
                 }
-                $cronlock->release();
-                throw new \moodle_exception('locktimeout');
-            }
-            $tasklocks[] = $lock;
-        }
 
-        // Got a lock on cron and all the tasks for this component, time to reset the config.
-        $DB->delete_records('task_scheduled', array('component' => $componentname));
-        foreach ($tasks as $task) {
-            $record = self::record_from_scheduled_task($task);
-            $DB->insert_record('task_scheduled', $record);
-        }
+                // Update the record from the default task data.
+                self::configure_scheduled_task($task);
+            } else {
+                // Ensure that the first run follows the schedule.
+                $task->set_next_run_time($task->get_next_scheduled_time());
 
-        // Release the locks.
-        foreach ($tasklocks as $tasklock) {
-            $tasklock->release();
+                // Insert the new task in the database.
+                $record = self::record_from_scheduled_task($task);
+                $DB->insert_record('task_scheduled', $record);
+            }
         }
-
-        $cronlock->release();
     }
 
     /**
@@ -160,20 +138,11 @@ class manager {
      */
     public static function configure_scheduled_task(scheduled_task $task) {
         global $DB;
-        $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
-
-        if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10, 60)) {
-            throw new \moodle_exception('locktimeout');
-        }
 
         $classname = get_class($task);
         if (strpos($classname, '\\') !== 0) {
             $classname = '\\' . $classname;
         }
-        if (!$lock = $cronlockfactory->get_lock($classname, 10, 60)) {
-            $cronlock->release();
-            throw new \moodle_exception('locktimeout');
-        }
 
         $original = $DB->get_record('task_scheduled', array('classname'=>$classname), 'id', MUST_EXIST);
 
@@ -182,8 +151,6 @@ class manager {
         $record->nextruntime = $task->get_next_scheduled_time();
         $result = $DB->update_record('task_scheduled', $record);
 
-        $lock->release();
-        $cronlock->release();
         return $result;
     }
 
index 972a30f..cac0f6e 100644 (file)
@@ -323,8 +323,11 @@ class component_installer {
     /// Move current revision to a safe place
         $destinationdir = $CFG->dataroot.'/'.$this->destpath;
         $destinationcomponent = $destinationdir.'/'.$this->componentname;
-        @remove_dir($destinationcomponent.'_old');     //Deleting possible old components before
-        @rename ($destinationcomponent, $destinationcomponent.'_old');  //Moving to a safe place
+        @remove_dir($destinationcomponent.'_old');     // Deleting a possible old version.
+
+        // Moving to a safe place.
+        @rename($destinationcomponent, $destinationcomponent.'_old');
+
     /// Unzip new version
         if (!unzip_file($zipfile, $destinationdir, false)) {
         /// Error so, go back to the older
index 86568ed..72e5d51 100644 (file)
@@ -168,9 +168,11 @@ function min_enable_zlib_compression() {
  * Note: ".php" is NOT allowed in slasharguments,
  *       it is intended for ASCII characters only.
  *
+ * @param boolean $clean - Should we do cleaning on this path argument. If you set this
+ *                         to false you MUST be very careful and do the cleaning manually.
  * @return string
  */
-function min_get_slash_argument() {
+function min_get_slash_argument($clean = true) {
     // Note: This code has to work in the same cases as normal get_file_argument(),
     //       but at the same time it may be simpler because we do not have to deal
     //       with encodings and other tricky stuff.
@@ -180,7 +182,12 @@ function min_get_slash_argument() {
     if (!empty($_GET['file']) and strpos($_GET['file'], '/') === 0) {
         // Server is using url rewriting, most probably IIS.
         // Always clean the result of this function as it may be used in unsafe calls to send_file.
-        return min_clean_param($_GET['file'], 'SAFEPATH');
+        $relativepath = $_GET['file'];
+        if ($clean) {
+            $relativepath = min_clean_param($relativepath, 'SAFEPATH');
+        }
+
+        return $relativepath;
 
     } else if (stripos($_SERVER['SERVER_SOFTWARE'], 'iis') !== false) {
         if (isset($_SERVER['PATH_INFO']) and $_SERVER['PATH_INFO'] !== '') {
@@ -199,5 +206,8 @@ function min_get_slash_argument() {
     }
 
     // Always clean the result of this function as it may be used in unsafe calls to send_file.
-    return min_clean_param($relativepath, 'SAFEPATH');
+    if ($clean) {
+        $relativepath = min_clean_param($relativepath, 'SAFEPATH');
+    }
+    return $relativepath;
 }
index 5815826..a6391b9 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20150211" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20150224" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
       <INDEXES>
         <INDEX NAME="useridfrom" UNIQUE="false" FIELDS="useridfrom"/>
         <INDEX NAME="useridto" UNIQUE="false" FIELDS="useridto"/>
+        <INDEX NAME="useridfromto" UNIQUE="false" FIELDS="useridfrom, useridto"/>
       </INDEXES>
     </TABLE>
     <TABLE NAME="message_read" COMMENT="Stores all messages that have been read">
       <INDEXES>
         <INDEX NAME="useridfrom" UNIQUE="false" FIELDS="useridfrom"/>
         <INDEX NAME="useridto" UNIQUE="false" FIELDS="useridto"/>
+        <INDEX NAME="useridfromto" UNIQUE="false" FIELDS="useridfrom, useridto"/>
       </INDEXES>
     </TABLE>
     <TABLE NAME="message_contacts" COMMENT="Maintains lists of relationships between users">
         <KEY NAME="pushid-userid" TYPE="unique" FIELDS="pushid, userid"/>
         <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
       </KEYS>
+      <INDEXES>
+        <INDEX NAME="uuid-userid" UNIQUE="false" FIELDS="uuid, userid" COMMENT="Index on uuid and userid"/>
+      </INDEXES>
     </TABLE>
     <TABLE NAME="user_password_resets" COMMENT="table tracking password reset confirmation tokens">
       <FIELDS>
index 9aa27c1..1b60c7a 100644 (file)
@@ -303,6 +303,15 @@ $functions = array(
         'type'        => 'write',
     ),
 
+    'core_group_get_course_user_groups' => array(
+        'classname'     => 'core_group_external',
+        'methodname'    => 'get_course_user_groups',
+        'classpath'     => 'group/externallib.php',
+        'description'   => 'Returns all groups in specified course for the specified user.',
+        'type'          => 'read',
+        'capabilities'  => 'moodle/course:managegroups',
+    ),
+
     // === file related functions ===
 
     'moodle_file_get_files' => array(
@@ -465,6 +474,15 @@ $functions = array(
         'capabilities'=> '',
     ),
 
+    'core_user_remove_user_device' => array(
+        'classname'     => 'core_user_external',
+        'methodname'    => 'remove_user_device',
+        'classpath'     => 'user/externallib.php',
+        'description'   => 'Remove a user device from the Moodle database.',
+        'type'          => 'write',
+        'capabilities'  => '',
+    ),
+
     // === enrol related functions ===
 
     'core_enrol_get_enrolled_users_with_capability' => array(
@@ -985,7 +1003,9 @@ $services = array(
             'core_message_get_contacts',
             'core_message_search_contacts',
             'core_message_get_blocked_users',
-            'gradereport_user_get_grades_table'
+            'gradereport_user_get_grades_table',
+            'core_group_get_course_user_groups',
+            'core_user_remove_user_device',
             ),
         'enabled' => 0,
         'restrictedusers' => 0,
index 3526517..8dc4f4e 100644 (file)
@@ -125,8 +125,8 @@ $tasks = array(
     array(
         'classname' => 'core\task\create_contexts_task',
         'blocking' => 1,
-        'minute' => '*',
-        'hour' => '*',
+        'minute' => '0',
+        'hour' => '0',
         'day' => '*',
         'dayofweek' => '*',
         'month' => '*'
index 2b03cf6..7b9944a 100644 (file)
@@ -4177,5 +4177,44 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2015021100.00);
     }
 
+    if ($oldversion < 2015022401.00) {
+
+        // Define index useridfromto (not unique) to be added to message.
+        $table = new xmldb_table('message');
+        $index = new xmldb_index('useridfromto', XMLDB_INDEX_NOTUNIQUE, array('useridfrom', 'useridto'));
+
+        // Conditionally launch add index useridfromto.
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Define index useridfromto (not unique) to be added to message_read.
+        $table = new xmldb_table('message_read');
+        $index = new xmldb_index('useridfromto', XMLDB_INDEX_NOTUNIQUE, array('useridfrom', 'useridto'));
+
+        // Conditionally launch add index useridfromto.
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2015022401.00);
+    }
+
+    if ($oldversion < 2015022500.00) {
+        $table = new xmldb_table('user_devices');
+        $index = new xmldb_index('uuid-userid', XMLDB_INDEX_NOTUNIQUE, array('uuid', 'userid'));
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+        upgrade_main_savepoint(true, 2015022500.00);
+    }
+
+    if ($oldversion < 2015030400.00) {
+        // We have long since switched to storing timemodified per hub rather than a single 'registered' timestamp.
+        unset_config('registered');
+        upgrade_main_savepoint(true, 2015030400.00);
+    }
+
     return true;
 }
index 9645c9a..e04ada7 100644 (file)
@@ -978,7 +978,8 @@ class oci_native_moodle_database extends moodle_database {
 
                     default: // Bind as CHAR (applying dirty hack)
                         // TODO: Optimise
-                        oci_bind_by_name($stmt, $key, $this->oracle_dirty_hack($tablename, $columnname, $params[$key]));
+                        $params[$key] = $this->oracle_dirty_hack($tablename, $columnname, $params[$key]);
+                        oci_bind_by_name($stmt, $key, $params[$key]);
                 }
             }
         }
index 5306249..fbd5c05 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js differ
index 12f0197..17f3959 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js differ
index c4a9cdb..2a54f7b 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js differ
index 7bf8f2b..93e4ede 100644 (file)
@@ -298,6 +298,21 @@ Y.extend(Editor, Y.Base, {
     setupAutomaticPolling: function() {
         this._registerEventHandle(this.editor.on(['keyup', 'paste', 'cut'], this.updateOriginal, this));
 
+        // Call this.updateOriginal after dropped content has been processed.
+        this._registerEventHandle(this.editor.on('drop', this.updateOriginalDelayed, this));
+
+        return this;
+    },
+
+    /**
+     * Calls updateOriginal on a short timer to allow native event handlers to run first.
+     *
+     * @method updateOriginalDelayed
+     * @chainable
+     */
+    updateOriginalDelayed: function() {
+        Y.soon(Y.bind(this.updateOriginal, this));
+
         return this;
     },
 
index b0897db..a0526c5 100644 (file)
@@ -108,11 +108,12 @@ class core_external extends external_api {
      * @return string
      * @since Moodle 2.4
      */
-    public static function get_string($stringid, $component = 'moodle', $stringparams = array()) {
+    public static function get_string($stringid, $component = 'moodle', $lang = null, $stringparams = array()) {
         $params = self::validate_parameters(self::get_string_parameters(),
-                      array('stringid'=>$stringid, 'component' => $component, 'stringparams' => $stringparams));
+                      array('stringid'=>$stringid, 'component' => $component, 'lang' => $lang, 'stringparams' => $stringparams));
 
-        return get_string($params['stringid'], $params['component'],
+        $stringmanager = get_string_manager();
+        return $stringmanager->get_string($params['stringid'], $params['component'],
             core_external::format_string_parameters($params['stringparams']), $params['lang']);
     }
 
@@ -163,11 +164,12 @@ class core_external extends external_api {
     public static function get_strings($strings) {
         $params = self::validate_parameters(self::get_strings_parameters(),
                       array('strings'=>$strings));
+        $stringmanager = get_string_manager();
 
         $translatedstrings = array();
         foreach($params['strings'] as $string) {
 
-            if (empty($string['lang'])) {
+            if (!empty($string['lang'])) {
                 $lang = $string['lang'];
             } else {
                 $lang = current_language();
@@ -177,7 +179,7 @@ class core_external extends external_api {
                 'stringid' => $string['stringid'],
                 'component' => $string['component'],
                 'lang' => $lang,
-                'string' => get_string($string['stringid'], $string['component'],
+                'string' => $stringmanager->get_string($string['stringid'], $string['component'],
                     core_external::format_string_parameters($string['stringparams']), $lang));
         }
 
index d0b0e16..85aed00 100644 (file)
@@ -41,7 +41,7 @@ class core_external_testcase extends externallib_advanced_testcase {
         $service->id = 12;
 
         // String with two parameters.
-        $returnedstring = core_external::get_string('addservice', 'webservice',
+        $returnedstring = core_external::get_string('addservice', 'webservice', null,
                 array(array('name' => 'name', 'value' => $service->name),
                       array('name' => 'id', 'value' => $service->id)));
 
@@ -53,7 +53,7 @@ class core_external_testcase extends externallib_advanced_testcase {
 
         // String with one parameter.
         $acapname = 'A capability name';
-        $returnedstring = core_external::get_string('missingrequiredcapability', 'webservice',
+        $returnedstring = core_external::get_string('missingrequiredcapability', 'webservice', null,
                 array(array('value' => $acapname)));
 
         // We need to execute the return values cleaning process to simulate the web service server.
@@ -73,7 +73,7 @@ class core_external_testcase extends externallib_advanced_testcase {
 
         // String with two parameter but one is invalid (not named).
         $this->setExpectedException('moodle_exception');
-        $returnedstring = core_external::get_string('addservice', 'webservice',
+        $returnedstring = core_external::get_string('addservice', 'webservice', null,
                 array(array('value' => $service->name),
                       array('name' => 'id', 'value' => $service->id)));
     }
@@ -84,6 +84,8 @@ class core_external_testcase extends externallib_advanced_testcase {
     public function test_get_strings() {
         $this->resetAfterTest(true);
 
+        $stringmanager = get_string_manager();
+
         $service = new stdClass();
         $service->name = 'Dummy Service';
         $service->id = 12;
@@ -94,16 +96,20 @@ class core_external_testcase extends externallib_advanced_testcase {
                         'stringid' => 'addservice', 'component' => 'webservice',
                         'stringparams' => array(array('name' => 'name', 'value' => $service->name),
                               array('name' => 'id', 'value' => $service->id)
-                        )
+                        ),
+                        'lang' => 'en'
                     ),
-                    array('stringid' =>  'addaservice', 'component' => 'webservice')
+                    array('stringid' =>  'addaservice', 'component' => 'webservice', 'lang' => 'en')
                 ));
 
         // We need to execute the return values cleaning process to simulate the web service server.
         $returnedstrings = external_api::clean_returnvalue(core_external::get_strings_returns(), $returnedstrings);
 
         foreach($returnedstrings as $returnedstring) {
-            $corestring = get_string($returnedstring['stringid'], $returnedstring['component'], $service);
+            $corestring = $stringmanager->get_string($returnedstring['stringid'],
+                                                     $returnedstring['component'],
+                                                     $service,
+                                                     'en');
             $this->assertSame($corestring, $returnedstring['string']);
         }
     }
index b346311..f51facd 100644 (file)
@@ -656,6 +656,7 @@ class zip_archive extends file_archive {
                             case 'ISO-8859-6': $encoding = 'CP720'; break;
                             case 'ISO-8859-7': $encoding = 'CP737'; break;
                             case 'ISO-8859-8': $encoding = 'CP862'; break;
+                            case 'EUC-JP':
                             case 'UTF-8':
                                 if ($winchar = get_string('localewincharset', 'langconfig')) {
                                     // Most probably works only for zh_cn,
index b6f0c17..88564a7 100644 (file)
@@ -2905,7 +2905,7 @@ class MoodleQuickForm_Rule_Required extends HTML_QuickForm_Rule {
         global $CFG;
         if (!empty($CFG->strictformsrequired)) {
             if (!empty($format) && $format == FORMAT_HTML) {
-                return array('', "{jsVar}.replace(/(<[^img|hr|canvas]+>)|&nbsp;|\s+/ig, '') == ''");
+                return array('', "{jsVar}.replace(/(<(?!img|hr|canvas)[^>]*>)|&nbsp;|\s+/ig, '') == ''");
             } else {
                 return array('', "{jsVar}.replace(/^\s+$/g, '') == ''");
             }
index 431f0c7..46bcf4c 100644 (file)
@@ -767,6 +767,8 @@ class grade_category extends grade_object {
      * Set the flags on the grade_grade items to indicate how individual grades are used
      * in the aggregation.
      *
+     * WARNING: This function is called a lot during gradebook recalculation, be very performance considerate.
+     *
      * @param int $userid The user we have aggregated the grades for.
      * @param array $usedweights An array with keys for each of the grade_item columns included in the aggregation. The value are the relative weight.
      * @param array $novalue An array with keys for each of the grade_item columns skipped because
@@ -779,44 +781,32 @@ class grade_category extends grade_object {
     private function set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit) {
         global $DB;
 
-        // First set them all to weight null and status = 'unknown'.
-        if ($allitems = grade_item::fetch_all(array('categoryid'=>$this->id))) {
-            list($itemsql, $itemlist) = $DB->get_in_or_equal(array_keys($allitems), SQL_PARAMS_NAMED, 'g');
+        // Reset aggregation to unknown and 0 for all grade items for this user and category.
+        $params = array('categoryid' => $this->id, 'userid' => $userid);
+        $itemssql = "SELECT id
+                       FROM {grade_items}
+                      WHERE categoryid = :categoryid";
 
-            $itemlist['userid'] = $userid;
+        $sql = "UPDATE {grade_grades}
+                   SET aggregationstatus = 'unknown',
+                       aggregationweight = 0
+                 WHERE userid = :userid
+                   AND itemid IN ($itemssql)";
 
-            $DB->set_field_select('grade_grades',
-                                  'aggregationstatus',
-                                  'unknown',
-                                  "itemid $itemsql AND userid = :userid",
-                                  $itemlist);
-            $DB->set_field_select('grade_grades',
-                                  'aggregationweight',
-                                  0,
-                                  "itemid $itemsql AND userid = :userid",
-                                  $itemlist);
-        }
+        $DB->execute($sql, $params);
 
-        // Included.
+        // Included with weights.
         if (!empty($usedweights)) {
             // The usedweights items are updated individually to record the weights.
             foreach ($usedweights as $gradeitemid => $contribution) {
-                $DB->set_field_select('grade_grades',
-                                      'aggregationweight',
-                                      $contribution,
-                                      "itemid = :itemid AND userid = :userid",
-                                      array('itemid'=>$gradeitemid, 'userid'=>$userid));
-            }
+                $sql = "UPDATE {grade_grades}
+                           SET aggregationstatus = 'used',
+                               aggregationweight = :contribution
+                         WHERE itemid = :itemid AND userid = :userid";
 
-            // Now set the status flag for all these weights.
-            list($itemsql, $itemlist) = $DB->get_in_or_equal(array_keys($usedweights), SQL_PARAMS_NAMED, 'g');
-            $itemlist['userid'] = $userid;
-
-            $DB->set_field_select('grade_grades',
-                                  'aggregationstatus',
-                                  'used',
-                                  "itemid $itemsql AND userid = :userid",
-                                  $itemlist);
+                $params = array('contribution' => $contribution, 'itemid' => $gradeitemid, 'userid' => $userid);
+                $DB->execute($sql, $params);
+            }
         }
 
         // No value.
@@ -844,6 +834,7 @@ class grade_category extends grade_object {
                                   "itemid $itemsql AND userid = :userid",
                                   $itemlist);
         }
+
         // Extra credit.
         if (!empty($extracredit)) {
             list($itemsql, $itemlist) = $DB->get_in_or_equal(array_keys($extracredit), SQL_PARAMS_NAMED, 'g');
index b3e1726..8b6af7c 100644 (file)
@@ -597,14 +597,32 @@ M.util.init_block_hider = function(Y, config) {
                     this.set('block', '#'+this.get('id'));
                     var b = this.get('block'),
                         t = b.one('.title'),
-                        a = null;
+                        a = null,
+                        hide,
+                        show;
                     if (t && (a = t.one('.block_action'))) {
-                        var hide = Y.Node.create('<img class="block-hider-hide" tabindex="0" alt="'+config.tooltipVisible+'" title="'+config.tooltipVisible+'" />');
-                        hide.setAttribute('src', this.get('iconVisible')).on('click', this.updateState, this, true);
+                        hide = Y.Node.create('<img />')
+                            .addClass('block-hider-hide')
+                            .setAttrs({
+                                alt:        config.tooltipVisible,
+                                src:        this.get('iconVisible'),
+                                tabindex:   0,
+                                'title':    config.tooltipVisible
+                            });
                         hide.on('keypress', this.updateStateKey, this, true);
-                        var show = Y.Node.create('<img class="block-hider-show" tabindex="0" alt="'+config.tooltipHidden+'" title="'+config.tooltipHidden+'" />');
-                        show.setAttribute('src', this.get('iconHidden')).on('click', this.updateState, this, false);
+                        hide.on('click', this.updateState, this, true);
+
+                        show = Y.Node.create('<img />')
+                            .addClass('block-hider-show')
+                            .setAttrs({
+                                alt:        config.tooltipHidden,
+                                src:        this.get('iconHidden'),
+                                tabindex:   0,
+                                'title':    config.tooltipHidden
+                            });
                         show.on('keypress', this.updateStateKey, this, false);
+                        show.on('click', this.updateState, this, false);
+
                         a.insert(show, 0).insert(hide, 0);
                     }
                 },
index 9a2ad64..9725e35 100644 (file)
@@ -536,15 +536,69 @@ class core_media_player_youtube extends core_media_player_external {
 
         self::pick_video_size($width, $height);
 
+        $params = '';
+        $start = self::get_start_time($url);
+        if ($start > 0) {
+            $params .= "start=$start&";
+        }
+
+        $listid = $url->param('list');
+        // Check for non-empty but valid playlist ID.
+        if (!empty($listid) && !preg_match('/[^a-zA-Z0-9\-_]/', $listid)) {
+            // This video is part of a playlist, and we want to embed it as such.
+            $params .= "list=$listid&";
+        }
+
         return <<<OET
 <span class="mediaplugin mediaplugin_youtube">
 <iframe title="$info" width="$width" height="$height"
-  src="https://www.youtube.com/embed/$videoid?rel=0&wmode=transparent" frameborder="0" allowfullscreen="1"></iframe>
+  src="https://www.youtube.com/embed/$videoid?{$params}rel=0&wmode=transparent" frameborder="0" allowfullscreen="1"></iframe>
 </span>
 OET;
 
     }
 
+    /**
+     * Check for start time parameter.  Note that it's in hours/mins/secs in the URL,
+     * but the embedded player takes only a number of seconds as the "start" parameter.
+     * @param moodle_url $url URL of video to be embedded.
+     * @return int Number of seconds video should start at.
+     */
+    protected static function get_start_time($url) {
+        $matches = array();
+        $seconds = 0;
+
+        $rawtime = $url->param('t');
+        if (empty($rawtime)) {
+            $rawtime = $url->param('start');
+        }
+
+        if (is_numeric($rawtime)) {
+            // Start time already specified as a number of seconds; ensure it's an integer.
+            $seconds = $rawtime;
+        } else if (preg_match('/(\d+?h)?(\d+?m)?(\d+?s)?/i', $rawtime, $matches)) {
+            // Convert into a raw number of seconds, as that's all embedded players accept.
+            for ($i = 1; $i < count($matches); $i++) {
+                if (empty($matches[$i])) {
+                    continue;
+                }
+                $part = str_split($matches[$i], strlen($matches[$i]) - 1);
+                switch ($part[1]) {
+                    case 'h':
+                        $seconds += 3600 * $part[0];
+                        break;
+                    case 'm':
+                        $seconds += 60 * $part[0];
+                        break;
+                    default:
+                        $seconds += $part[0];
+                }
+            }
+        }
+
+        return intval($seconds);
+    }
+
     protected function get_regex() {
         // Regex for standard youtube link
          $link = '(youtube(-nocookie)?\.com/(?:watch\?v=|v/))';
index 7b64921..7d8ea2f 100644 (file)
@@ -2853,10 +2853,6 @@ function require_login($courseorid = null, $autologinguest = true, $cm = null, $
                 $modinfo = get_fast_modinfo($course);
                 $cm = $modinfo->get_cm($cm->id);
             }
-            $PAGE->set_cm($cm, $course); // Set's up global $COURSE.
-            $PAGE->set_pagelayout('incourse');
-        } else {
-            $PAGE->set_course($course); // Set's up global $COURSE.
         }
     } else {
         // Do not touch global $COURSE via $PAGE->set_course(),
@@ -2960,6 +2956,13 @@ function require_login($courseorid = null, $autologinguest = true, $cm = null, $
 
     // Do not bother admins with any formalities.
     if (is_siteadmin()) {
+        // Set the global $COURSE.
+        if ($cm) {
+            $PAGE->set_cm($cm, $course);
+            $PAGE->set_pagelayout('incourse');
+        } else if (!empty($courseorid)) {
+            $PAGE->set_course($course);
+        }
         // Set accesstime or the user will appear offline which messes up messaging.
         user_accesstime_log($course->id);
         return;
@@ -3017,6 +3020,7 @@ function require_login($courseorid = null, $autologinguest = true, $cm = null, $
                 if ($preventredirect) {
                     throw new require_login_exception('Course is hidden');
                 }
+                $PAGE->set_context(null);
                 // We need to override the navigation URL as the course won't have been added to the navigation and thus
                 // the navigation will mess up when trying to find it.
                 navigation_node::override_active_url(new moodle_url('/'));
@@ -3037,6 +3041,7 @@ function require_login($courseorid = null, $autologinguest = true, $cm = null, $
                 if ($preventredirect) {
                     throw new require_login_exception('Invalid course login-as access');
                 }
+                $PAGE->set_context(null);
                 echo $OUTPUT->header();
                 notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/');
             }
@@ -3138,6 +3143,15 @@ function require_login($courseorid = null, $autologinguest = true, $cm = null, $
         }
     }
 
+    // Set the global $COURSE.
+    // TODO MDL-49434: setting current course/cm should be after the check $cm->uservisible .
+    if ($cm) {
+        $PAGE->set_cm($cm, $course);
+        $PAGE->set_pagelayout('incourse');
+    } else if (!empty($courseorid)) {
+        $PAGE->set_course($course);
+    }
+
     // Check visibility of activity to current user; includes visible flag, conditional availability, etc.
     if ($cm && !$cm->uservisible) {
         if ($preventredirect) {
index 46c2da2..31c4d0d 100644 (file)
@@ -35,11 +35,12 @@ defined('MOODLE_INTERNAL') || die();
  *
  * Typical usage would be
  * <pre>
- *     $PAGE->requires->js_init_call('M.mod_forum.init_view');
+ *     $PAGE->requires->js_call_amd('mod_forum/view', 'init');
  * </pre>
  *
- * It also supports obsoleted coding style withouth YUI3 modules.
+ * It also supports obsoleted coding style with/without YUI3 modules.
  * <pre>
+ *     $PAGE->requires->js_init_call('M.mod_forum.init_view');
  *     $PAGE->requires->css('/mod/mymod/userstyles.php?id='.$id); // not overridable via themes!
  *     $PAGE->requires->js('/mod/mymod/script.js');
  *     $PAGE->requires->js('/mod/mymod/small_but_urgent.js', true);
@@ -78,6 +79,11 @@ class page_requirements_manager {
      */
     protected $jsincludes = array('head'=>array(), 'footer'=>array());
 
+    /**
+     * @var array Inline scripts using RequireJS module loading.
+     */
+    protected $amdjscode = array('');
+
     /**
      * @var array List of needed function calls
      */
@@ -943,6 +949,52 @@ class page_requirements_manager {
         $this->jscalls[$where][] = array($function, $arguments, $delay);
     }
 
+    /**
+     * This function appends a block of code to the AMD specific javascript block executed
+     * in the page footer, just after loading the requirejs library.
+     *
+     * The code passed here can rely on AMD module loading, e.g. require('jquery', function($) {...});
+     *
+     * @param string $code The JS code to append.
+     */
+    public function js_amd_inline($code) {
+        $this->amdjscode[] = $code;
+    }
+
+    /**
+     * This function creates a minimal JS script that requires and calls a single function from an AMD module with arguments.
+     * If it is called multiple times, it will be executed multiple times.
+     *
+     * @param string $fullmodule The format for module names is <component name>/<module name>.
+     * @param string $func The function from the module to call
+     * @param array $params The params to pass to the function. They will be json encoded, so no nasty classes/types please.
+     */
+    public function js_call_amd($fullmodule, $func, $params = array()) {
+        global $CFG;
+
+        list($component, $module) = explode('/', $fullmodule, 2);
+
+        $component = clean_param($component, PARAM_COMPONENT);
+        $module = clean_param($module, PARAM_ALPHANUMEXT);
+        $func = clean_param($func, PARAM_ALPHANUMEXT);
+
+        $jsonparams = array();
+        foreach ($params as $param) {
+            $jsonparams[] = json_encode($param);
+        }
+        $strparams = implode(', ', $jsonparams);
+        if ($CFG->debugdeveloper) {
+            $toomanyparamslimit = 1024;
+            if (strlen($strparams) > $toomanyparamslimit) {
+                debugging('Too many params passed to js_call_amd("' . $fullmodule . '", "' . $func . '")', DEBUG_DEVELOPER);
+            }
+        }
+
+        $js = 'require(["' . $component . '/' . $module . '"], function(amd) { amd.' . $func . '(' . $strparams . '); });';
+
+        $this->js_amd_inline($js);
+    }
+
     /**
      * Creates a JavaScript function call that requires one or more modules to be loaded.
      *
@@ -952,6 +1004,9 @@ class page_requirements_manager {
      *     - Moodle modules  [moodle-*]
      *     - Gallery modules [gallery-*]
      *
+     * Before writing new code that makes extensive use of YUI, you should consider it's replacement AMD/JQuery.
+     * @see js_call_amd()
+     *
      * @param array|string $modules One or more modules
      * @param string $function The function to call once modules have been loaded
      * @param array $arguments An array of arguments to pass to the function
@@ -1205,6 +1260,47 @@ class page_requirements_manager {
         return '';
     }
 
+    /**
+     * Returns js code to load amd module loader, then insert inline script tags
+     * that contain require() calls using RequireJS.
+     * @return string
+     */
+    protected function get_amd_footercode() {
+        global $CFG;
+        $output = '';
+        $jsrev = $this->get_jsrev();
+
+        $jsloader = new moodle_url($CFG->httpswwwroot . '/lib/javascript.php');
+        $jsloader->set_slashargument('/' . $jsrev . '/');
+        $requirejsloader = new moodle_url($CFG->httpswwwroot . '/lib/requirejs.php');
+        $requirejsloader->set_slashargument('/' . $jsrev . '/');
+
+        $requirejsconfig = file_get_contents($CFG->dirroot . '/lib/requirejs/moodle-config.js');
+
+        // No extension required unless slash args is disabled.
+        $jsextension = '.js';
+        if (!empty($CFG->slasharguments)) {
+            $jsextension = '';
+        }
+
+        $requirejsconfig = str_replace('[BASEURL]', $requirejsloader, $requirejsconfig);
+        $requirejsconfig = str_replace('[JSURL]', $jsloader, $requirejsconfig);
+        $requirejsconfig = str_replace('[JSEXT]', $jsextension, $requirejsconfig);
+
+        $output .= html_writer::script($requirejsconfig);
+        if ($CFG->debugdeveloper) {
+            $output .= html_writer::script('', $this->js_fix_url('/lib/requirejs/require.js'));
+        } else {
+            $output .= html_writer::script('', $this->js_fix_url('/lib/requirejs/require.min.js'));
+        }
+
+        // First include must be to a module with no dependencies, this prevents multiple requests.
+        $prefix = "require(['core/first'], function() {\n";
+        $suffix = "\n});";
+        $output .= html_writer::script($prefix . implode(";\n", $this->amdjscode) . $suffix);
+        return $output;
+    }
+
     /**
      * Returns basic YUI3 JS loading code.
      * YUI3 is using autoloading of both CSS and JS code.
@@ -1426,9 +1522,13 @@ class page_requirements_manager {
      */
     public function get_end_code() {
         global $CFG;
+        $output = '';
+
+        // Call amd init functions.
+        $output .= $this->get_amd_footercode();
 
         // Add other requested modules.
-        $output = $this->get_extra_modules_code();
+        $output .= $this->get_extra_modules_code();
 
         $this->js_init_code('M.util.js_complete("init");', true);
 
diff --git a/lib/requirejs.php b/lib/requirejs.php
new file mode 100644 (file)
index 0000000..7cc8d0f
--- /dev/null
@@ -0,0 +1,144 @@
+<?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 file is serving optimised JS for RequireJS.
+ *
+ * @package    core
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// Disable moodle specific debug messages and any errors in output,
+// comment out when debugging or better look into error log!
+define('NO_DEBUG_DISPLAY', true);
+
+// We need just the values from config.php and minlib.php.
+define('ABORT_AFTER_CONFIG', true);
+require('../config.php'); // This stops immediately at the beginning of lib/setup.php.
+require_once("$CFG->dirroot/lib/jslib.php");
+require_once("$CFG->dirroot/lib/classes/requirejs.php");
+
+$slashargument = min_get_slash_argument();
+if (!$slashargument) {
+    // The above call to min_get_slash_argument should always work.
+    die('Invalid request');
+}
+
+$slashargument = ltrim($slashargument, '/');
+if (substr_count($slashargument, '/') < 1) {
+    header('HTTP/1.0 404 not found');
+    die('Slash argument must contain both a revision and a file path');
+}
+// Split into revision and module name.
+list($rev, $file) = explode('/', $slashargument, 2);
+$rev  = min_clean_param($rev, 'INT');
+$file = '/' . min_clean_param($file, 'SAFEPATH');
+
+// Only load js files from the js modules folder from the components.
+$jsfiles = array();
+list($unused, $component, $module) = explode('/', $file, 3);
+
+// No subdirs allowed - only flat module structure please.
+if (strpos('/', $module) !== false) {
+    die('Invalid module');
+}
+
+// Some (huge) modules are better loaded lazily (when they are used). If we are requesting
+// one of these modules, only return the one module, not the combo.
+$lazysuffix = "-lazy.js";
+$lazyload = (strpos($module, $lazysuffix) !== false);
+
+if ($lazyload) {
+    // We are lazy loading a single file - so include the component/filename pair in the etag.
+    $etag = sha1($rev . '/' . $component . '/' . $module);
+} else {
+    // We loading all (non-lazy) files - so only the rev makes this request unique.
+    $etag = sha1($rev);
+}
+
+
+// Use the caching only for meaningful revision numbers which prevents future cache poisoning.
+if ($rev > 0 and $rev < (time() + 60 * 60)) {
+    $candidate = $CFG->localcachedir . '/requirejs/' . $etag;
+
+    if (file_exists($candidate)) {
+        if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
+            // We do not actually need to verify the etag value because our files
+            // never change in cache because we increment the rev parameter.
+            js_send_unmodified(filemtime($candidate), $etag);
+        }
+        js_send_cached($candidate, $etag, 'requirejs.php');
+        exit(0);
+
+    } else {
+        $jsfiles = array();
+        if ($lazyload) {
+            $jsfiles = core_requirejs::find_one_amd_module($component, $module);
+        } else {
+            // Here we respond to the request by returning ALL amd modules. This saves
+            // round trips in production.
+
+            $jsfiles = core_requirejs::find_all_amd_modules();
+        }
+
+        $content = '';
+        foreach ($jsfiles as $modulename => $jsfile) {
+            $js = file_get_contents($jsfile) . "\n";
+            // Inject the module name into the define.
+            $replace = 'define(\'' . $modulename . '\', ';
+            $search = 'define(';
+            // Replace only the first occurrence.
+            $js = implode($replace, explode($search, $js, 2));
+            $content .= $js;
+        }
+
+        js_write_cache_file_content($candidate, $content);
+        // Verify nothing failed in cache file creation.
+        clearstatcache();
+        if (file_exists($candidate)) {
+            js_send_cached($candidate, $etag, 'requirejs.php');
+            exit(0);
+        }
+    }
+}
+
+if ($lazyload) {
+    $jsfiles = core_requirejs::find_one_amd_module($component, $module, true);
+} else {
+    $jsfiles = core_requirejs::find_all_amd_modules(true);
+}
+
+$content = '';
+foreach ($jsfiles as $modulename => $jsfile) {
+    $shortfilename = str_replace($CFG->dirroot, '', $jsfile);
+    $js = "// ---- $shortfilename ----\n";
+    $js .= file_get_contents($jsfile) . "\n";
+    // Inject the module name into the define.
+    $replace = 'define(\'' . $modulename . '\', ';
+    $search = 'define(';
+
+    if (strpos($js, $search) === false) {
+        // We can't call debugging because we only have minimal config loaded.
+        header('HTTP/1.0 500 error');
+        die('JS file: ' . $shortfilename . ' does not contain a javascript module in AMD format. "define()" not found.');
+    }
+
+    // Replace only the first occurrence.
+    $js = implode($replace, explode($search, $js, 2));
+    $content .= $js;
+}
+js_send_uncached($content, $etag, 'requirejs.php');
diff --git a/lib/requirejs/LICENSE b/lib/requirejs/LICENSE
new file mode 100644 (file)
index 0000000..d3b4181
--- /dev/null
@@ -0,0 +1,58 @@
+RequireJS is released under two licenses: new BSD, and MIT. You may pick the
+license that best suits your development needs. The text of both licenses are
+provided below.
+
+
+The "New" BSD License:
+----------------------
+
+Copyright (c) 2010-2014, The Dojo Foundation
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+  * Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation
+    and/or other materials provided with the distribution.
+  * Neither the name of the Dojo Foundation nor the names of its contributors
+    may be used to endorse or promote products derived from this software
+    without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+
+MIT License
+-----------
+
+Copyright (c) 2010-2014, The Dojo Foundation
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/lib/requirejs/jquery-private.js b/lib/requirejs/jquery-private.js
new file mode 100644 (file)
index 0000000..eaac228
--- /dev/null
@@ -0,0 +1,28 @@
+// 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 module depends on the real jquery - and returns the non-global version of it.
+ *
+ * @module     jquery-private
+ * @package    core
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery'], function ($) {
+    // This noConflict call tells JQuery to remove the variable from the global scope - so
+    // the only remaining instance will be the sandboxed one.
+    return $.noConflict( true );
+});
diff --git a/lib/requirejs/moodle-config.js b/lib/requirejs/moodle-config.js
new file mode 100644 (file)
index 0000000..06c216d
--- /dev/null
@@ -0,0 +1,24 @@
+var require = {
+    baseUrl : '[BASEURL]',
+    // We only support AMD modules with an explicit define() statement.
+    enforceDefine: true,
+    skipDataMain: true,
+
+    paths: {
+        jquery: '[JSURL]lib/jquery/jquery-1.11.1.min[JSEXT]',
+        jqueryui: '[JSURL]lib/jquery/ui-1.11.1/jquery-ui.min[JSEXT]',
+        jqueryprivate: '[JSURL]lib/requirejs/jquery-private[JSEXT]'
+    },
+
+    // Custom jquery config map.
+    map: {
+      // '*' means all modules will get 'jqueryprivate'
+      // for their 'jquery' dependency.
+      '*': { jquery: 'jqueryprivate' },
+
+      // 'jquery-private' wants the real jQuery module
+      // though. If this line was not here, there would
+      // be an unresolvable cyclic dependency.
+      jqueryprivate: { jquery: 'jquery' }
+    }
+};
diff --git a/lib/requirejs/require.js b/lib/requirejs/require.js
new file mode 100644 (file)
index 0000000..77a5bb1
--- /dev/null
@@ -0,0 +1,2076 @@
+/** vim: et:ts=4:sw=4:sts=4
+ * @license RequireJS 2.1.15 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved.
+ * Available via the MIT or new BSD license.
+ * see: http://github.com/jrburke/requirejs for details
+ */
+//Not using strict: uneven strict support in browsers, #392, and causes
+//problems with requirejs.exec()/transpiler plugins that may not be strict.
+/*jslint regexp: true, nomen: true, sloppy: true */
+/*global window, navigator, document, importScripts, setTimeout, opera */
+
+var requirejs, require, define;
+(function (global) {
+    var req, s, head, baseElement, dataMain, src,
+        interactiveScript, currentlyAddingScript, mainScript, subPath,
+        version = '2.1.15',
+        commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,
+        cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,
+        jsSuffixRegExp = /\.js$/,
+        currDirRegExp = /^\.\//,
+        op = Object.prototype,
+        ostring = op.toString,
+        hasOwn = op.hasOwnProperty,
+        ap = Array.prototype,
+        apsp = ap.splice,
+        isBrowser = !!(typeof window !== 'undefined' && typeof navigator !== 'undefined' && window.document),
+        isWebWorker = !isBrowser && typeof importScripts !== 'undefined',
+        //PS3 indicates loaded and complete, but need to wait for complete
+        //specifically. Sequence is 'loading', 'loaded', execution,
+        // then 'complete'. The UA check is unfortunate, but not sure how
+        //to feature test w/o causing perf issues.
+        readyRegExp = isBrowser && navigator.platform === 'PLAYSTATION 3' ?
+                      /^complete$/ : /^(complete|loaded)$/,
+        defContextName = '_',
+        //Oh the tragedy, detecting opera. See the usage of isOpera for reason.
+        isOpera = typeof opera !== 'undefined' && opera.toString() === '[object Opera]',
+        contexts = {},
+        cfg = {},
+        globalDefQueue = [],
+        useInteractive = false;
+
+    function isFunction(it) {
+        return ostring.call(it) === '[object Function]';
+    }
+
+    function isArray(it) {
+        return ostring.call(it) === '[object Array]';
+    }
+
+    /**
+     * Helper function for iterating over an array. If the func returns
+     * a true value, it will break out of the loop.
+     */
+    function each(ary, func) {
+        if (ary) {
+            var i;
+            for (i = 0; i < ary.length; i += 1) {
+                if (ary[i] && func(ary[i], i, ary)) {
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
+     * Helper function for iterating over an array backwards. If the func
+     * returns a true value, it will break out of the loop.
+     */
+    function eachReverse(ary, func) {
+        if (ary) {
+            var i;
+            for (i = ary.length - 1; i > -1; i -= 1) {
+                if (ary[i] && func(ary[i], i, ary)) {
+                    break;
+                }
+            }
+        }
+    }
+
+    function hasProp(obj, prop) {
+        return hasOwn.call(obj, prop);
+    }
+
+    function getOwn(obj, prop) {
+        return hasProp(obj, prop) && obj[prop];
+    }
+
+    /**
+     * Cycles over properties in an object and calls a function for each
+     * property value. If the function returns a truthy value, then the
+     * iteration is stopped.
+     */
+    function eachProp(obj, func) {
+        var prop;
+        for (prop in obj) {
+            if (hasProp(obj, prop)) {
+                if (func(obj[prop], prop)) {
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
+     * Simple function to mix in properties from source into target,
+     * but only if target does not already have a property of the same name.
+     */
+    function mixin(target, source, force, deepStringMixin) {
+        if (source) {
+            eachProp(source, function (value, prop) {
+                if (force || !hasProp(target, prop)) {
+                    if (deepStringMixin && typeof value === 'object' && value &&
+                        !isArray(value) && !isFunction(value) &&
+                        !(value instanceof RegExp)) {
+
+                        if (!target[prop]) {
+                            target[prop] = {};
+                        }
+                        mixin(target[prop], value, force, deepStringMixin);
+                    } else {
+                        target[prop] = value;
+                    }
+                }
+            });
+        }
+        return target;
+    }
+
+    //Similar to Function.prototype.bind, but the 'this' object is specified
+    //first, since it is easier to read/figure out what 'this' will be.
+    function bind(obj, fn) {
+        return function () {
+            return fn.apply(obj, arguments);
+        };
+    }
+
+    function scripts() {
+        return document.getElementsByTagName('script');
+    }
+
+    function defaultOnError(err) {
+        throw err;
+    }
+
+    //Allow getting a global that is expressed in
+    //dot notation, like 'a.b.c'.
+    function getGlobal(value) {
+        if (!value) {
+            return value;
+        }
+        var g = global;
+        each(value.split('.'), function (part) {
+            g = g[part];
+        });
+        return g;
+    }
+
+    /**
+     * Constructs an error with a pointer to an URL with more information.
+     * @param {String} id the error ID that maps to an ID on a web page.
+     * @param {String} message human readable error.
+     * @param {Error} [err] the original error, if there is one.
+     *
+     * @returns {Error}
+     */
+    function makeError(id, msg, err, requireModules) {
+        var e = new Error(msg + '\nhttp://requirejs.org/docs/errors.html#' + id);
+        e.requireType = id;
+        e.requireModules = requireModules;
+        if (err) {
+            e.originalError = err;
+        }
+        return e;
+    }
+
+    if (typeof define !== 'undefined') {
+        //If a define is already in play via another AMD loader,
+        //do not overwrite.
+        return;
+    }
+
+    if (typeof requirejs !== 'undefined') {
+        if (isFunction(requirejs)) {
+            //Do not overwrite an existing requirejs instance.
+            return;
+        }
+        cfg = requirejs;
+        requirejs = undefined;
+    }
+
+    //Allow for a require config object
+    if (typeof require !== 'undefined' && !isFunction(require)) {
+        //assume it is a config object.
+        cfg = require;
+        require = undefined;
+    }
+
+    function newContext(contextName) {
+        var inCheckLoaded, Module, context, handlers,
+            checkLoadedTimeoutId,
+            config = {
+                //Defaults. Do not set a default for map
+                //config to speed up normalize(), which
+                //will run faster if there is no default.
+                waitSeconds: 7,
+                baseUrl: './',
+                paths: {},
+                bundles: {},
+                pkgs: {},
+                shim: {},
+                config: {}
+            },
+            registry = {},
+            //registry of just enabled modules, to speed
+            //cycle breaking code when lots of modules
+            //are registered, but not activated.
+            enabledRegistry = {},
+            undefEvents = {},
+            defQueue = [],
+            defined = {},
+            urlFetched = {},
+            bundlesMap = {},
+            requireCounter = 1,
+            unnormalizedCounter = 1;
+
+        /**
+         * Trims the . and .. from an array of path segments.
+         * It will keep a leading path segment if a .. will become
+         * the first path segment, to help with module name lookups,
+         * which act like paths, but can be remapped. But the end result,
+         * all paths that use this function should look normalized.
+         * NOTE: this method MODIFIES the input array.
+         * @param {Array} ary the array of path segments.
+         */
+        function trimDots(ary) {
+            var i, part;
+            for (i = 0; i < ary.length; i++) {
+                part = ary[i];
+                if (part === '.') {
+                    ary.splice(i, 1);
+                    i -= 1;
+                } else if (part === '..') {
+                    // If at the start, or previous value is still ..,
+                    // keep them so that when converted to a path it may
+                    // still work when converted to a path, even though
+                    // as an ID it is less than ideal. In larger point
+                    // releases, may be better to just kick out an error.
+                    if (i === 0 || (i == 1 && ary[2] === '..') || ary[i - 1] === '..') {
+                        continue;
+                    } else if (i > 0) {
+                        ary.splice(i - 1, 2);
+                        i -= 2;
+                    }
+                }
+            }
+        }
+
+        /**
+         * Given a relative module name, like ./something, normalize it to
+         * a real name that can be mapped to a path.
+         * @param {String} name the relative name
+         * @param {String} baseName a real name that the name arg is relative
+         * to.
+         * @param {Boolean} applyMap apply the map config to the value. Should
+         * only be done if this normalization is for a dependency ID.
+         * @returns {String} normalized name
+         */
+        function normalize(name, baseName, applyMap) {
+            var pkgMain, mapValue, nameParts, i, j, nameSegment, lastIndex,
+                foundMap, foundI, foundStarMap, starI, normalizedBaseParts,
+                baseParts = (baseName && baseName.split('/')),
+                map = config.map,
+                starMap = map && map['*'];
+
+            //Adjust any relative paths.
+            if (name) {
+                name = name.split('/');
+                lastIndex = name.length - 1;
+
+                // If wanting node ID compatibility, strip .js from end