Merge branch 'MDL-49107-master' of git://github.com/FMCorz/moodle
authorDavid Monllao <davidm@moodle.com>
Tue, 10 Mar 2015 03:42:58 +0000 (11:42 +0800)
committerDavid Monllao <davidm@moodle.com>
Tue, 10 Mar 2015 03:42:58 +0000 (11:42 +0800)
238 files changed:
.gitignore
.jshintrc
Gruntfile.js [new file with mode: 0644]
admin/environment.xml
admin/index.php
admin/registration/confirmregistration.php
admin/registration/index.php
admin/registration/renderer.php
admin/settings/courses.php
admin/tool/langimport/classes/controller.php
admin/webservice/forms.php
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/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/externallib.php
enrol/renderer.php
enrol/users.php
enrol/users_forms.php
filter/urltolink/filter.php
filter/urltolink/tests/filter_test.php
grade/export/key.php
grade/export/keymanager.php
grade/import/key.php
grade/import/keymanager.php
grade/lib.php
grade/report/grader/lib.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/group.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/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/componentlib.class.php
lib/db/install.xml
lib/db/services.php
lib/db/tasks.php
lib/db/upgrade.php
lib/dml/mariadb_native_moodle_database.php
lib/dml/oci_native_moodle_database.php
lib/external/externallib.php
lib/external/tests/external_test.php
lib/filestorage/zip_archive.php
lib/formslib.php
lib/grade/grade_category.php
lib/gradelib.php
lib/grouplib.php
lib/javascript-static.js
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/tests/behat/behat_forms.php
lib/tests/blocklib_test.php
lib/tests/grouplib_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/upgradelib.php
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/locallib.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/data/backup/moodle2/backup_data_stepslib.php
mod/data/db/install.xml
mod/data/db/upgrade.php
mod/data/edit.php
mod/data/field.php
mod/data/field/checkbox/field.class.php
mod/data/field/checkbox/mod.html
mod/data/field/date/field.class.php
mod/data/field/file/field.class.php
mod/data/field/file/mod.html
mod/data/field/latlong/field.class.php
mod/data/field/latlong/mod.html
mod/data/field/menu/field.class.php
mod/data/field/menu/mod.html
mod/data/field/multimenu/field.class.php
mod/data/field/multimenu/mod.html
mod/data/field/number/mod.html
mod/data/field/picture/field.class.php
mod/data/field/picture/mod.html
mod/data/field/radiobutton/field.class.php
mod/data/field/radiobutton/mod.html
mod/data/field/text/mod.html
mod/data/field/textarea/field.class.php
mod/data/field/textarea/mod.html
mod/data/field/url/field.class.php
mod/data/field/url/mod.html
mod/data/lang/en/data.php
mod/data/lib.php
mod/data/styles.css
mod/data/tests/behat/add_entries.feature
mod/data/tests/behat/required_entries.feature [new file with mode: 0644]
mod/data/tests/behat/view_entries.feature
mod/data/version.php
mod/forum/lib.php
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/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/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/time_limit.feature
mod/lesson/tests/events_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/type/match/backup/moodle2/restore_qtype_match_plugin.class.php
question/type/multianswer/lang/en/qtype_multianswer.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
repository/googledocs/lang/en/repository_googledocs.php
repository/picasa/lang/en/repository_picasa.php
repository/s3/lang/en/repository_s3.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/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
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 38ec017..27a0b5a 100644 (file)
       </CUSTOM_CHECK>
     </CUSTOM_CHECKS>
   </MOODLE>
+  <MOODLE version="2.9" requires="2.2">
+    <UNICODE level="required">
+      <FEEDBACK>
+        <ON_ERROR message="unicoderequired" />
+      </FEEDBACK>
+    </UNICODE>
+    <DATABASE level="required">
+      <VENDOR name="mariadb" version="5.5.31" />
+      <VENDOR name="mysql" version="5.5.31" />
+      <VENDOR name="postgres" version="9.1" />
+      <VENDOR name="mssql" version="10.0" />
+      <VENDOR name="oracle" version="10.2" />
+    </DATABASE>
+    <PHP version="5.4.4" level="required">
+    </PHP>
+    <PCREUNICODE level="optional">
+      <FEEDBACK>
+        <ON_CHECK message="pcreunicodewarning" />
+      </FEEDBACK>
+    </PCREUNICODE>
+    <PHP_EXTENSIONS>
+      <PHP_EXTENSION name="iconv" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="iconvrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="mbstring" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="mbstringrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="curl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="curlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="openssl" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="opensslrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="tokenizer" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="tokenizerrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xmlrpc" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="xmlrpcrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="soap" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="soaprecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="ctype" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ctyperequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zip" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ziprequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zlib" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="gd" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="gdrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="simplexml" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="simplexmlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="spl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="splrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="pcre" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="dom" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xml" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="intl" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="intlrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="json" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="hash" level="required"/>
+    </PHP_EXTENSIONS>
+    <PHP_SETTINGS>
+      <PHP_SETTING name="memory_limit" value="96M" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="settingmemorylimit" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="file_uploads" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="settingfileuploads" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="opcache.enable" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="opcacherecommended" />
+        </FEEDBACK>
+      </PHP_SETTING>
+    </PHP_SETTINGS>
+    <CUSTOM_CHECKS>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbstorageengine" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="quizattemptsupgradedmessage" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+    </CUSTOM_CHECKS>
+  </MOODLE>
 </COMPATIBILITY_MATRIX>
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 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 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 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) {
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 c91ccb1..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';
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 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 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 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 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 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 2b836a6..43b6d35 100644 (file)
@@ -2847,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'));
             }
@@ -2875,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 daaeddc..f6f1821 100644 (file)
@@ -1345,7 +1345,7 @@ class grade_report_grader extends grade_report {
      * @return array Array of rows for the right part of the report
      */
     public function get_right_avg_row($rows=array(), $grouponly=false) {
-        global $USER, $DB, $OUTPUT;
+        global $USER, $DB, $OUTPUT, $CFG;
 
         if (!$this->canviewhidden) {
             // Totals might be affected by hiding, if user can not see hidden grades the aggregations might be altered
@@ -1377,7 +1377,11 @@ class grade_report_grader extends grade_report {
             list($gradebookrolessql, $gradebookrolesparams) = $DB->get_in_or_equal(explode(',', $this->gradebookroles), SQL_PARAMS_NAMED, 'grbr0');
 
             // Limit to users with an active enrollment.
-            list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context);
+            $coursecontext = $this->context->get_course_context(true);
+            $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol);
+            $showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol);
+            $showonlyactiveenrol = $showonlyactiveenrol || !has_capability('moodle/course:viewsuspendedusers', $coursecontext);
+            list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context, '', 0, $showonlyactiveenrol);
 
             // We want to query both the current context and parent contexts.
             list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
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 d9193b9..35c0ef1 100644 (file)
@@ -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.';
@@ -1068,6 +1067,7 @@ $string['uninstallplugin'] = 'Uninstall';
 $string['unlockaccount'] = 'Unlock account';
 $string['unsettheme'] = 'Unset theme';
 $string['unsupported'] = 'Unsupported';
+$string['unsupporteddbstorageengine'] = 'The database storage engine being used is no longer supported.';
 $string['unsuspenduser'] = 'Activate user account';
 $string['updateaccounts'] = 'Update existing accounts';
 $string['updatecomponent'] = 'Update component';
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 14060bb..5536a7a 100644 (file)
@@ -159,6 +159,8 @@ $string['nousersinrole'] = 'There are no suitable users in the selected role';
 $string['number'] = 'Group/member count';
 $string['numgroups'] = 'Number of groups';
 $string['nummembers'] = 'Members per group';
+$string['mygroups'] = 'My groups';
+$string['othergroups'] = 'Other groups';
 $string['overview'] = 'Overview';
 $string['potentialmembers'] = 'Potential members: {$a}';
 $string['potentialmembs'] = 'Potential members';
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 eb8c403..4b1272c 100644 (file)
@@ -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 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 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 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 12dc9eb..ffc7796 100644 (file)
@@ -101,24 +101,4 @@ class mariadb_native_moodle_database extends mysqli_native_moodle_database {
         }
         return true;
     }
-
-    /**
-     * Returns the current db engine.
-     *
-     * MyISAM is NOT supported!
-     *
-     * @return string or null MySQL engine name
-     */
-    public function get_dbengine() {
-        if ($this->external) {
-            return null;
-        }
-
-        $engine = parent::get_dbengine();
-        if ($engine === 'MyISAM') {
-            debugging('MyISAM tables are not supported in MariaDB driver!');
-            $engine = 'XtraDB';
-        }
-        return $engine;
-    }
 }
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 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 cb9170a..6dfa227 100644 (file)
@@ -1014,6 +1014,8 @@ function grade_recover_history_grades($userid, $courseid) {
  * @return bool true if ok, array of errors if problems found. Grade item id => error message
  */
 function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null) {
+    // This may take a very long time.
+    \core_php_time_limit::raise();
 
     $course_item = grade_item::fetch_course_item($courseid);
 
index 98ac4be..3f3a233 100644 (file)
@@ -513,8 +513,11 @@ function groups_print_course_menu($course, $urlroot, $return=false) {
     $context = context_course::instance($course->id);
     $aag = has_capability('moodle/site:accessallgroups', $context);
 
+    $usergroups = array();
     if ($groupmode == VISIBLEGROUPS or $aag) {
         $allowedgroups = groups_get_all_groups($course->id, 0, $course->defaultgroupingid);
+        // Get user's own groups and put to the top.
+        $usergroups = groups_get_all_groups($course->id, $USER->id, $course->defaultgroupingid);
     } else {
         $allowedgroups = groups_get_all_groups($course->id, $USER->id, $course->defaultgroupingid);
     }
@@ -526,11 +529,7 @@ function groups_print_course_menu($course, $urlroot, $return=false) {
         $groupsmenu[0] = get_string('allparticipants');
     }
 
-    if ($allowedgroups) {
-        foreach ($allowedgroups as $group) {
-            $groupsmenu[$group->id] = format_string($group->name);
-        }
-    }
+    $groupsmenu += groups_sort_menu_options($allowedgroups, $usergroups);
 
     if ($groupmode == VISIBLEGROUPS) {
         $grouplabel = get_string('groupsvisible');
@@ -562,6 +561,55 @@ function groups_print_course_menu($course, $urlroot, $return=false) {
     }
 }
 
+/**
+ * Turn an array of groups into an array of menu options.
+ * @param array $groups of group objects.
+ * @return array groupid => formatted group name.
+ */
+function groups_list_to_menu($groups) {
+    $groupsmenu = array();
+    foreach ($groups as $group) {
+        $groupsmenu[$group->id] = format_string($group->name);
+    }
+    return $groupsmenu;
+}
+
+/**
+ * Takes user's allowed groups and own groups and formats for use in group selector menu
+ * If user has allowed groups + own groups will add to an optgroup
+ * Own groups are removed from allowed groups
+ * @param array $allowedgroups All groups user is allowed to see
+ * @param array $usergroups Groups user belongs to
+ * @return array
+ */
+function groups_sort_menu_options($allowedgroups, $usergroups) {
+    $useroptions = array();
+    if ($usergroups) {
+        $useroptions = groups_list_to_menu($usergroups);
+
+        // Remove user groups from other groups list.
+        foreach ($usergroups as $group) {
+            unset($allowedgroups[$group->id]);
+        }
+    }
+
+    $allowedoptions = array();
+    if ($allowedgroups) {
+        $allowedoptions = groups_list_to_menu($allowedgroups);
+    }
+
+    if ($useroptions && $allowedoptions) {
+        return array(
+            1 => array(get_string('mygroups', 'group') => $useroptions),
+            2 => array(get_string('othergroups', 'group') => $allowedoptions)
+        );
+    } else if ($useroptions) {
+        return $useroptions;
+    } else {
+        return $allowedoptions;
+    }
+}
+
 /**
  * Generates html to print menu selector for course level, listing all groups.
  * Note: This api does not do any group mode check use groups_print_course_menu() instead if you want proper checks.
@@ -587,9 +635,7 @@ function groups_allgroups_course_menu($course, $urlroot, $update = false, $activ
         $allowedgroups = groups_get_all_groups($course->id, $USER->id, $course->defaultgroupingid);
     }
 
-    foreach ($allowedgroups as $group) {
-        $groupsmenu[$group->id] = format_string($group->name);
-    }
+    $groupsmenu += groups_list_to_menu($allowedgroups);
 
     if ($update) {
         // Init activegroup array if necessary.
@@ -665,8 +711,11 @@ function groups_print_activity_menu($cm, $urlroot, $return=false, $hideallpartic
     $context = context_module::instance($cm->id);
     $aag = has_capability('moodle/site:accessallgroups', $context);
 
+    $usergroups = array();
     if ($groupmode == VISIBLEGROUPS or $aag) {
         $allowedgroups = groups_get_all_groups($cm->course, 0, $cm->groupingid); // any group in grouping
+        // Get user's own groups and put to the top.
+        $usergroups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid);
     } else {
         $allowedgroups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid); // only assigned groups
     }
@@ -678,11 +727,7 @@ function groups_print_activity_menu($cm, $urlroot, $return=false, $hideallpartic
         $groupsmenu[0] = get_string('allparticipants');
     }
 
-    if ($allowedgroups) {
-        foreach ($allowedgroups as $group) {
-            $groupsmenu[$group->id] = format_string($group->name);
-        }
-    }
+    $groupsmenu += groups_sort_menu_options($allowedgroups, $usergroups);
 
     if ($groupmode == VISIBLEGROUPS) {
         $grouplabel = get_string('groupsvisible');
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 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
+                // of IDs. Have to do this here, and not in nameToUrl
+                // because node allows either .js or non .js to map
+                // to same file.
+                if (config.nodeIdCompat && jsSuffixRegExp.test(name[lastIndex])) {
+                    name[lastIndex] = name[lastIndex].replace(jsSuffixRegExp, '');
+                }
+
+                // Starts with a '.' so need the baseName
+                if (name[0].charAt(0) === '.' && baseParts) {
+                    //Convert baseName to array, and lop off the last part,
+                    //so that . matches that 'directory' and not name of the baseName's
+                    //module. For instance, baseName of 'one/two/three', maps to
+                    //'one/two/three.js', but we want the directory, 'one/two' for
+                    //this normalization.
+                    normalizedBaseParts = baseParts.slice(0, baseParts.length - 1);
+                    name = normalizedBaseParts.concat(name);
+                }
+
+                trimDots(name);
+                name = name.join('/');
+            }
+
+            //Apply map config if available.
+            if (applyMap && map && (baseParts || starMap)) {
+                nameParts = name.split('/');
+
+                outerLoop: for (i = nameParts.length; i > 0; i -= 1) {
+                    nameSegment = nameParts.slice(0, i).join('/');
+
+                    if (baseParts) {
+                        //Find the longest baseName segment match in the config.
+                        //So, do joins on the biggest to smallest lengths of baseParts.
+                        for (j = baseParts.length; j > 0; j -= 1) {
+                            mapValue = getOwn(map, baseParts.slice(0, j).join('/'));
+
+                            //baseName segment has config, find if it has one for
+                            //this name.
+                            if (mapValue) {
+                                mapValue = getOwn(mapValue, nameSegment);
+                                if (mapValue) {
+                                    //Match, update name to the new value.
+                                    foundMap = mapValue;
+                                    foundI = i;
+                                    break outerLoop;
+                                }
+                            }
+                        }
+                    }
+
+                    //Check for a star map match, but just hold on to it,
+                    //if there is a shorter segment match later in a matching
+                    //config, then favor over this star map.
+                    if (!foundStarMap && starMap && getOwn(starMap, nameSegment)) {
+                        foundStarMap = getOwn(starMap, nameSegment);
+                        starI = i;
+                    }
+                }
+
+                if (!foundMap && foundStarMap) {
+                    foundMap = foundStarMap;
+                    foundI = starI;
+                }
+
+                if (foundMap) {
+                    nameParts.splice(0, foundI, foundMap);
+                    name = nameParts.join('/');
+                }
+            }
+
+            // If the name points to a package's name, use
+            // the package main instead.
+            pkgMain = getOwn(config.pkgs, name);
+
+            return pkgMain ? pkgMain : name;
+        }
+
+        function removeScript(name) {
+            if (isBrowser) {
+                each(scripts(), function (scriptNode) {
+                    if (scriptNode.getAttribute('data-requiremodule') === name &&
+                            scriptNode.getAttribute('data-requirecontext') === context.contextName) {
+                        scriptNode.parentNode.removeChild(scriptNode);
+                        return true;
+                    }
+                });
+            }
+        }
+
+        function hasPathFallback(id) {
+            var pathConfig = getOwn(config.paths, id);
+            if (pathConfig && isArray(pathConfig) && pathConfig.length > 1) {
+                //Pop off the first array value, since it failed, and
+                //retry
+                pathConfig.shift();
+                context.require.undef(id);
+
+                //Custom require that does not do map translation, since
+                //ID is "absolute", already mapped/resolved.
+                context.makeRequire(null, {
+                    skipMap: true
+                })([id]);
+
+                return true;
+            }
+        }
+
+        //Turns a plugin!resource to [plugin, resource]
+        //with the plugin being undefined if the name
+        //did not have a plugin prefix.
+        function splitPrefix(name) {
+            var prefix,
+                index = name ? name.indexOf('!') : -1;
+            if (index > -1) {
+                prefix = name.substring(0, index);
+                name = name.substring(index + 1, name.length);
+            }
+            return [prefix, name];
+        }
+
+        /**
+         * Creates a module mapping that includes plugin prefix, module
+         * name, and path. If parentModuleMap is provided it will
+         * also normalize the name via require.normalize()
+         *
+         * @param {String} name the module name
+         * @param {String} [parentModuleMap] parent module map
+         * for the module name, used to resolve relative names.
+         * @param {Boolean} isNormalized: is the ID already normalized.
+         * This is true if this call is done for a define() module ID.
+         * @param {Boolean} applyMap: apply the map config to the ID.
+         * Should only be true if this map is for a dependency.
+         *
+         * @returns {Object}
+         */
+        function makeModuleMap(name, parentModuleMap, isNormalized, applyMap) {
+            var url, pluginModule, suffix, nameParts,
+                prefix = null,
+                parentName = parentModuleMap ? parentModuleMap.name : null,
+                originalName = name,
+                isDefine = true,
+                normalizedName = '';
+
+            //If no name, then it means it is a require call, generate an
+            //internal name.
+            if (!name) {
+                isDefine = false;
+                name = '_@r' + (requireCounter += 1);
+            }
+
+            nameParts = splitPrefix(name);
+            prefix = nameParts[0];
+            name = nameParts[1];
+
+            if (prefix) {
+                prefix = normalize(prefix, parentName, applyMap);
+                pluginModule = getOwn(defined, prefix);
+            }
+
+            //Account for relative paths if there is a base name.
+            if (name) {
+                if (prefix) {
+                    if (pluginModule && pluginModule.normalize) {
+                        //Plugin is loaded, use its normalize method.
+                        normalizedName = pluginModule.normalize(name, function (name) {
+                            return normalize(name, parentName, applyMap);
+                        });
+                    } else {
+                        // If nested plugin references, then do not try to
+                        // normalize, as it will not normalize correctly. This
+                        // places a restriction on resourceIds, and the longer
+                        // term solution is not to normalize until plugins are
+                        // loaded and all normalizations to allow for async
+                        // loading of a loader plugin. But for now, fixes the
+                        // common uses. Details in #1131
+                        normalizedName = name.indexOf('!') === -1 ?
+                                         normalize(name, parentName, applyMap) :
+                                         name;
+                    }
+                } else {
+                    //A regular module.
+                    normalizedName = normalize(name, parentName, applyMap);
+
+                    //Normalized name may be a plugin ID due to map config
+                    //application in normalize. The map config values must
+                    //already be normalized, so do not need to redo that part.
+                    nameParts = splitPrefix(normalizedName);
+                    prefix = nameParts[0];
+                    normalizedName = nameParts[1];
+                    isNormalized = true;
+
+                    url = context.nameToUrl(normalizedName);
+                }
+            }
+
+            //If the id is a plugin id that cannot be determined if it needs
+            //normalization, stamp it with a unique ID so two matching relative
+            //ids that may conflict can be separate.
+            suffix = prefix && !pluginModule && !isNormalized ?
+                     '_unnormalized' + (unnormalizedCounter += 1) :
+                     '';
+
+            return {
+                prefix: prefix,
+                name: normalizedName,
+                parentMap: parentModuleMap,
+                unnormalized: !!suffix,
+                url: url,
+                originalName: originalName,
+                isDefine: isDefine,
+                id: (prefix ?
+                        prefix + '!' + normalizedName :
+                        normalizedName) + suffix
+            };
+        }
+
+        function getModule(depMap) {
+            var id = depMap.id,
+                mod = getOwn(registry, id);
+
+            if (!mod) {
+                mod = registry[id] = new context.Module(depMap);
+            }
+
+            return mod;
+        }
+
+        function on(depMap, name, fn) {
+            var id = depMap.id,
+                mod = getOwn(registry, id);
+
+            if (hasProp(defined, id) &&
+                    (!mod || mod.defineEmitComplete)) {
+                if (name === 'defined') {
+                    fn(defined[id]);
+                }
+            } else {
+                mod = getModule(depMap);
+                if (mod.error && name === 'error') {
+                    fn(mod.error);
+                } else {
+                    mod.on(name, fn);
+                }
+            }
+        }
+
+        function onError(err, errback) {
+            var ids = err.requireModules,
+                notified = false;
+
+            if (errback) {
+                errback(err);
+            } else {
+                each(ids, function (id) {
+                    var mod = getOwn(registry, id);
+                    if (mod) {
+                        //Set error on module, so it skips timeout checks.
+                        mod.error = err;
+                        if (mod.events.error) {
+                            notified = true;
+                            mod.emit('error', err);
+                        }
+                    }
+                });
+
+                if (!notified) {
+                    req.onError(err);
+                }
+            }
+        }
+
+        /**
+         * Internal method to transfer globalQueue items to this context's
+         * defQueue.
+         */
+        function takeGlobalQueue() {
+            //Push all the globalDefQueue items into the context's defQueue
+            if (globalDefQueue.length) {
+                //Array splice in the values since the context code has a
+                //local var ref to defQueue, so cannot just reassign the one
+                //on context.
+                apsp.apply(defQueue,
+                           [defQueue.length, 0].concat(globalDefQueue));
+                globalDefQueue = [];
+            }
+        }
+
+        handlers = {
+            'require': function (mod) {
+                if (mod.require) {
+                    return mod.require;
+                } else {
+                    return (mod.require = context.makeRequire(mod.map));
+                }
+            },
+            'exports': function (mod) {
+                mod.usingExports = true;
+                if (mod.map.isDefine) {
+                    if (mod.exports) {
+                        return (defined[mod.map.id] = mod.exports);
+                    } else {
+                        return (mod.exports = defined[mod.map.id] = {});
+                    }
+                }
+            },
+            'module': function (mod) {
+                if (mod.module) {
+                    return mod.module;
+                } else {
+                    return (mod.module = {
+                        id: mod.map.id,
+                        uri: mod.map.url,
+                        config: function () {
+                            return  getOwn(config.config, mod.map.id) || {};
+                        },
+                        exports: mod.exports || (mod.exports = {})
+                    });
+                }
+            }
+        };
+
+        function cleanRegistry(id) {
+            //Clean up machinery used for waiting modules.
+            delete registry[id];
+            delete enabledRegistry[id];
+        }
+
+        function breakCycle(mod, traced, processed) {
+            var id = mod.map.id;
+
+            if (mod.error) {
+                mod.emit('error', mod.error);
+            } else {
+                traced[id] = true;
+                each(mod.depMaps, function (depMap, i) {
+                    var depId = depMap.id,
+                        dep = getOwn(registry, depId);
+
+                    //Only force things that have not completed
+                    //being defined, so still in the registry,
+                    //and only if it has not been matched up
+                    //in the module already.
+                    if (dep && !mod.depMatched[i] && !processed[depId]) {
+                        if (getOwn(traced, depId)) {
+                            mod.defineDep(i, defined[depId]);
+                            mod.check(); //pass false?
+                        } else {
+                            breakCycle(dep, traced, processed);
+                        }
+                    }
+                });
+                processed[id] = true;
+            }
+        }
+
+        function checkLoaded() {
+            var err, usingPathFallback,
+                waitInterval = config.waitSeconds * 1000,
+                //It is possible to disable the wait interval by using waitSeconds of 0.
+                expired = waitInterval && (context.startTime + waitInterval) < new Date().getTime(),
+                noLoads = [],
+                reqCalls = [],
+                stillLoading = false,
+                needCycleCheck = true;
+
+            //Do not bother if this call was a result of a cycle break.
+            if (inCheckLoaded) {
+                return;
+            }
+
+            inCheckLoaded = true;
+
+            //Figure out the state of all the modules.
+            eachProp(enabledRegistry, function (mod) {
+                var map = mod.map,
+                    modId = map.id;
+
+                //Skip things that are not enabled or in error state.
+                if (!mod.enabled) {
+                    return;
+                }
+
+                if (!map.isDefine) {
+                    reqCalls.push(mod);
+                }
+
+                if (!mod.error) {
+                    //If the module should be executed, and it has not
+                    //been inited and time is up, remember it.
+                    if (!mod.inited && expired) {
+                        if (hasPathFallback(modId)) {
+                            usingPathFallback = true;
+                            stillLoading = true;
+                        } else {
+                            noLoads.push(modId);
+                            removeScript(modId);
+                        }
+                    } else if (!mod.inited && mod.fetched && map.isDefine) {
+                        stillLoading = true;
+                        if (!map.prefix) {
+                            //No reason to keep looking for unfinished
+                            //loading. If the only stillLoading is a
+                            //plugin resource though, keep going,
+                            //because it may be that a plugin resource
+                            //is waiting on a non-plugin cycle.
+                            return (needCycleCheck = false);
+                        }
+                    }
+                }
+            });
+
+            if (expired && noLoads.length) {
+                //If wait time expired, throw error of unloaded modules.
+                err = makeError('timeout', 'Load timeout for modules: ' + noLoads, null, noLoads);
+                err.contextName = context.contextName;
+                return onError(err);
+            }
+
+            //Not expired, check for a cycle.
+            if (needCycleCheck) {
+                each(reqCalls, function (mod) {
+                    breakCycle(mod, {}, {});
+                });
+            }
+
+            //If still waiting on loads, and the waiting load is something
+            //other than a plugin resource, or there are still outstanding
+            //scripts, then just try back later.
+            if ((!expired || usingPathFallback) && stillLoading) {
+                //Something is still waiting to load. Wait for it, but only
+                //if a timeout is not already in effect.
+                if ((isBrowser || isWebWorker) && !checkLoadedTimeoutId) {
+                    checkLoadedTimeoutId = setTimeout(function () {
+                        checkLoadedTimeoutId = 0;
+                        checkLoaded();
+                    }, 50);
+                }
+            }
+
+            inCheckLoaded = false;
+        }
+
+        Module = function (map) {
+            this.events = getOwn(undefEvents, map.id) || {};
+            this.map = map;
+            this.shim = getOwn(config.shim, map.id);
+            this.depExports = [];
+            this.depMaps = [];
+            this.depMatched = [];
+            this.pluginMaps = {};
+            this.depCount = 0;
+
+            /* this.exports this.factory
+               this.depMaps = [],
+               this.enabled, this.fetched
+            */
+        };
+
+        Module.prototype = {
+            init: function (depMaps, factory, errback, options) {
+                options = options || {};
+
+                //Do not do more inits if already done. Can happen if there
+                //are multiple define calls for the same module. That is not
+                //a normal, common case, but it is also not unexpected.
+                if (this.inited) {
+                    return;
+                }
+
+                this.factory = factory;
+
+                if (errback) {
+                    //Register for errors on this module.
+                    this.on('error', errback);
+                } else if (this.events.error) {
+                    //If no errback already, but there are error listeners
+                    //on this module, set up an errback to pass to the deps.
+                    errback = bind(this, function (err) {
+                        this.emit('error', err);
+                    });
+                }
+
+                //Do a copy of the dependency array, so that
+                //source inputs are not modified. For example
+                //"shim" deps are passed in here directly, and
+                //doing a direct modification of the depMaps array
+                //would affect that config.
+                this.depMaps = depMaps && depMaps.slice(0);
+
+                this.errback = errback;
+
+                //Indicate this module has be initialized
+                this.inited = true;
+
+                this.ignore = options.ignore;
+
+                //Could have option to init this module in enabled mode,
+                //or could have been previously marked as enabled. However,
+                //the dependencies are not known until init is called. So
+                //if enabled previously, now trigger dependencies as enabled.
+                if (options.enabled || this.enabled) {
+                    //Enable this module and dependencies.
+                    //Will call this.check()
+                    this.enable();
+                } else {
+                    this.check();
+                }
+            },
+
+            defineDep: function (i, depExports) {
+                //Because of cycles, defined callback for a given
+                //export can be called more than once.
+                if (!this.depMatched[i]) {
+                    this.depMatched[i] = true;
+                    this.depCount -= 1;
+                    this.depExports[i] = depExports;
+                }
+            },
+
+            fetch: function () {
+                if (this.fetched) {
+                    return;
+                }
+                this.fetched = true;
+
+                context.startTime = (new Date()).getTime();
+
+                var map = this.map;
+
+                //If the manager is for a plugin managed resource,
+                //ask the plugin to load it now.
+                if (this.shim) {
+                    context.makeRequire(this.map, {
+                        enableBuildCallback: true
+                    })(this.shim.deps || [], bind(this, function () {
+                        return map.prefix ? this.callPlugin() : this.load();
+                    }));
+                } else {
+                    //Regular dependency.
+                    return map.prefix ? this.callPlugin() : this.load();
+                }
+            },
+
+            load: function () {
+                var url = this.map.url;
+
+                //Regular dependency.
+                if (!urlFetched[url]) {
+                    urlFetched[url] = true;
+                    context.load(this.map.id, url);
+                }
+            },
+
+            /**
+             * Checks if the module is ready to define itself, and if so,
+             * define it.
+             */
+            check: function () {
+                if (!this.enabled || this.enabling) {
+                    return;
+                }
+
+                var err, cjsModule,
+                    id = this.map.id,
+                    depExports = this.depExports,
+                    exports = this.exports,
+                    factory = this.factory;
+
+                if (!this.inited) {
+                    this.fetch();
+                } else if (this.error) {
+                    this.emit('error', this.error);
+                } else if (!this.defining) {
+                    //The factory could trigger another require call
+                    //that would result in checking this module to
+                    //define itself again. If already in the process
+                    //of doing that, skip this work.
+                    this.defining = true;
+
+                    if (this.depCount < 1 && !this.defined) {
+                        if (isFunction(factory)) {
+                            //If there is an error listener, favor passing
+                            //to that instead of throwing an error. However,
+                            //only do it for define()'d  modules. require
+                            //errbacks should not be called for failures in
+                            //their callbacks (#699). However if a global
+                            //onError is set, use that.
+                            if ((this.events.error && this.map.isDefine) ||
+                                req.onError !== defaultOnError) {
+                                try {
+                                    exports = context.execCb(id, factory, depExports, exports);
+                                } catch (e) {
+                                    err = e;
+                                }
+                            } else {
+                                exports = context.execCb(id, factory, depExports, exports);
+                            }
+
+                            // Favor return value over exports. If node/cjs in play,
+                            // then will not have a return value anyway. Favor
+                            // module.exports assignment over exports object.
+                            if (this.map.isDefine && exports === undefined) {
+                                cjsModule = this.module;
+                                if (cjsModule) {
+                                    exports = cjsModule.exports;
+                                } else if (this.usingExports) {
+                                    //exports already set the defined value.
+                                    exports = this.exports;
+                                }
+                            }
+
+                            if (err) {
+                                err.requireMap = this.map;
+                                err.requireModules = this.map.isDefine ? [this.map.id] : null;
+                                err.requireType = this.map.isDefine ? 'define' : 'require';
+                                return onError((this.error = err));
+                            }
+
+                        } else {
+                            //Just a literal value
+                            exports = factory;
+                        }
+
+                        this.exports = exports;
+
+                        if (this.map.isDefine && !this.ignore) {
+                            defined[id] = exports;
+
+                            if (req.onResourceLoad) {
+                                req.onResourceLoad(context, this.map, this.depMaps);
+                            }
+                        }
+
+                        //Clean up
+                        cleanRegistry(id);
+
+                        this.defined = true;
+                    }
+
+                    //Finished the define stage. Allow calling check again
+                    //to allow define notifications below in the case of a
+                    //cycle.
+                    this.defining = false;
+
+                    if (this.defined && !this.defineEmitted) {
+                        this.defineEmitted = true;
+                        this.emit('defined', this.exports);
+                        this.defineEmitComplete = true;
+                    }
+
+                }
+            },
+
+            callPlugin: function () {
+                var map = this.map,
+                    id = map.id,
+                    //Map already normalized the prefix.
+                    pluginMap = makeModuleMap(map.prefix);
+
+                //Mark this as a dependency for this plugin, so it
+                //can be traced for cycles.
+                this.depMaps.push(pluginMap);
+
+                on(pluginMap, 'defined', bind(this, function (plugin) {
+                    var load, normalizedMap, normalizedMod,
+                        bundleId = getOwn(bundlesMap, this.map.id),
+                        name = this.map.name,
+                        parentName = this.map.parentMap ? this.map.parentMap.name : null,
+                        localRequire = context.makeRequire(map.parentMap, {
+                            enableBuildCallback: true
+                        });
+
+                    //If current map is not normalized, wait for that
+                    //normalized name to load instead of continuing.
+                    if (this.map.unnormalized) {
+                        //Normalize the ID if the plugin allows it.
+                        if (plugin.normalize) {
+                            name = plugin.normalize(name, function (name) {
+                                return normalize(name, parentName, true);
+                            }) || '';
+                        }
+
+                        //prefix and name should already be normalized, no need
+                        //for applying map config again either.
+                        normalizedMap = makeModuleMap(map.prefix + '!' + name,
+                                                      this.map.parentMap);
+                        on(normalizedMap,
+                            'defined', bind(this, function (value) {
+                                this.init([], function () { return value; }, null, {
+                                    enabled: true,
+                                    ignore: true
+                                });
+                            }));
+
+                        normalizedMod = getOwn(registry, normalizedMap.id);
+                        if (normalizedMod) {
+                            //Mark this as a dependency for this plugin, so it
+                            //can be traced for cycles.
+                            this.depMaps.push(normalizedMap);
+
+                            if (this.events.error) {
+                                normalizedMod.on('error', bind(this, function (err) {
+                                    this.emit('error', err);
+                                }));
+                            }
+                            normalizedMod.enable();
+                        }
+
+                        return;
+                    }
+
+                    //If a paths config, then just load that file instead to
+                    //resolve the plugin, as it is built into that paths layer.
+                    if (bundleId) {
+                        this.map.url = context.nameToUrl(bundleId);
+                        this.load();
+                        return;
+                    }
+
+                    load = bind(this, function (value) {
+                        this.init([], function () { return value; }, null, {
+                            enabled: true
+                        });
+                    });
+
+                    load.error = bind(this, function (err) {
+                        this.inited = true;
+                        this.error = err;
+                        err.requireModules = [id];
+
+                        //Remove temp unnormalized modules for this module,
+                        //since they will never be resolved otherwise now.
+                        eachProp(registry, function (mod) {
+                            if (mod.map.id.indexOf(id + '_unnormalized') === 0) {
+                                cleanRegistry(mod.map.id);
+                            }
+                        });
+
+                        onError(err);
+                    });
+
+                    //Allow plugins to load other code without having to know the
+                    //context or how to 'complete' the load.
+                    load.fromText = bind(this, function (text, textAlt) {
+                        /*jslint evil: true */
+                        var moduleName = map.name,
+                            moduleMap = makeModuleMap(moduleName),
+                            hasInteractive = useInteractive;
+
+                        //As of 2.1.0, support just passing the text, to reinforce
+                        //fromText only being called once per resource. Still
+                        //support old style of passing moduleName but discard
+                        //that moduleName in favor of the internal ref.
+                        if (textAlt) {
+                            text = textAlt;
+                        }
+
+                        //Turn off interactive script matching for IE for any define
+                        //calls in the text, then turn it back on at the end.
+                        if (hasInteractive) {
+                            useInteractive = false;
+                        }
+
+                        //Prime the system by creating a module instance for
+                        //it.
+                        getModule(moduleMap);
+
+                        //Transfer any config to this other module.
+                        if (hasProp(config.config, id)) {
+                            config.config[moduleName] = config.config[id];
+                        }
+
+                        try {
+                            req.exec(text);
+                        } catch (e) {
+                            return onError(makeError('fromtexteval',
+                                             'fromText eval for ' + id +
+                                            ' failed: ' + e,
+                                             e,
+                                             [id]));
+                        }
+
+                        if (hasInteractive) {
+                            useInteractive = true;
+                        }
+
+                        //Mark this as a dependency for the plugin
+                        //resource
+                        this.depMaps.push(moduleMap);
+
+                        //Support anonymous modules.
+                        context.completeLoad(moduleName);
+
+                        //Bind the value of that module to the value for this
+                        //resource ID.
+                        localRequire([moduleName], load);
+                    });
+
+                    //Use parentName here since the plugin's name is not reliable,
+                    //could be some weird string with no path that actually wants to
+                    //reference the parentName's path.
+                    plugin.load(map.name, localRequire, load, config);
+                }));
+
+                context.enable(pluginMap, this);
+                this.pluginMaps[pluginMap.id] = pluginMap;
+            },
+
+            enable: function () {
+                enabledRegistry[this.map.id] = this;
+                this.enabled = true;
+
+                //Set flag mentioning that the module is enabling,
+                //so that immediate calls to the defined callbacks
+                //for dependencies do not trigger inadvertent load
+                //with the depCount still being zero.
+                this.enabling = true;
+
+                //Enable each dependency
+                each(this.depMaps, bind(this, function (depMap, i) {
+                    var id, mod, handler;
+
+                    if (typeof depMap === 'string') {
+                        //Dependency needs to be converted to a depMap
+                        //and wired up to this module.
+                        depMap = makeModuleMap(depMap,
+                                               (this.map.isDefine ? this.map : this.map.parentMap),
+                                               false,
+                                               !this.skipMap);
+                        this.depMaps[i] = depMap;
+
+                        handler = getOwn(handlers, depMap.id);
+
+                        if (handler) {
+                            this.depExports[i] = handler(this);
+                            return;
+                        }
+
+                        this.depCount += 1;
+
+                        on(depMap, 'defined', bind(this, function (depExports) {
+                            this.defineDep(i, depExports);
+                            this.check();
+                        }));
+
+                        if (this.errback) {
+                            on(depMap, 'error', bind(this, this.errback));
+                        }
+                    }
+
+                    id = depMap.id;
+                    mod = registry[id];
+
+                    //Skip special modules like 'require', 'exports', 'module'
+                    //Also, don't call enable if it is already enabled,
+                    //important in circular dependency cases.
+                    if (!hasProp(handlers, id) && mod && !mod.enabled) {
+                        context.enable(depMap, this);
+                    }
+                }));
+
+                //Enable each plugin that is used in
+                //a dependency
+                eachProp(this.pluginMaps, bind(this, function (pluginMap) {
+                    var mod = getOwn(registry, pluginMap.id);
+                    if (mod && !mod.enabled) {
+                        context.enable(pluginMap, this);
+                    }
+                }));
+
+                this.enabling = false;
+
+                this.check();
+            },
+
+            on: function (name, cb) {
+                var cbs = this.events[name];
+                if (!cbs) {
+                    cbs = this.events[name] = [];
+                }
+                cbs.push(cb);
+            },
+
+            emit: function (name, evt) {
+                each(this.events[name], function (cb) {
+                    cb(evt);
+                });
+                if (name === 'error') {
+                    //Now that the error handler was triggered, remove
+                    //the listeners, since this broken Module instance
+                    //can stay around for a while in the registry.
+                    delete this.events[name];
+                }
+            }
+        };
+
+        function callGetModule(args) {
+            //Skip modules already defined.
+            if (!hasProp(defined, args[0])) {
+                getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]);
+            }
+        }
+
+        function removeListener(node, func, name, ieName) {
+            //Favor detachEvent because of IE9
+            //issue, see attachEvent/addEventListener comment elsewhere
+            //in this file.
+            if (node.detachEvent && !isOpera) {
+                //Probably IE. If not it will throw an error, which will be
+                //useful to know.
+                if (ieName) {
+                    node.detachEvent(ieName, func);
+                }
+            } else {
+                node.removeEventListener(name, func, false);
+            }
+        }
+
+        /**
+         * Given an event from a script node, get the requirejs info from it,
+         * and then removes the event listeners on the node.
+         * @param {Event} evt
+         * @returns {Object}
+         */
+        function getScriptData(evt) {
+            //Using currentTarget instead of target for Firefox 2.0's sake. Not
+            //all old browsers will be supported, but this one was easy enough
+            //to support and still makes sense.
+            var node = evt.currentTarget || evt.srcElement;
+
+            //Remove the listeners once here.
+            removeListener(node, context.onScriptLoad, 'load', 'onreadystatechange');
+            removeListener(node, context.onScriptError, 'error');
+
+            return {
+                node: node,
+                id: node && node.getAttribute('data-requiremodule')
+            };
+        }
+
+        function intakeDefines() {
+            var args;
+
+            //Any defined modules in the global queue, intake them now.
+            takeGlobalQueue();
+
+            //Make sure any remaining defQueue items get properly processed.
+            while (defQueue.length) {
+                args = defQueue.shift();
+                if (args[0] === null) {
+                    return onError(makeError('mismatch', 'Mismatched anonymous define() module: ' + args[args.length - 1]));
+                } else {
+                    //args are id, deps, factory. Should be normalized by the
+                    //define() function.
+                    callGetModule(args);
+                }
+            }
+        }
+
+        context = {
+            config: config,
+            contextName: contextName,
+            registry: registry,
+            defined: defined,
+            urlFetched: urlFetched,
+            defQueue: defQueue,
+            Module: Module,
+            makeModuleMap: makeModuleMap,
+            nextTick: req.nextTick,
+            onError: onError,
+
+            /**
+             * Set a configuration for the context.
+             * @param {Object} cfg config object to integrate.
+             */
+            configure: function (cfg) {
+                //Make sure the baseUrl ends in a slash.
+                if (cfg.baseUrl) {
+                    if (cfg.baseUrl.charAt(cfg.baseUrl.length - 1) !== '/') {
+                        cfg.baseUrl += '/';
+                    }
+                }
+
+                //Save off the paths since they require special processing,
+                //they are additive.
+                var shim = config.shim,
+                    objs = {
+                        paths: true,
+                        bundles: true,
+                        config: true,
+                        map: true
+                    };
+
+                eachProp(cfg, function (value, prop) {
+                    if (objs[prop]) {
+                        if (!config[prop]) {
+                            config[prop] = {};
+                        }
+                        mixin(config[prop], value, true, true);
+                    } else {
+                        config[prop] = value;
+                    }
+                });
+
+                //Reverse map the bundles
+                if (cfg.bundles) {
+                    eachProp(cfg.bundles, function (value, prop) {
+                        each(value, function (v) {
+                            if (v !== prop) {
+                                bundlesMap[v] = prop;
+                            }
+                        });
+                    });
+                }
+
+                //Merge shim
+                if (cfg.shim) {
+                    eachProp(cfg.shim, function (value, id) {
+                        //Normalize the structure
+                        if (isArray(value)) {
+                            value = {
+                                deps: value
+                            };
+                        }
+                        if ((value.exports || value.init) && !value.exportsFn) {
+                            value.exportsFn = context.makeShimExports(value);
+                        }
+                        shim[id] = value;
+                    });
+                    config.shim = shim;
+                }
+
+                //Adjust packages if necessary.
+                if (cfg.packages) {
+                    each(cfg.packages, function (pkgObj) {
+                        var location, name;
+
+                        pkgObj = typeof pkgObj === 'string' ? { name: pkgObj } : pkgObj;
+
+                        name = pkgObj.name;
+                        location = pkgObj.location;
+                        if (location) {
+                            config.paths[name] = pkgObj.location;
+                        }
+
+                        //Save pointer to main module ID for pkg name.
+                        //Remove leading dot in main, so main paths are normalized,
+                        //and remove any trailing .js, since different package
+                        //envs have different conventions: some use a module name,
+                        //some use a file name.
+                        config.pkgs[name] = pkgObj.name + '/' + (pkgObj.main || 'main')
+                                     .replace(currDirRegExp, '')
+                                     .replace(jsSuffixRegExp, '');
+                    });
+                }
+
+                //If there are any "waiting to execute" modules in the registry,
+                //update the maps for them, since their info, like URLs to load,
+                //may have changed.
+                eachProp(registry, function (mod, id) {
+                    //If module already has init called, since it is too
+                    //late to modify them, and ignore unnormalized ones
+                    //since they are transient.
+         &nb