Merge branch 'MDL-40825_master' of git://github.com/dmonllao/moodle
authorMarina Glancy <marina@moodle.com>
Mon, 5 Aug 2013 03:15:25 +0000 (13:15 +1000)
committerMarina Glancy <marina@moodle.com>
Mon, 5 Aug 2013 03:15:25 +0000 (13:15 +1000)
248 files changed:
admin/environment.xml
admin/settings/appearance.php
admin/tool/uploadcourse/classes/course.php
admin/tool/uploadcourse/classes/helper.php
admin/tool/uploadcourse/tests/helper_test.php
backup/moodle2/restore_stepslib.php
backup/upgrade.txt
backup/util/dbops/backup_controller_dbops.class.php
backup/util/dbops/restore_dbops.class.php
backup/util/dbops/tests/backup_dbops_test.php
backup/util/ui/tests/behat/duplicate_activities.feature
badges/lib/backpacklib.php
blocks/comments/tests/behat/behat_block_comments.php
blocks/course_overview/locallib.php
blocks/site_main_menu/block_site_main_menu.php
blocks/social_activities/block_social_activities.php
blog/tests/behat/comment.feature
cache/stores/file/lib.php
cache/tests/cache_test.php
comment/comment.js
course/dndupload.js
course/dnduploadlib.php
course/format/upgrade.txt
course/lib.php
course/renderer.php
course/tests/behat/activities_indentation.feature
course/tests/behat/behat_course.php
course/tests/behat/course_controls.feature
course/yui/dragdrop/dragdrop.js
course/yui/toolboxes/toolboxes.js
enrol/meta/locallib.php
enrol/meta/settings.php
enrol/meta/tests/plugin_test.php [new file with mode: 0644]
files/renderer.php
grade/report/user/lib.php
install/lang/ca/error.php
install/lang/fa/admin.php
install/lang/fa/moodle.php
install/lang/ru/install.php
lang/en/admin.php
lib/behat/lib.php
lib/blocklib.php
lib/classes/minify.php [new file with mode: 0644]
lib/csslib.php
lib/db/caches.php
lib/db/upgrade.php
lib/db/upgradelib.php
lib/ddl/mssql_sql_generator.php
lib/deprecatedlib.php
lib/dml/mssql_native_moodle_database.php
lib/dml/sqlsrv_native_moodle_database.php
lib/editor/tinymce/plugins/loader.php
lib/editor/tinymce/plugins/pdw/readme_moodle.txt
lib/editor/tinymce/plugins/pdw/tinymce/editor_plugin.js
lib/editor/tinymce/plugins/pdw/tinymce/editor_plugin_src.js [deleted file]
lib/filestorage/zip_archive.php
lib/grouplib.php
lib/javascript.php
lib/jslib.php
lib/minify/config.php
lib/minify/lib/CSSmin.php [new file with mode: 0644]
lib/minify/lib/DooDigestAuth.php [new file with mode: 0644]
lib/minify/lib/Minify.php
lib/minify/lib/Minify/Build.php
lib/minify/lib/Minify/CSS.php
lib/minify/lib/Minify/Cache/File.php
lib/minify/lib/Minify/Cache/XCache.php [new file with mode: 0644]
lib/minify/lib/Minify/ClosureCompiler.php [new file with mode: 0644]
lib/minify/lib/Minify/Controller/Base.php
lib/minify/lib/Minify/Controller/Files.php
lib/minify/lib/Minify/Controller/Groups.php
lib/minify/lib/Minify/Controller/MinApp.php
lib/minify/lib/Minify/Controller/Page.php
lib/minify/lib/Minify/Controller/Version1.php
lib/minify/lib/Minify/HTML.php
lib/minify/lib/Minify/HTML/Helper.php
lib/minify/lib/Minify/JS/ClosureCompiler.php
lib/minify/lib/Minify/Lines.php
lib/minify/lib/Minify/Loader.php [new file with mode: 0644]
lib/minify/lib/Minify/YUICompressor.php
lib/minify/lib/MrClay/Cli.php
lib/minify/lib/MrClay/Cli/Arg.php
lib/minify/readme_moodle.txt
lib/minify/utils.php
lib/modinfolib.php
lib/moodlelib.php
lib/outputcomponents.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/questionlib.php
lib/setuplib.php
lib/testing/classes/util.php
lib/tests/accesslib_test.php
lib/tests/admintree_test.php
lib/tests/authlib_test.php
lib/tests/behat/behat_hooks.php
lib/tests/blocklib_test.php
lib/tests/code_test.php
lib/tests/collator_test.php
lib/tests/completionlib_advanced_test.php [deleted file]
lib/tests/completionlib_test.php
lib/tests/component_test.php
lib/tests/componentlib_test.php
lib/tests/conditionlib_test.php
lib/tests/configonlylib_test.php
lib/tests/coursecatlib_test.php
lib/tests/csslib_test.php
lib/tests/csvclass_test.php
lib/tests/datalib_test.php
lib/tests/environment_test.php
lib/tests/event_test.php
lib/tests/eventslib_test.php
lib/tests/externallib_test.php
lib/tests/filelib_test.php
lib/tests/filterlib_test.php [moved from lib/tests/filter_test.php with 74% similarity]
lib/tests/formslib_test.php
lib/tests/gradelib_test.php
lib/tests/grouplib_test.php
lib/tests/html2text_test.php
lib/tests/html_writer_test.php [moved from lib/tests/htmlwriter_test.php with 69% similarity]
lib/tests/htmlpurifier_test.php
lib/tests/markdown_test.php
lib/tests/mathslib_test.php
lib/tests/medialib_test.php
lib/tests/messagelib_test.php
lib/tests/minify_test.php [new file with mode: 0644]
lib/tests/modinfolib_test.php
lib/tests/moodle_page_test.php [moved from lib/tests/pagelib_test.php with 71% similarity]
lib/tests/moodlelib_test.php
lib/tests/navigationlib_test.php
lib/tests/outputcomponents_test.php
lib/tests/outputrequirementslib_test.php
lib/tests/pluginlib_test.php
lib/tests/questionlib_test.php
lib/tests/rsslib_test.php
lib/tests/setuplib_test.php
lib/tests/statslib_test.php
lib/tests/string_manager_test.php [moved from lib/tests/string_test.php with 94% similarity]
lib/tests/text_test.php
lib/tests/theme_config_test.php [moved from lib/tests/outputlib_test.php with 53% similarity]
lib/tests/upgradelib_test.php
lib/tests/weblib_test.php
lib/tests/xhtml_container_stack_test.php [new file with mode: 0644]
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/weblib.php
lib/yui/build/moodle-core-actionmenu/moodle-core-actionmenu-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-actionmenu/moodle-core-actionmenu-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-actionmenu/moodle-core-actionmenu.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception.js [new file with mode: 0644]
lib/yui/build/moodle-core-notification/moodle-core-notification-debug.js
lib/yui/build/moodle-core-notification/moodle-core-notification-min.js
lib/yui/build/moodle-core-notification/moodle-core-notification.js
lib/yui/src/actionmenu/build.json [new file with mode: 0644]
lib/yui/src/actionmenu/js/actionmenu.js [new file with mode: 0644]
lib/yui/src/actionmenu/meta/actionmenu.json [new file with mode: 0644]
lib/yui/src/notification/build.json
lib/yui/src/notification/js/ajaxexception.js [new file with mode: 0644]
lib/yui/src/notification/js/alert.js [new file with mode: 0644]
lib/yui/src/notification/js/confirm.js [new file with mode: 0644]
lib/yui/src/notification/js/dialogue.js [new file with mode: 0644]
lib/yui/src/notification/js/exception.js [new file with mode: 0644]
lib/yui/src/notification/js/notification.js
lib/yui/src/notification/js/shared.js [new file with mode: 0644]
lib/yui/src/notification/meta/notification.json
mod/assign/db/services.php
mod/assign/externallib.php
mod/assign/feedback/file/importziplib.php
mod/assign/tests/externallib_test.php
mod/assign/version.php
mod/assignment/lib.php
mod/choice/styles.css
mod/feedback/import_form.php
mod/feedback/lib.php
mod/feedback/styles.css
mod/forum/lib.php
mod/label/lib.php
mod/lti/db/access.php
mod/lti/lang/en/lti.php
mod/lti/lib.php
mod/lti/locallib.php
mod/lti/mod_form.php
mod/lti/service.php
mod/page/lib.php
mod/quiz/report/statistics/lib.php
mod/quiz/report/statistics/report.php
mod/resource/lib.php
mod/url/lib.php
question/editlib.php
question/format/aiken/format.php
question/format/aiken/lang/en/qformat_aiken.php
question/format/aiken/version.php
question/format/blackboard_six/format.php
question/format/blackboard_six/formatqti.php
question/format/examview/format.php
question/format/gift/format.php
question/format/gift/lang/en/qformat_gift.php
question/format/gift/tests/giftformat_test.php
question/format/gift/version.php
question/format/learnwise/format.php
question/format/learnwise/lang/en/qformat_learnwise.php
question/format/learnwise/version.php
question/format/missingword/format.php
question/format/missingword/lang/en/qformat_missingword.php
question/format/missingword/version.php
question/format/multianswer/lang/en/qformat_multianswer.php
question/format/multianswer/version.php
question/format/upgrade.txt
question/format/webct/TODO.txt
question/format/webct/format.php
question/format/webct/lang/en/qformat_webct.php
question/format/webct/version.php
question/format/xhtml/format.php
question/format/xhtml/lang/en/qformat_xhtml.php
question/format/xhtml/version.php
question/format/xhtml/xhtml.css
question/format/xml/format.php
question/format/xml/lang/en/qformat_xml.php
question/format/xml/tests/xmlformat_test.php
question/format/xml/version.php
question/upgrade.txt [new file with mode: 0644]
report/log/locallib.php
repository/s3/README_MOODLE.txt
repository/s3/S3.php
theme/base/style/core.css
theme/base/style/course.css
theme/bootstrapbase/less/moodle/blocks.less
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/style/moodle.css
theme/javascript.php
theme/upgrade.txt
user/filters/date.php
version.php

index 5fac54c..1e937cd 100644 (file)
           <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>
   </MOODLE>
 </COMPATIBILITY_MATRIX>
index 1367452..1ee3a7a 100644 (file)
@@ -214,6 +214,8 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $setting->set_updatedcallback('js_reset_all_caches');
     $temp->add($setting);
     $temp->add(new admin_setting_configcheckbox('modchooserdefault', new lang_string('modchooserdefault', 'admin'), new lang_string('configmodchooserdefault', 'admin'), 1));
+    $temp->add(new admin_setting_configcheckbox('modeditingmenu', new lang_string('modeditingmenu', 'admin'), new lang_string('modeditingmenu_desc', 'admin'), 1));
+    $temp->add(new admin_setting_configcheckbox('blockeditingmenu', new lang_string('blockeditingmenu', 'admin'), new lang_string('blockeditingmenu_desc', 'admin'), 1));
     $ADMIN->add('appearance', $temp);
 
     // link to tag management interface
index 629e498..faa404d 100644 (file)
@@ -715,6 +715,7 @@ class tool_uploadcourse_course {
                 $this->error('errorwhilerestoringcourse', new lang_string('errorwhilerestoringthecourse', 'tool_uploadcourse'));
             }
             $rc->destroy();
+            unset($rc); // File logging is a mess, we can only try to rely on gc to close handles.
         }
 
         // Proceed with enrolment data.
index b328a42..11ff373 100644 (file)
@@ -44,6 +44,13 @@ class tool_uploadcourse_helper {
      */
     public static function clean_restore_content() {
         global $CFG;
+
+        // There are some sloppy unclosed file handles in backup/restore code,
+        // let's hope somebody unset all controllers before calling this
+        // and destroy magic will close all remaining open file handles,
+        // otherwise Windows will fail deleting the directory.
+        gc_collect_cycles();
+
         if (!empty($CFG->keeptempdirectoriesonbackup)) {
             $cache = cache::make('tool_uploadcourse', 'helper');
             $backupids = (array) $cache->get('backupids');
index 873e221..31e53e0 100644 (file)
@@ -129,6 +129,7 @@ class tool_uploadcourse_helper_testcase extends advanced_testcase {
         $this->assertTrue(isset($result['backup_destination']));
         $c1backupfile = $result['backup_destination']->copy_content_to_temp();
         $bc->destroy();
+        unset($bc); // File logging is a mess, we can only try to rely on gc to close handles.
 
         // Creating backup file.
         $bc = new backup_controller(backup::TYPE_1COURSE, $c2->id, backup::FORMAT_MOODLE,
@@ -138,6 +139,7 @@ class tool_uploadcourse_helper_testcase extends advanced_testcase {
         $this->assertTrue(isset($result['backup_destination']));
         $c2backupfile = $result['backup_destination']->copy_content_to_temp();
         $bc->destroy();
+        unset($bc); // File logging is a mess, we can only try to rely on gc to close handles.
 
         // Checking restore dir.
         $dir = tool_uploadcourse_helper::get_restore_content_dir($c1backupfile, null);
index ed36b0a..2b6cdab 100644 (file)
@@ -510,11 +510,11 @@ class restore_review_pending_block_positions extends restore_execution_step {
 
         // Get all the block_position objects pending to match
         $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'block_position');
-        $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid');
+        $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
         // Process block positions, creating them or accumulating for final step
         foreach($rs as $posrec) {
-            // Get the complete position object (stored as info)
-            $position = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'block_position', $posrec->itemid)->info;
+            // Get the complete position object out of the info field.
+            $position = backup_controller_dbops::decode_backup_temp_info($posrec->info);
             // If position is for one already mapped (known) contextid
             // process it now, creating the position, else nothing to
             // do, position finally discarded
@@ -546,12 +546,12 @@ class restore_process_course_modules_availability extends restore_execution_step
 
         // Get all the module_availability objects to process
         $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'module_availability');
-        $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid');
+        $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
         // Process availabilities, creating them if everything matches ok
         foreach($rs as $availrec) {
             $allmatchesok = true;
             // Get the complete availabilityobject
-            $availability = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'module_availability', $availrec->itemid)->info;
+            $availability = backup_controller_dbops::decode_backup_temp_info($availrec->info);
             // Map the sourcecmid if needed and possible
             if (!empty($availability->sourcecmid)) {
                 $newcm = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'course_module', $availability->sourcecmid);
@@ -3624,7 +3624,7 @@ class restore_process_file_aliases_queue extends restore_execution_step {
 
         // Iterate over aliases in the queue.
         foreach ($rs as $record) {
-            $info = unserialize(base64_decode($record->info));
+            $info = restore_dbops::decode_backup_temp_info($record->info);
 
             // Try to pick a repository instance that should serve the alias.
             $repository = $this->choose_repository($info);
@@ -3657,7 +3657,7 @@ class restore_process_file_aliases_queue extends restore_execution_step {
                 $source = null;
 
                 foreach ($candidates as $candidate) {
-                    $candidateinfo = unserialize(base64_decode($candidate->info));
+                    $candidateinfo = backup_controller_dbops::decode_backup_temp_info($candidate->info);
                     if ($candidateinfo->filename === $reference['filename']
                             and $candidateinfo->filepath === $reference['filepath']
                             and !is_null($candidate->newcontextid)
index 586ee70..26e2e00 100644 (file)
@@ -7,6 +7,12 @@ information provided here is intended especially for developers.
   method is not available anymore. Temp tables must be created
   inline always.
 
+* Using the info field from backup_ids_temp or backup_files_temp
+  must now go via backup_controller_dbops::decode_backup_temp_info() and
+  backup_controller_dbops::encode_backup_temp_info(). The implementation
+  of the encoding has changed.  These new functions encapsulate any future
+  changes to the encoding.
+
 === 2.5 ===
 
 * New optional param $sortby in backup set_source_table() allows to
index 085fe22..668ff61 100644 (file)
@@ -154,6 +154,44 @@ abstract class backup_controller_dbops extends backup_dbops {
         $dbman->drop_table($table); // And drop it
     }
 
+    /**
+     * Decode the info field from backup_ids_temp or backup_files_temp.
+     *
+     * @param mixed $info The info field data to decode, may be an object or a simple integer.
+     * @return mixed The decoded information.  For simple types it returns, for complex ones we decode.
+     */
+    public static function decode_backup_temp_info($info) {
+        // We encode all data except null.
+        if ($info != null) {
+            if (extension_loaded('zlib')) {
+                return unserialize(gzuncompress(base64_decode($info)));
+            } else {
+                return unserialize(base64_decode($info));
+            }
+        }
+        return $info;
+    }
+
+    /**
+     * Encode the info field for backup_ids_temp or backup_files_temp.
+     *
+     * @param mixed $info string The info field data to encode.
+     * @return string An encoded string of data or null if the input is null.
+     */
+    public static function encode_backup_temp_info($info) {
+        // We encode if there is any information to keep the translations simpler.
+        if ($info != null) {
+            // We compress if possible. It reduces db, network and memory storage. The saving is greater than CPU compression cost.
+            // Compression level 1 is chosen has it produces good compression with the smallest possible overhead, see MDL-40618.
+            if (extension_loaded('zlib')) {
+                return base64_encode(gzcompress(serialize($info), 1));
+            } else {
+                return base64_encode(serialize($info));
+            }
+        }
+        return $info;
+    }
+
     /**
      * Given one type and id from controller, return the corresponding courseid
      */
index 12f2601..0e5fabc 100644 (file)
@@ -152,7 +152,7 @@ abstract class restore_dbops {
         $problems = array(); // To store warnings/errors
 
         // Get loaded roles from backup_ids
-        $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'role'), '', 'itemid');
+        $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'role'), '', 'itemid, info');
         foreach ($rs as $recrole) {
             // If the rolemappings->modified flag is set, that means that we are coming from
             // manually modified mappings (by UI), so accept those mappings an put them to backup_ids
@@ -163,14 +163,13 @@ abstract class restore_dbops {
             // Else, we haven't any info coming from UI, let's calculate the mappings, matching
             // in multiple ways and checking permissions. Note mapping to 0 means "skip"
             } else {
-                $role = (object)self::get_backup_ids_record($restoreid, 'role', $recrole->itemid)->info;
+                $role = (object)backup_controller_dbops::decode_backup_temp_info($recrole->info);
                 $match = self::get_best_assignable_role($role, $courseid, $userid, $samesite);
                 // Send match to backup_ids
                 self::set_backup_ids_record($restoreid, 'role', $recrole->itemid, $match);
                 // Build the rolemappings element for controller
                 unset($role->id);
                 unset($role->nameincourse);
-                unset($role->nameincourse);
                 $role->targetroleid = $match;
                 $rolemappings->mappings[$recrole->itemid] = $role;
                 // Prepare warning if no match found
@@ -673,20 +672,21 @@ abstract class restore_dbops {
         global $DB;
 
         $results = array();
-        $qcats = $DB->get_records_sql("SELECT itemid, parentitemid AS contextid
+        $qcats = $DB->get_recordset_sql("SELECT itemid, parentitemid AS contextid, info
                                          FROM {backup_ids_temp}
                                        WHERE backupid = ?
                                          AND itemname = 'question_category'", array($restoreid));
         foreach ($qcats as $qcat) {
             // If this qcat context haven't been acummulated yet, do that
             if (!isset($results[$qcat->contextid])) {
-                $temprec = self::get_backup_ids_record($restoreid, 'question_category', $qcat->itemid);
+                $info = backup_controller_dbops::decode_backup_temp_info($qcat->info);
                 // Filter by contextlevel if necessary
-                if (is_null($contextlevel) || $contextlevel == $temprec->info->contextlevel) {
-                    $results[$qcat->contextid] = $temprec->info->contextlevel;
+                if (is_null($contextlevel) || $contextlevel == $info->contextlevel) {
+                    $results[$qcat->contextid] = $info->contextlevel;
                 }
             }
         }
+        $qcats->close();
         // Sort by value (contextlevel from CONTEXT_SYSTEM downto CONTEXT_MODULE)
         asort($results);
         return $results;
@@ -700,15 +700,16 @@ abstract class restore_dbops {
         global $DB;
 
         $results = array();
-        $qcats = $DB->get_records_sql("SELECT itemid
+        $qcats = $DB->get_recordset_sql("SELECT itemid, info
                                          FROM {backup_ids_temp}
                                         WHERE backupid = ?
                                           AND itemname = 'question_category'
                                           AND parentitemid = ?", array($restoreid, $contextid));
         foreach ($qcats as $qcat) {
-            $temprec = self::get_backup_ids_record($restoreid, 'question_category', $qcat->itemid);
-            $results[$qcat->itemid] = $temprec->info;
+            $results[$qcat->itemid] = backup_controller_dbops::decode_backup_temp_info($qcat->info);
         }
+        $qcats->close();
+
         return $results;
     }
 
@@ -798,15 +799,15 @@ abstract class restore_dbops {
         global $DB;
 
         $results = array();
-        $qs = $DB->get_records_sql("SELECT itemid
+        $qs = $DB->get_recordset_sql("SELECT itemid, info
                                       FROM {backup_ids_temp}
                                      WHERE backupid = ?
                                        AND itemname = 'question'
                                        AND parentitemid = ?", array($restoreid, $qcatid));
         foreach ($qs as $q) {
-            $temprec = self::get_backup_ids_record($restoreid, 'question', $q->itemid);
-            $results[$q->itemid] = $temprec->info;
+            $results[$q->itemid] = backup_controller_dbops::decode_backup_temp_info($q->info);
         }
+        $qs->close();
         return $results;
     }
 
@@ -893,7 +894,7 @@ abstract class restore_dbops {
         $basepath = $basepath . '/files/';// Get backup file pool base
         $rs = $DB->get_recordset_sql($sql, $params);
         foreach ($rs as $rec) {
-            $file = (object)unserialize(base64_decode($rec->info));
+            $file = (object)backup_controller_dbops::decode_backup_temp_info($rec->info);
 
             // ignore root dirs (they are created automatically)
             if ($file->filepath == '/' && $file->filename == '.') {
@@ -1018,9 +1019,9 @@ abstract class restore_dbops {
         $themes    = get_list_of_themes(); // Get themes for quick search later
 
         // Iterate over all the included users with newitemid = 0, have to create them
-        $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'user', 'newitemid' => 0), '', 'itemid, parentitemid');
+        $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'user', 'newitemid' => 0), '', 'itemid, parentitemid, info');
         foreach ($rs as $recuser) {
-            $user = (object)self::get_backup_ids_record($restoreid, 'user', $recuser->itemid)->info;
+            $user = (object)backup_controller_dbops::decode_backup_temp_info($recuser->info);
 
             // if user lang doesn't exist here, use site default
             if (!array_key_exists($user->lang, $languages)) {
@@ -1409,9 +1410,9 @@ abstract class restore_dbops {
         }
 
         // Iterate over all the included users
-        $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'user'), '', 'itemid');
+        $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'user'), '', 'itemid, info');
         foreach ($rs as $recuser) {
-            $user = (object)self::get_backup_ids_record($restoreid, 'user', $recuser->itemid)->info;
+            $user = (object)backup_controller_dbops::decode_backup_temp_info($recuser->info);
 
             // Find the correct mnethostid for user before performing any further check
             if (empty($user->mnethosturl) || $user->mnethosturl === $CFG->wwwroot) {
@@ -1496,7 +1497,7 @@ abstract class restore_dbops {
         global $DB;
 
         // Store external files info in `info` field
-        $filerec->info     = base64_encode(serialize($filerec)); // Serialize the whole rec in info
+        $filerec->info     = backup_controller_dbops::encode_backup_temp_info($filerec); // Encode the whole record into info.
         $filerec->backupid = $restoreid;
         $DB->insert_record('backup_files_temp', $filerec);
     }
@@ -1511,7 +1512,7 @@ abstract class restore_dbops {
             $extrarecord['parentitemid'] = $parentitemid;
         }
         if ($info != null) {
-            $extrarecord['info'] = base64_encode(serialize($info));
+            $extrarecord['info'] = backup_controller_dbops::encode_backup_temp_info($info);
         }
 
         self::set_backup_ids_cached($restoreid, $itemname, $itemid, $extrarecord);
@@ -1520,8 +1521,9 @@ abstract class restore_dbops {
     public static function get_backup_ids_record($restoreid, $itemname, $itemid) {
         $dbrec = self::get_backup_ids_cached($restoreid, $itemname, $itemid);
 
+        // We must test if info is a string, as the cache stores info in object form.
         if ($dbrec && isset($dbrec->info) && is_string($dbrec->info)) {
-            $dbrec->info = unserialize(base64_decode($dbrec->info));
+            $dbrec->info = backup_controller_dbops::decode_backup_temp_info($dbrec->info);
         }
 
         return $dbrec;
@@ -1566,18 +1568,17 @@ abstract class restore_dbops {
         // Get the course context
         $coursectx = context_course::instance($courseid);
         // Get all the mapped roles we have
-        $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'role'), '', 'itemid');
+        $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'role'), '', 'itemid, info, newitemid');
         foreach ($rs as $recrole) {
-            // Get the complete temp_ids record
-            $role = (object)self::get_backup_ids_record($restoreid, 'role', $recrole->itemid);
+            $info = backup_controller_dbops::decode_backup_temp_info($recrole->info);
             // If it's one mapped role and we have one name for it
-            if (!empty($role->newitemid) && !empty($role->info['nameincourse'])) {
+            if (!empty($recrole->newitemid) && !empty($info['nameincourse'])) {
                 // If role name doesn't exist, add it
                 $rolename = new stdclass();
-                $rolename->roleid = $role->newitemid;
+                $rolename->roleid = $recrole->newitemid;
                 $rolename->contextid = $coursectx->id;
                 if (!$DB->record_exists('role_names', (array)$rolename)) {
-                    $rolename->name = $role->info['nameincourse'];
+                    $rolename->name = $info['nameincourse'];
                     $DB->insert_record('role_names', $rolename);
                 }
             }
index 55d57ba..e57dc7e 100644 (file)
@@ -147,6 +147,18 @@ class backup_dbops_testcase extends advanced_testcase {
         // Drop and check it doesn't exists anymore
         backup_controller_dbops::drop_backup_ids_temp_table('testingid');
         $this->assertFalse($dbman->table_exists('backup_ids_temp'));
+
+        // Test encoding/decoding of backup_ids_temp,backup_files_temp encode/decode functions.
+        // We need to handle both objects and data elements.
+        $object = new stdClass();
+        $object->item1 = 10;
+        $object->item2 = 'a String';
+        $testarray = array($object, 10, null, 'string', array('a' => 'b', 1 => 1));
+        foreach ($testarray as $item) {
+            $encoded = backup_controller_dbops::encode_backup_temp_info($item);
+            $decoded = backup_controller_dbops::decode_backup_temp_info($encoded);
+            $this->assertEquals($item, $decoded);
+        }
     }
 
     /**
index 3af2839..099a498 100644 (file)
@@ -21,7 +21,8 @@ Feature: Duplicate activities
     And I add a "Database" to section "1" and I fill the form with:
       | Name | Test database name |
       | Description | Test database description |
-    When I click on "Duplicate" "link" in the "#section-1" "css_element"
+    And I click on "Actions" "link" in the "Test database name" activity
+    When I click on "Duplicate" "link" in the "Test database name" activity
     And I press "Continue"
     And I press "Edit the new copy"
     And I fill the moodle form with:
index 58a9b74..4ecf084 100644 (file)
@@ -60,10 +60,11 @@ class OpenBadgesBackpackHandler {
         }
 
         $options = array(
-            'FRESH_CONNECT' => true,
+            'FRESH_CONNECT'  => true,
             'RETURNTRANSFER' => true,
-            'FORBID_REUSE' => true,
-            'HEADER' => 0,
+            'FORBID_REUSE'   => true,
+            'HEADER'         => 0,
+            'HTTPHEADER'     => array('Expect:'),
             'CONNECTTIMEOUT' => 3,
         );
 
index 3cac07a..e7eaf88 100644 (file)
@@ -103,10 +103,6 @@ class behat_block_comments extends behat_base {
         $deleteicon = $this->find('css', '.comment-delete a img', $deleteexception, $commentnode);
         $deleteicon->click();
 
-        // Yes confirm.
-        $confirmnode = $this->find('xpath', "//div[@class='comment-delete-confirm']/descendant::a[contains(., '" . get_string('yes') . "')]");
-        $confirmnode->click();
-
         // Wait for the AJAX request.
         $this->getSession()->wait(4 * 1000, false);
     }
index bc159de..71267b6 100644 (file)
 function block_course_overview_get_overviews($courses) {
     $htmlarray = array();
     if ($modules = get_plugin_list_with_function('mod', 'print_overview')) {
-        foreach ($modules as $fname) {
-            $fname($courses,$htmlarray);
+        // Split courses list into batches with no more than MAX_MODINFO_CACHE_SIZE courses in one batch.
+        // Otherwise we exceed the cache limit in get_fast_modinfo() and rebuild it too often.
+        if (defined('MAX_MODINFO_CACHE_SIZE') && MAX_MODINFO_CACHE_SIZE > 0 && count($courses) > MAX_MODINFO_CACHE_SIZE) {
+            $batches = array_chunk($courses, MAX_MODINFO_CACHE_SIZE, true);
+        } else {
+            $batches = array($courses);
+        }
+        foreach ($batches as $courses) {
+            foreach ($modules as $fname) {
+                $fname($courses, $htmlarray);
+            }
         }
     }
     return $htmlarray;
index bc4420c..fc150c2 100644 (file)
@@ -59,7 +59,8 @@ class block_site_main_menu extends block_list {
             return $this->content;
         }
 
-/// slow & hacky editing mode
+        // Slow & hacky editing mode.
+        /** @var core_course_renderer $courserenderer */
         $courserenderer = $this->page->get_renderer('core', 'course');
         $ismoving = ismoving($course->id);
         course_create_sections_if_missing($course, 0);
@@ -72,7 +73,7 @@ class block_site_main_menu extends block_list {
             $strcancel= get_string('cancel');
             $stractivityclipboard = $USER->activitycopyname;
         }
-    /// Casting $course->modinfo to string prevents one notice when the field is null
+        // Casting $course->modinfo to string prevents one notice when the field is null.
         $editbuttons = '';
 
         if ($ismoving) {
@@ -90,8 +91,9 @@ class block_site_main_menu extends block_list {
                 if (!$ismoving) {
                     $actions = course_get_cm_edit_actions($mod, -1);
                     $editbuttons = html_writer::tag('div',
-                            $courserenderer->course_section_cm_edit_actions($actions),
-                            array('class' => 'buttons'));
+                        $courserenderer->course_section_cm_edit_actions($actions, $mod, array('donotenhance' => true)),
+                        array('class' => 'buttons')
+                    );
                 } else {
                     $editbuttons = '';
                 }
index 5e394e0..bcab936 100644 (file)
@@ -62,7 +62,8 @@ class block_social_activities extends block_list {
         }
 
 
-/// slow & hacky editing mode
+        // Slow & hacky editing mode.
+        /** @var core_course_renderer $courserenderer */
         $courserenderer = $this->page->get_renderer('core', 'course');
         $ismoving = ismoving($course->id);
         $modinfo = get_fast_modinfo($course);
@@ -74,7 +75,7 @@ class block_social_activities extends block_list {
             $strcancel= get_string('cancel');
             $stractivityclipboard = $USER->activitycopyname;
         }
-    /// Casting $course->modinfo to string prevents one notice when the field is null
+        // Casting $course->modinfo to string prevents one notice when the field is null.
         $editbuttons = '';
 
         if ($ismoving) {
@@ -92,7 +93,7 @@ class block_social_activities extends block_list {
                 if (!$ismoving) {
                     $actions = course_get_cm_edit_actions($mod, -1);
                     $editbuttons = '<br />'.
-                            $courserenderer->course_section_cm_edit_actions($actions);
+                            $courserenderer->course_section_cm_edit_actions($actions, $mod);
                 } else {
                     $editbuttons = '';
                 }
index 7828194..1527a3c 100644 (file)
@@ -55,7 +55,6 @@ Feature: Comment on a blog entry
     And I follow "Save comment"
     And I wait "4" seconds
     When I click on ".comment-delete a" "css_element"
-    And I click on "Yes" "link"
     And I wait "4" seconds
     Then I should not see "$My own >nasty< \"string\"!"
     And I follow "Blog post from user 1"
index 96d11b2..adc1a9b 100644 (file)
@@ -266,7 +266,7 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i
         $this->definition = $definition;
         $hash = preg_replace('#[^a-zA-Z0-9]+#', '_', $this->definition->get_id());
         $this->path = $this->filestorepath.'/'.$hash;
-        make_writable_directory($this->path);
+        make_writable_directory($this->path, false);
         if ($this->prescan && $definition->get_mode() !== self::MODE_REQUEST) {
             $this->prescan = false;
         }
@@ -314,11 +314,13 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i
             return $this->path . '/' . $key . '.cache';
         } else {
             // We are using a single subdirectory to achieve 1 level.
-            $subdir = substr($key, 0, 3);
+           // We suffix the subdir so it does not clash with any windows
+           // reserved filenames like 'con'.
+            $subdir = substr($key, 0, 3) . '-cache';
             $dir = $this->path . '/' . $subdir;
             if ($create) {
                 // Create the directory. This function does it recursivily!
-                make_writable_directory($dir);
+                make_writable_directory($dir, false);
             }
             return $dir . '/' . $key . '.cache';
         }
index 5121875..1388004 100644 (file)
@@ -119,6 +119,23 @@ class cache_phpunit_tests extends advanced_testcase {
         }
     }
 
+    /**
+     * Tests for cache keys that would break on windows.
+     */
+    public function test_windows_nasty_keys() {
+        $instance = cache_config_phpunittest::instance();
+        $instance->phpunit_add_definition('phpunit/windowskeytest', array(
+            'mode' => cache_store::MODE_APPLICATION,
+            'component' => 'phpunit',
+            'area' => 'windowskeytest',
+            'simplekeys' => true,
+            'simpledata' => true
+        ));
+        $cache = cache::make('phpunit', 'windowskeytest');
+        $this->assertTrue($cache->set('contest', 'test data 1'));
+        $this->assertEquals('test data 1', $cache->get('contest'));
+    }
+
     /**
      * Tests the default application cache
      */
@@ -191,9 +208,9 @@ class cache_phpunit_tests extends advanced_testcase {
      * @param cache_loader $cache
      */
     protected function run_on_cache(cache_loader $cache) {
-        $key = 'testkey';
+        $key = 'contestkey';
         $datascalars = array('test data', null);
-        $dataarray = array('test' => 'data', 'part' => 'two');
+        $dataarray = array('contest' => 'data', 'part' => 'two');
         $dataobject = (object)$dataarray;
 
         foreach ($datascalars as $datascalar) {
@@ -850,7 +867,7 @@ class cache_phpunit_tests extends advanced_testcase {
         // OK data added, data invalidated, and invalidation time has been set.
         // Now we need to manually add back the data and adjust the invalidation time.
         $hash = md5(cache_store::MODE_APPLICATION.'/phpunit/eventinvalidationtest/'.$CFG->wwwroot.'phpunit');
-        $timefile = $CFG->dataroot."/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/las/lastinvalidation-$hash.cache";
+        $timefile = $CFG->dataroot."/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/las-cache/lastinvalidation-$hash.cache";
         // Make sure the file is correct.
         $this->assertTrue(file_exists($timefile));
         $timecont = serialize(cache::now() - 60); // Back 60sec in the past to force it to re-invalidate.
@@ -858,7 +875,7 @@ class cache_phpunit_tests extends advanced_testcase {
         file_put_contents($timefile, $timecont);
         $this->assertTrue(file_exists($timefile));
 
-        $datafile = $CFG->dataroot."/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/tes/testkey1-$hash.cache";
+        $datafile = $CFG->dataroot."/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/tes-cache/testkey1-$hash.cache";
         $datacont = serialize("test data 1");
         make_writable_directory(dirname($datafile));
         file_put_contents($datafile, $datacont);
index 544d82f..d1cf768 100644 (file)
@@ -59,11 +59,6 @@ M.core_comment = {
                     }, this);
                 }
                 scope.toggle_textarea(false);
-                CommentHelper.confirmoverlay = new Y.Overlay({
-bodyContent: '<div class="comment-delete-confirm"><a href="#" id="confirmdelete-'+this.client_id+'">'+M.str.moodle.yes+'</a> <a href="#" id="canceldelete-'+this.client_id+'">'+M.str.moodle.no+'</a></div>',
-                                        visible: false
-                                        });
-                CommentHelper.confirmoverlay.render(document.body);
             },
             post: function() {
                 var ta = Y.one('#dlg-content-'+this.client_id);
@@ -238,7 +233,6 @@ bodyContent: '<div class="comment-delete-confirm"><a href="#" id="confirmdelete-
             dodelete: function(id) { // note: delete is a reserved word in javascript, chrome and safary do not like it at all here!
                 var scope = this;
                 var params = {'commentid': id};
-                scope.cancel_delete();
                 function remove_dom(type, anim, cmt) {
                     cmt.remove();
                 }
@@ -294,37 +288,23 @@ bodyContent: '<div class="comment-delete-confirm"><a href="#" id="confirmdelete-
                         if (commentid[1]) {
                             Y.Event.purgeElement('#'+theid, false, 'click');
                         }
-                        node.on('click', function(e, node) {
+                        node.on('click', function(e) {
                             e.preventDefault();
-                            var width = CommentHelper.confirmoverlay.bodyNode.getStyle('width');
-                            var re = new RegExp("(\\d+).*", "i");
-                            var result = width.match(re);
-                            if (result[1]) {
-                                width = Number(result[1]);
-                            } else {
-                                width = 0;
+                            if (commentid[1]) {
+                                scope.dodelete(commentid[1]);
                             }
-                            //CommentHelper.confirmoverlay.set('xy', [e.pageX-(width/2), e.pageY]);
-                            CommentHelper.confirmoverlay.set('xy', [e.pageX-width-5, e.pageY]);
-                            CommentHelper.confirmoverlay.set('visible', true);
-                            Y.one('#canceldelete-'+scope.client_id).on('click', function(e) {
-                                e.preventDefault();
-                                scope.cancel_delete();
-                                });
-                            Y.Event.purgeElement('#confirmdelete-'+scope.client_id, false, 'click');
-                            Y.one('#confirmdelete-'+scope.client_id).on('click', function(e) {
-                                e.preventDefault();
-                                if (commentid[1]) {
-                                    scope.dodelete(commentid[1]);
-                                }
-                            });
-                        }, scope, node);
+                        });
+                        // Also handle space/enter key.
+                        node.on('key', function(e) {
+                            e.preventDefault();
+                            if (commentid[1]) {
+                                scope.dodelete(commentid[1]);
+                            }
+                        }, '13,32');
+                        // 13 and 32 are the keycodes for space and enter.
                     }
                 );
             },
-            cancel_delete: function() {
-                CommentHelper.confirmoverlay.set('visible', false);
-            },
             register_pagination: function() {
                 var scope = this;
                 // page buttons
@@ -374,7 +354,7 @@ bodyContent: '<div class="comment-delete-confirm"><a href="#" id="confirmdelete-
                 if (ta) {
                     //toggle_textarea.apply(ta, [false]);
                     //// reset textarea size
-                    ta.on('click', function() {
+                    ta.on('focus', function() {
                         this.toggle_textarea(true);
                     }, this);
                     //ta.onkeypress = function() {
index b1dbc63..118647c 100644 (file)
@@ -1053,8 +1053,12 @@ M.course_dndupload = {
      * @param sectionnumber the number of the selected course section
      */
     add_editing: function(elementid) {
+        var node = Y.one('#' + elementid);
         YUI().use('moodle-course-coursebase', function(Y) {
-            M.course.coursebase.invoke_function('setup_for_resource', '#' + elementid);
+            M.course.register_new_module(node);
         });
+        if (M.core.actionmenu && M.core.actionmenu.newDOMNode) {
+            M.core.actionmenu.newDOMNode(node);
+        }
     }
 };
index a1b8d02..fee7ec0 100644 (file)
@@ -744,11 +744,11 @@ class dndupload_ajax_processor {
         $resp->content = $mod->get_content();
         $resp->elementid = 'module-'.$mod->id;
         $actions = course_get_cm_edit_actions($mod, 0, $mod->sectionnum);
-        $resp->commands = ' '. $courserenderer->course_section_cm_edit_actions($actions);
+        $resp->commands = ' '. $courserenderer->course_section_cm_edit_actions($actions, $mod);
         $resp->onclick = $mod->get_on_click();
         $resp->visible = $mod->visible;
 
-        // if using groupings, then display grouping name
+        // If using groupings, then display grouping name.
         if (!empty($mod->groupingid) && has_capability('moodle/course:managegroups', $this->context)) {
             $groupings = groups_get_all_groupings($this->course->id);
             $resp->groupingname = format_string($groupings[$mod->groupingid]->name);
index 2481c55..c484953 100644 (file)
@@ -2,6 +2,11 @@ This files describes API changes for course formats
 
 Overview of this plugin type at http://docs.moodle.org/dev/Course_formats
 
+=== 2.6 ===
+
+* core_course_renderer::course_section_cm_edit_actions has two new optional arguments and now uses and action_menu component.
+* core_course_renderer::course_section_cm has been altered to call core_course_renderer::course_section_cm_edit_actions with the two new arguments
+
 === 2.5 ===
 
 * Functions responsible for output in course/lib.php are deprecated, the code is moved to
index 2c54cdb..670e6d2 100644 (file)
@@ -1773,7 +1773,7 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
     $editcaps = array('moodle/course:manageactivities', 'moodle/course:activityvisibility', 'moodle/role:assign');
     $dupecaps = array('moodle/backup:backuptargetimport', 'moodle/restore:restoretargetimport');
 
-    // no permission to edit anything
+    // No permission to edit anything.
     if (!has_any_capability($editcaps, $modcontext) and !has_all_capabilities($dupecaps, $coursecontext)) {
         return array();
     }
@@ -1799,20 +1799,20 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
     }
     $actions = array();
 
-    // AJAX edit title
+    // AJAX edit title.
     if ($mod->has_view() && $hasmanageactivities &&
                 (($mod->course == $COURSE->id && course_ajax_enabled($COURSE)) ||
                  ($mod->course == SITEID && course_ajax_enabled($SITE)))) {
         // we will not display link if we are on some other-course page (where we should not see this module anyway)
-        $actions['title'] = new action_link(
+        $actions['title'] = new action_menu_link_secondary(
             new moodle_url($baseurl, array('update' => $mod->id)),
             new pix_icon('t/editstring', $str->edittitle, 'moodle', array('class' => 'iconsmall visibleifjs', 'title' => '')),
-            null,
-            array('class' => 'editing_title', 'title' => $str->edittitle)
+            $str->edittitle,
+            array('class' => 'editing_title', 'data-action' => 'edittitle')
         );
     }
 
-    // leftright
+    // Indent.
     if ($hasmanageactivities) {
         if (right_to_left()) {   // Exchange arrows on RTL
             $rightarrow = 't/left';
@@ -1822,86 +1822,90 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
             $leftarrow  = 't/left';
         }
 
+        $hiddenclass = 'hidden';
         if ($indent > 0) {
-            $actions['moveleft'] = new action_link(
-                new moodle_url($baseurl, array('id' => $mod->id, 'indent' => '-1')),
-                new pix_icon($leftarrow, $str->moveleft, 'moodle', array('class' => 'iconsmall', 'title' => '')),
-                null,
-                array('class' => 'editing_moveleft', 'title' => $str->moveleft)
-            );
+            $hiddenclass = '';
         }
+        $actions['moveleft'] = new action_menu_link_secondary(
+            new moodle_url($baseurl, array('id' => $mod->id, 'indent' => '-1')),
+            new pix_icon($leftarrow, $str->moveleft, 'moodle', array('class' => 'iconsmall', 'title' => '')),
+            $str->moveleft,
+            array('class' => 'editing_moveleft ' . $hiddenclass, 'data-action' => 'moveleft')
+        );
+        $hiddenclass = 'hidden';
         if ($indent >= 0) {
-            $actions['moveright'] = new action_link(
-                new moodle_url($baseurl, array('id' => $mod->id, 'indent' => '1')),
-                new pix_icon($rightarrow, $str->moveright, 'moodle', array('class' => 'iconsmall', 'title' => '')),
-                null,
-                array('class' => 'editing_moveright', 'title' => $str->moveright)
-            );
+            $hiddenclass = '';
         }
+        $actions['moveright'] = new action_menu_link_secondary(
+            new moodle_url($baseurl, array('id' => $mod->id, 'indent' => '1')),
+            new pix_icon($rightarrow, $str->moveright, 'moodle', array('class' => 'iconsmall', 'title' => '')),
+            $str->moveright,
+            array('class' => 'editing_moveright ' . $hiddenclass, 'data-action' => 'moveright')
+        );
     }
 
-    // move
+    // Move.
     if ($hasmanageactivities) {
-        $actions['move'] = new action_link(
+        $actions['move'] = new action_menu_link_primary(
             new moodle_url($baseurl, array('copy' => $mod->id)),
             new pix_icon('t/move', $str->move, 'moodle', array('class' => 'iconsmall', 'title' => '')),
-            null,
-            array('class' => 'editing_move', 'title' => $str->move)
+            $str->move,
+            array('class' => 'editing_move status', 'data-action' => 'move')
         );
     }
 
-    // Update
+    // Update.
     if ($hasmanageactivities) {
-        $actions['update'] = new action_link(
+        $actions['update'] = new action_menu_link_secondary(
             new moodle_url($baseurl, array('update' => $mod->id)),
             new pix_icon('t/edit', $str->update, 'moodle', array('class' => 'iconsmall', 'title' => '')),
-            null,
-            array('class' => 'editing_update', 'title' => $str->update)
+            $str->update,
+            array('class' => 'editing_update', 'data-action' => 'update')
         );
     }
 
     // Duplicate (require both target import caps to be able to duplicate and backup2 support, see modduplicate.php)
-    // note that restoring on front page is never allowed
+    // Note that restoring on front page is never allowed.
     if ($mod->course != SITEID && has_all_capabilities($dupecaps, $coursecontext) &&
             plugin_supports('mod', $mod->modname, FEATURE_BACKUP_MOODLE2)) {
-        $actions['duplicate'] = new action_link(
+        $actions['duplicate'] = new action_menu_link_secondary(
             new moodle_url($baseurl, array('duplicate' => $mod->id)),
             new pix_icon('t/copy', $str->duplicate, 'moodle', array('class' => 'iconsmall', 'title' => '')),
-            null,
-            array('class' => 'editing_duplicate', 'title' => $str->duplicate)
+            $str->duplicate,
+            array('class' => 'editing_duplicate', 'data-action' => 'duplicate')
         );
     }
 
-    // Delete
+    // Delete.
     if ($hasmanageactivities) {
-        $actions['delete'] = new action_link(
+        $actions['delete'] = new action_menu_link_secondary(
             new moodle_url($baseurl, array('delete' => $mod->id)),
             new pix_icon('t/delete', $str->delete, 'moodle', array('class' => 'iconsmall', 'title' => '')),
-            null,
-            array('class' => 'editing_delete', 'title' => $str->delete)
+            $str->delete,
+            array('class' => 'editing_delete', 'data-action' => 'delete')
         );
     }
 
-    // hideshow
+    // Hide/Show.
     if (has_capability('moodle/course:activityvisibility', $modcontext)) {
         if ($mod->visible) {
-            $actions['hide'] = new action_link(
+            $actions['hide'] = new action_menu_link_primary(
                 new moodle_url($baseurl, array('hide' => $mod->id)),
                 new pix_icon('t/hide', $str->hide, 'moodle', array('class' => 'iconsmall', 'title' => '')),
-                null,
-                array('class' => 'editing_hide', 'title' => $str->hide)
+                $str->hide,
+                array('class' => 'editing_hide', 'data-action' => 'hide')
             );
         } else {
-            $actions['show'] = new action_link(
+            $actions['show'] = new action_menu_link_primary(
                 new moodle_url($baseurl, array('show' => $mod->id)),
                 new pix_icon('t/show', $str->show, 'moodle', array('class' => 'iconsmall', 'title' => '')),
-                null,
-                array('class' => 'editing_show', 'title' => $str->show)
+                $str->show,
+                array('class' => 'editing_show', 'data-action' => 'show')
             );
         }
     }
 
-    // groupmode
+    // Groupmode.
     if ($hasmanageactivities and plugin_supports('mod', $mod->modname, FEATURE_GROUPS, 0)) {
         if ($mod->coursegroupmodeforce) {
             $modgroupmode = $mod->coursegroupmode;
@@ -1909,43 +1913,43 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
             $modgroupmode = $mod->groupmode;
         }
         if ($modgroupmode == SEPARATEGROUPS) {
-            $groupmode = NOGROUPS;
+            $nextgroupmode = VISIBLEGROUPS;
             $grouptitle = $str->groupsseparate;
             $forcedgrouptitle = $str->forcedgroupsseparate;
             $actionname = 'groupsseparate';
             $groupimage = 't/groups';
         } else if ($modgroupmode == VISIBLEGROUPS) {
-            $groupmode = SEPARATEGROUPS;
+            $nextgroupmode = NOGROUPS;
             $grouptitle = $str->groupsvisible;
             $forcedgrouptitle = $str->forcedgroupsvisible;
             $actionname = 'groupsvisible';
             $groupimage = 't/groupv';
         } else {
-            $groupmode = VISIBLEGROUPS;
+            $nextgroupmode = SEPARATEGROUPS;
             $grouptitle = $str->groupsnone;
             $forcedgrouptitle = $str->forcedgroupsnone;
             $actionname = 'groupsnone';
             $groupimage = 't/groupn';
         }
         if (!$mod->coursegroupmodeforce) {
-            $actions[$actionname] = new action_link(
-                new moodle_url($baseurl, array('id' => $mod->id, 'groupmode' => $groupmode)),
+            $actions[$actionname] = new action_menu_link_primary(
+                new moodle_url($baseurl, array('id' => $mod->id, 'groupmode' => $nextgroupmode)),
                 new pix_icon($groupimage, $grouptitle, 'moodle', array('class' => 'iconsmall', 'title' => '')),
-                null,
-                array('class' => 'editing_'. $actionname, 'title' => $grouptitle)
+                $grouptitle,
+                array('class' => 'editing_'. $actionname, 'data-action' => $actionname, 'data-nextgroupmode' => $nextgroupmode)
             );
         } else {
-            $actions[$actionname] = new pix_icon($groupimage, $forcedgrouptitle, 'moodle', array('title' => $forcedgrouptitle, 'class' => 'iconsmall'));
+            $actions[$actionname] = new pix_icon($groupimage, $forcedgrouptitle, 'moodle', array('title' => '', 'class' => 'iconsmall'));
         }
     }
 
-    // Assign
+    // Assign.
     if (has_capability('moodle/role:assign', $modcontext)){
-        $actions['assign'] = new action_link(
+        $actions['assign'] = new action_menu_link_secondary(
             new moodle_url('/admin/roles/assign.php', array('contextid' => $modcontext->id)),
             new pix_icon('t/assignroles', $str->assign, 'moodle', array('class' => 'iconsmall', 'title' => '')),
-            null,
-            array('class' => 'editing_assign', 'title' => $str->assign)
+            $str->assign,
+            array('class' => 'editing_assign', 'data-action' => 'assignroles')
         );
     }
 
index 884b14a..8e04b64 100644 (file)
@@ -325,20 +325,54 @@ class core_course_renderer extends plugin_renderer_base {
      *
      * @see course_get_cm_edit_actions()
      *
-     * @param array $actions array of action_link or pix_icon objects
+     * @param action_link[] $actions Array of action_link objects
+     * @param cm_info $mod The module we are displaying actions for.
+     * @param array $displayoptions additional display options:
+     *     ownerselector => A JS/CSS selector that can be used to find an cm node.
+     *         If specified the owning node will be given the class 'action-menu-shown' when the action
+     *         menu is being displayed.
+     *     constraintselector => A JS/CSS selector that can be used to find the parent node for which to constrain
+     *         the action menu to when it is being displayed.
+     *     donotenhance => If set to true the action menu that gets displayed won't be enhanced by JS.
      * @return string
      */
-    public function course_section_cm_edit_actions($actions) {
-        $output = html_writer::start_tag('span', array('class' => 'commands'));
+    public function course_section_cm_edit_actions($actions, cm_info $mod = null, $displayoptions = array()) {
+        global $CFG;
+
+        if (empty($actions)) {
+            return '';
+        }
+
+        if (isset($displayoptions['ownerselector'])) {
+            $ownerselector = $displayoptions['ownerselector'];
+        } else if ($mod) {
+            $ownerselector = '#module-'.$mod->id;
+        } else {
+            debugging('You should upgrade your call to '.__FUNCTION__.' and provide $mod', DEBUG_DEVELOPER);
+            $ownerselector = 'li.activity';
+        }
+
+        if (isset($displayoptions['constraintselector'])) {
+            $constraint = $displayoptions['constraintselector'];
+        } else {
+            $constraint = '.course-content';
+        }
+
+        $menu = new action_menu();
+        $menu->set_owner_selector($ownerselector);
+        $menu->set_contraint($constraint);
+        $menu->set_alignment(action_menu::TL, action_menu::TR);
+        if (isset($CFG->modeditingmenu) && !$CFG->modeditingmenu || !empty($displayoptions['donotenhance'])) {
+            $menu->do_not_enhance();
+        }
         foreach ($actions as $action) {
-            if ($action instanceof renderable) {
-                $output .= $this->output->render($action);
-            } else {
-                $output .= $action;
+            if ($action instanceof action_menu_link) {
+                $action->add_class('cm-edit-action');
             }
+            $menu->add($action);
         }
-        $output .= html_writer::end_tag('span');
-        return $output;
+        $menu->attributes['class'] .= ' section-cm-edit-actions commands';
+        return $this->render($menu);
     }
 
     /**
@@ -751,11 +785,20 @@ class core_course_renderer extends plugin_renderer_base {
             return $output;
         }
         $content = $mod->get_formatted_content(array('overflowdiv' => true, 'noclean' => true));
-        $conditionalhidden = $this->is_cm_conditionally_hidden($mod);
-        $accessiblebutdim = !$mod->visible || $conditionalhidden;
+        if ($this->page->user_is_editing()) {
+            // In editing mode, when an item is conditionally hidden from some users
+            // we show it as greyed out.
+            $conditionalhidden = $this->is_cm_conditionally_hidden($mod);
+            $dim = !$mod->visible || $conditionalhidden;
+        } else {
+            // When not in editing mode, we only show item as hidden if it is
+            // actually not available to the user
+            $conditionalhidden = false;
+            $dim = !$mod->uservisible;
+        }
         $textclasses = '';
         $accesstext = '';
-        if ($accessiblebutdim) {
+        if ($dim) {
             $textclasses .= ' dimmed_text';
             if ($conditionalhidden) {
                 $textclasses .= ' conditionalhidden';
@@ -895,7 +938,7 @@ class core_course_renderer extends plugin_renderer_base {
 
         if ($this->page->user_is_editing()) {
             $editactions = course_get_cm_edit_actions($mod, $mod->indent, $sectionreturn);
-            $output .= ' '. $this->course_section_cm_edit_actions($editactions);
+            $output .= ' '. $this->course_section_cm_edit_actions($editactions, $mod, $displayoptions);
             $output .= $mod->get_after_edit_icons();
         }
 
index 7cd0079..2937c17 100644 (file)
@@ -26,7 +26,7 @@ Feature: Indent items on the course page
     When I indent right "Test glossary name" activity
     Then "#section-1 li.glossary div.mod-indent-1" "css_element" should exists
     And I indent right "Test glossary name" activity
-    And "//li[@id='section-1']/descendant::li[contains(concat(' ', @class, ' '), ' glossary ')]/descendant::a[@title='Move left']" "xpath_element" should exists
+    And "//li[@id='section-1']/descendant::li[contains(concat(' ', @class, ' '), ' glossary ')]/descendant::a[not(contains(concat(' ', @class, ' '), ' hidden '))]/descendant::span[normalize-space(.)='Move left']" "xpath_element" should exists
     And "#section-1 li.glossary div.mod-indent-2" "css_element" should exists
     And I reload the page
     And "#section-1 li.glossary div.mod-indent-2" "css_element" should exists
@@ -34,4 +34,4 @@ Feature: Indent items on the course page
     And I indent left "Test glossary name" activity
     And "#section-1 li.glossary div.mod-indent-2" "css_element" should not exists
     And "#section-1 li.glossary div.mod-indent-1" "css_element" should not exists
-    And "//li[@id='section-1']/descendant::li[contains(concat(' ', @class, ' '), ' glossary ')]/descendant::a[@title='Move left']" "xpath_element" should not exists
+    And "//li[@id='section-1']/descendant::li[contains(concat(' ', @class, ' '), ' glossary ')]/descendant::a[not(contains(concat(' ', @class, ' '), ' hidden '))]/descendant::span[normalize-space(.)='Move left']" "xpath_element" should not exists
index 49145f2..35e9164 100644 (file)
@@ -485,8 +485,10 @@ class behat_course extends behat_base {
         }
 
         // Adding chr(10) to save changes.
+        $activity = $this->escape($activityname);
         return array(
-            new Given('I click on "' . get_string('edittitle') . '" "link" in the "' . $this->escape($activityname) .'" activity'),
+            new Given('I click on "' . get_string('actions', 'moodle') . '" "link" in the "' . $activity . '" activity'),
+            new Given('I click on "' . get_string('edittitle') . '" "link" in the "' . $activity .'" activity'),
             new Given('I fill in "title" with "' . $this->escape($newactivityname) . chr(10) . '"'),
             new Given('I wait "2" seconds')
         );
@@ -500,9 +502,12 @@ class behat_course extends behat_base {
      */
     public function i_indent_right_activity($activityname) {
 
-        $steps = array(
-            new Given('I click on "' . get_string('moveright') . '" "link" in the "' . $this->escape($activityname) . '" activity')
-        );
+        $steps = array();
+        $activity = $this->escape($activityname);
+        if ($this->running_javascript()) {
+            $steps[] = new Given('I click on "' . get_string('actions', 'moodle') . '" "link" in the "' . $activity . '" activity');
+        }
+        $steps[] = new Given('I click on "' . get_string('moveright') . '" "link" in the "' . $activity . '" activity');
 
         if ($this->running_javascript()) {
             $steps[] = new Given('I wait "2" seconds');
@@ -519,9 +524,12 @@ class behat_course extends behat_base {
      */
     public function i_indent_left_activity($activityname) {
 
-        $steps = array(
-            new Given('I click on "' . get_string('moveleft') . '" "link" in the "' . $this->escape($activityname) . '" activity')
-        );
+        $steps = array();
+        $activity = $this->escape($activityname);
+        if ($this->running_javascript()) {
+            $steps[] = new Given('I click on "' . get_string('actions', 'moodle') . '" "link" in the "' . $activity . '" activity');
+        }
+        $steps[] = new Given('I click on "' . get_string('moveleft') . '" "link" in the "' . $activity . '" activity');
 
         if ($this->running_javascript()) {
             $steps[] = new Given('I wait "2" seconds');
@@ -572,11 +580,15 @@ class behat_course extends behat_base {
      * @param string $activityname
      */
     public function i_duplicate_activity($activityname) {
-        return array(
-            new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $this->escape($activityname) . '" activity'),
-            new Given('I press "' . get_string('continue') .'"'),
-            new Given('I press "' . get_string('duplicatecontcourse') .'"')
-        );
+        $steps = array();
+        $activity = $this->escape($activityname);
+        if ($this->running_javascript()) {
+            $steps[] = new Given('I click on "' . get_string('actions', 'moodle') . '" "link" in the "' . $activity . '" activity');
+        }
+        $steps[] = new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $activity . '" activity');
+        $steps[] = new Given('I press "' . get_string('continue') .'"');
+        $steps[] = new Given('I press "' . get_string('duplicatecontcourse') .'"');
+        return $steps;
     }
 
     /**
@@ -587,13 +599,17 @@ class behat_course extends behat_base {
      * @param TableNode $data
      */
     public function i_duplicate_activity_editing_the_new_copy_with($activityname, TableNode $data) {
-        return array(
-            new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $this->escape($activityname) . '" activity'),
-            new Given('I press "' . get_string('continue') .'"'),
-            new Given('I press "' . get_string('duplicatecontedit') . '"'),
-            new Given('I fill the moodle form with:', $data),
-            new Given('I press "' . get_string('savechangesandreturntocourse') . '"')
-        );
+        $steps = array();
+        $activity = $this->escape($activityname);
+        if ($this->running_javascript()) {
+            $steps[] = new Given('I click on "' . get_string('actions', 'moodle') . '" "link" in the "' . $activity . '" activity');
+        }
+        $steps[] = new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $activity . '" activity');
+        $steps[] = new Given('I press "' . get_string('continue') .'"');
+        $steps[] = new Given('I press "' . get_string('duplicatecontedit') . '"');
+        $steps[] = new Given('I fill the moodle form with:', $data);
+        $steps[] = new Given('I press "' . get_string('savechangesandreturntocourse') . '"');
+        return $steps;
     }
 
     /**
index 973c287..8b2ef82 100644 (file)
@@ -40,6 +40,7 @@ Feature: Course activity controls works as expected
     And I should see "Turn editing on"
     And "Turn editing on" "button" should exists
     And I turn editing mode on
+    And I click on "Actions" "link" in the ".block_recent_activity" "css_element"
     And I click on "Delete Recent activity block" "link"
     And I press "Yes"
     And "#section-2" "css_element" <should_see_other_sections> exists
@@ -54,11 +55,13 @@ Feature: Course activity controls works as expected
     And "#section-2" "css_element" <should_see_other_sections> exists
     And I indent left "Test forum name 1" activity
     And "#section-2" "css_element" <should_see_other_sections> exists
+    And I click on "Actions" "link" in the "Test forum name 1" activity
     And I click on "Update" "link" in the "Test forum name 1" activity
     And I should see "Updating Forum"
     And I should see "Display description on course page"
     And I press "Save and return to course"
     And "#section-2" "css_element" <should_see_other_sections> exists
+    And I click on "Actions" "link" in the "Test forum name 1" activity
     And I click on "Hide" "link" in the "Test forum name 1" activity
     And "#section-2" "css_element" <should_see_other_sections> exists
     And I duplicate "Test forum name 2" activity editing the new copy with:
@@ -111,6 +114,7 @@ Feature: Course activity controls works as expected
     And I should see "Turn editing on"
     And "Turn editing on" "button" should exists
     And I turn editing mode on
+    And I click on "Actions" "link" in the ".block_recent_activity" "css_element"
     And I click on "Delete Recent activity block" "link"
     And I press "Yes"
     And "#section-2" "css_element" <should_see_other_sections> exists
@@ -125,11 +129,13 @@ Feature: Course activity controls works as expected
     And "#section-2" "css_element" <should_see_other_sections> exists
     And I indent left "Test forum name 1" activity
     And "#section-2" "css_element" <should_see_other_sections> exists
+    And I click on "Actions" "link" in the "Test forum name 1" activity
     And I click on "Update" "link" in the "Test forum name 1" activity
     And I should see "Updating Forum"
     And I should see "Display description on course page"
     And I press "Save and return to course"
     And "#section-2" "css_element" <should_see_other_sections> exists
+    And I click on "Actions" "link" in the "Test forum name 1" activity
     And I click on "Hide" "link" in the "Test forum name 1" activity
     And "#section-2" "css_element" <should_see_other_sections> exists
     And I delete "Test forum name 1" activity
index 6872ece..0145f0f 100644 (file)
@@ -2,7 +2,7 @@ YUI.add('moodle-course-dragdrop', function(Y) {
 
     var CSS = {
         ACTIVITY : 'activity',
-        COMMANDSPAN : 'span.commands',
+        COMMANDSPAN : '.commands',
         CONTENT : 'content',
         COURSECONTENT : 'course-content',
         EDITINGMOVE : 'editing_move',
index 0955095..2b160b8 100644 (file)
@@ -1,43 +1,47 @@
 YUI.add('moodle-course-toolboxes', function(Y) {
-    WAITICON = {'pix':"i/loading_small",'component':'moodle'};
-    // The CSS selectors we use
+
+    // The following properties contain common strings.
+    // We separate them out here because when this JS is minified the content is less as
+    // Variables get compacted to single/double characters and the full length of the string
+    // exists only once.
+
+    // The CSS classes we use.
     var CSS = {
-        ACTIVITYLI : 'li.activity',
-        COMMANDSPAN : 'span.commands',
-        SPINNERCOMMANDSPAN : 'span.commands',
-        CONTENTAFTERLINK : 'div.contentafterlink',
-        DELETE : 'a.editing_delete',
+        ACTIVITYINSTANCE : 'activityinstance',
+        AVAILABILITYINFODIV : 'div.availabilityinfo',
+        CONDITIONALHIDDEN : 'conditionalhidden',
         DIMCLASS : 'dimmed',
         DIMMEDTEXT : 'dimmed_text',
-        EDITTITLE : 'a.editing_title',
-        EDITTITLECLASS : 'edittitle',
-        GENERICICONCLASS : 'iconsmall',
-        GROUPSNONE : 'a.editing_groupsnone',
-        GROUPSSEPARATE : 'a.editing_groupsseparate',
-        GROUPSVISIBLE : 'a.editing_groupsvisible',
-        HIDE : 'a.editing_hide',
-        HIGHLIGHT : 'a.editing_highlight',
-        INSTANCENAME : 'span.instancename',
-        LIGHTBOX : 'lightbox',
+        EDITINSTRUCTIONS : 'editinstructions',
+        HIDE : 'hide',
         MODINDENTCOUNT : 'mod-indent-',
-        MODINDENTDIV : 'div.mod-indent',
         MODINDENTHUGE : 'mod-indent-huge',
         MODULEIDPREFIX : 'module-',
-        MOVELEFT : 'a.editing_moveleft',
-        MOVELEFTCLASS : 'editing_moveleft',
-        MOVERIGHT : 'a.editing_moveright',
-        PAGECONTENT : 'div#page-content',
-        RIGHTSIDE : '.right',
         SECTIONHIDDENCLASS : 'hidden',
         SECTIONIDPREFIX : 'section-',
+        SHOW : 'editing_show',
+        TITLEEDITOR : 'titleeditor'
+    },
+    // The CSS selectors we use.
+    SELECTOR = {
+        ACTIONLINKTEXT : '.actionlinktext',
+        ACTIVITYACTION : 'a.cm-edit-action[data-action]',
+        ACTIVITYFORM : 'form.'+CSS.ACTIVITYINSTANCE,
+        ACTIVITYICON : 'img.activityicon',
+        ACTIVITYLI : 'li.activity',
+        ACTIVITYTITLE : 'input[name=title]',
+        COMMANDSPAN : '.commands',
+        CONTENTAFTERLINK : 'div.contentafterlink',
+        HIDE : 'a.editing_hide',
+        HIGHLIGHT : 'a.editing_highlight',
+        INSTANCENAME : 'span.instancename',
+        MODINDENTDIV : 'div.mod-indent',
+        PAGECONTENT : 'div#page-content',
         SECTIONLI : 'li.section',
-        SHOW : 'a.editing_show',
-        SHOWHIDE : 'a.editing_showhide',
-        CONDITIONALHIDDEN : 'conditionalhidden',
-        AVAILABILITYINFODIV : 'div.availabilityinfo',
-        SHOWCLASS : 'editing_show',
-        HIDECLASS : 'hide'
-    };
+        SHOW : 'a.'+CSS.SHOW,
+        SHOWHIDE : 'a.editing_showhide'
+    },
+    BODY = Y.one(document.body);
 
     /**
      * The toolbox classes
@@ -51,57 +55,6 @@ YUI.add('moodle-course-toolboxes', function(Y) {
     }
 
     Y.extend(TOOLBOX, Y.Base, {
-        /**
-         * Toggle the visibility and availability for the specified
-         * resource show/hide button
-         */
-        toggle_hide_resource_ui : function(button) {
-            var element = button.ancestor(CSS.ACTIVITYLI);
-            var hideicon = button.one('img');
-
-            var dimarea;
-            var toggle_class;
-            if (this.get_instance_name(element) == null) {
-                toggle_class = CSS.DIMMEDTEXT;
-                dimarea = element.all(CSS.MODINDENTDIV + ' > div').item(1);
-            } else {
-                toggle_class = CSS.DIMCLASS;
-                dimarea = element.one('a');
-            }
-
-            var status = '';
-            var value;
-            if (button.hasClass(CSS.SHOWCLASS)) {
-                status = 'hide';
-                value = 1;
-            } else {
-                status = 'show';
-                value = 0;
-            }
-            // Update button info.
-            var newstring = M.util.get_string(status, 'moodle');
-            hideicon.setAttrs({
-                'alt' : newstring,
-                'src'   : M.util.image_url('t/' + status)
-            });
-            button.set('title', newstring);
-            button.set('className', 'editing_'+status);
-
-            // If activity is conditionally hidden, then don't toggle.
-            if (!dimarea.hasClass(CSS.CONDITIONALHIDDEN)) {
-                // Change the UI.
-                dimarea.toggleClass(toggle_class);
-                // We need to toggle dimming on the description too.
-                element.all(CSS.CONTENTAFTERLINK).toggleClass(CSS.DIMMEDTEXT);
-            }
-            // Toggle availablity info for conditional activities.
-            var availabilityinfo = element.one(CSS.AVAILABILITYINFODIV);
-
-            if (availabilityinfo) {
-                availabilityinfo.toggleClass(CSS.HIDECLASS);
-            }
-            return value;
-        },
         /**
          * Send a request using the REST API
          *
@@ -180,8 +133,8 @@ YUI.add('moodle-course-toolboxes', function(Y) {
          * @return string|null Instance name
          */
         get_instance_name : function(target) {
-            if (target.one(CSS.INSTANCENAME)) {
-                return target.one(CSS.INSTANCENAME).get('firstChild').get('data');
+            if (target.one(SELECTOR.INSTANCENAME)) {
+                return target.one(SELECTOR.INSTANCENAME).get('firstChild').get('data');
             }
             return null;
         },
@@ -221,81 +174,158 @@ YUI.add('moodle-course-toolboxes', function(Y) {
     }
     );
 
-
+    /**
+     * Resource and activity toolbox class.
+     *
+     * This class is responsible for managing AJAX interactions with activities and resources
+     * when viewing a course in editing mode.
+     *
+     * @namespace M.course.toolbox
+     * @class ResourceToolbox
+     * @constructor
+     */
     var RESOURCETOOLBOX = function() {
         RESOURCETOOLBOX.superclass.constructor.apply(this, arguments);
     }
 
     Y.extend(RESOURCETOOLBOX, TOOLBOX, {
-        // Variables
+        /**
+         * No groups are being used.
+         * @static
+         * @const GROUPS_NONE
+         * @type Number
+         */
         GROUPS_NONE     : 0,
+        /**
+         * Separate groups are being used.
+         * @static
+         * @const GROUPS_SEPARATE
+         * @type Number
+         */
         GROUPS_SEPARATE : 1,
+        /**
+         * Visible groups are being used.
+         * @static
+         * @const GROUPS_VISIBLE
+         * @type Number
+         */
         GROUPS_VISIBLE  : 2,
 
+        /**
+         * Events that were added when editing a title.
+         * These should all be detached when editing is complete.
+         * @property edittitleevents
+         * @type {Event[]}
+         * @protected
+         */
+        edittitleevents : [],
+
         /**
          * Initialize the resource toolbox
          *
-         * Updates all span.commands with relevant handlers and other required changes
+         * For each activity the commands are updated and a reference to the activity is attached.
+         * This way it doesn't matter where the commands are going to called from they have a reference to the
+         * activity that they relate to.
+         * This is essential as some of the actions are displayed in an actionmenu which removes them from the
+         * page flow.
+         *
+         * This function also creates a single event delegate to manage all AJAX actions for all activities on
+         * the page.
+         *
+         * @method initializer
          */
         initializer : function(config) {
-            this.setup_for_resource();
             M.course.coursebase.register_module(this);
-
-            var prefix = CSS.ACTIVITYLI + ' ' + CSS.COMMANDSPAN + ' ';
-            Y.delegate('click', this.edit_resource_title, CSS.PAGECONTENT, prefix + CSS.EDITTITLE, this);
-            Y.delegate('click', this.move_left, CSS.PAGECONTENT, prefix + CSS.MOVELEFT, this);
-            Y.delegate('click', this.move_right, CSS.PAGECONTENT, prefix + CSS.MOVERIGHT, this);
-            Y.delegate('click', this.delete_resource, CSS.PAGECONTENT, prefix + CSS.DELETE, this);
-            Y.delegate('click', this.toggle_hide_resource, CSS.PAGECONTENT, prefix + CSS.HIDE, this);
-            Y.delegate('click', this.toggle_hide_resource, CSS.PAGECONTENT, prefix + CSS.SHOW, this);
-            Y.delegate('click', this.toggle_groupmode, CSS.PAGECONTENT, prefix + CSS.GROUPSNONE, this);
-            Y.delegate('click', this.toggle_groupmode, CSS.PAGECONTENT, prefix + CSS.GROUPSSEPARATE, this);
-            Y.delegate('click', this.toggle_groupmode, CSS.PAGECONTENT, prefix + CSS.GROUPSVISIBLE, this);
+            Y.all(SELECTOR.ACTIVITYLI).each(function(activity){
+                activity.setData('toolbox', this);
+                activity.all(SELECTOR.COMMANDSPAN+ ' ' + SELECTOR.ACTIVITYACTION).each(function(){
+                    this.setData('activity', activity);
+                });
+            }, this);
+            Y.delegate('click', this.handle_data_action, BODY, SELECTOR.ACTIVITYACTION, this);
         },
 
         /**
-         * Update any span.commands within the scope of the specified
-         * selector with AJAX equivelants
+         * Handles the delegation event. When this is fired someone has triggered an action.
          *
-         * @param baseselector The selector to limit scope to
-         * @return void
+         * Note not all actions will result in an AJAX enhancement.
+         *
+         * @protected
+         * @method handle_data_action
+         * @param {EventFacade} ev The event that was triggered.
+         * @returns {boolean}
          */
-        setup_for_resource : function(baseselector) {
-            if (!baseselector) {
-                var baseselector = CSS.PAGECONTENT + ' ' + CSS.ACTIVITYLI;
+        handle_data_action : function(ev) {
+            // We need to get the anchor element that triggered this event.
+            var node = ev.target;
+            if (!node.test('a')) {
+                node = node.ancestor(SELECTOR.ACTIVITYACTION);
             }
 
-            Y.all(baseselector).each(this._setup_for_resource, this);
-        },
-        _setup_for_resource : function(toolboxtarget) {
-            toolboxtarget = Y.one(toolboxtarget);
-
-            // Set groupmode attribute for use by this.toggle_groupmode()
-            var groups;
-            groups = toolboxtarget.all(CSS.COMMANDSPAN + ' ' + CSS.GROUPSNONE);
-            groups.setAttribute('groupmode', this.GROUPS_NONE);
-
-            groups = toolboxtarget.all(CSS.COMMANDSPAN + ' ' + CSS.GROUPSSEPARATE);
-            groups.setAttribute('groupmode', this.GROUPS_SEPARATE);
+            // From the anchor we can get both the activity (added during initialisation) and the action being
+            // performed (added by the UI as a data attribute).
+            var action = node.getData('action'),
+                activity = node.getData('activity');
+            if (!node.test('a') || !action || !activity) {
+                // It wasn't a valid action node.
+                return;
+            }
 
-            groups = toolboxtarget.all(CSS.COMMANDSPAN + ' ' + CSS.GROUPSVISIBLE);
-            groups.setAttribute('groupmode', this.GROUPS_VISIBLE);
-        },
-        move_left : function(e) {
-            this.move_leftright(e, -1);
-        },
-        move_right : function(e) {
-            this.move_leftright(e, 1);
+            // Switch based upon the action and do the desired thing.
+            switch (action) {
+                case 'edittitle' :
+                    // The user wishes to edit the title of the event.
+                    this.edit_title(ev, node, activity, action);
+                    break;
+                case 'moveleft' :
+                case 'moveright' :
+                    // The user changing the indent of the activity.
+                    this.change_indent(ev, node, activity, action);
+                    break;
+                case 'delete' :
+                    // The user is deleting the activity.
+                    this.delete_with_confirmation(ev, node, activity, action);
+                    break;
+                case 'hide' :
+                case 'show' :
+                    // The user is changing the visibility of the activity.
+                    this.change_visibility(ev, node, activity, action);
+                    break;
+                case 'groupsseparate' :
+                case 'groupsvisible' :
+                case 'groupsnone' :
+                    // The user is changing the group mode.
+                    callback = 'change_groupmode';
+                    this.change_groupmode(ev, node, activity, action);
+                    break;
+                case 'move' :
+                case 'update' :
+                case 'duplicate' :
+                case 'assignroles' :
+                default:
+                    // Nothing to do here!
+                    break;
+            }
         },
-        move_leftright : function(e, direction) {
+
+        /**
+         * Change the indent of the activity or resource.
+         *
+         * @protected
+         * @method change_indent
+         * @param {EventFacade} ev The event that was fired.
+         * @param {Node} button The button that triggered this action.
+         * @param {Node} activity The activity node that this action will be performed on.
+         * @param {String} action The action that has been requested. Will be 'moveleft' or 'moveright'.
+         */
+        change_indent : function(ev, button, activity, action) {
             // Prevent the default button action
-            e.preventDefault();
+            ev.preventDefault();
 
-            // Get the element we're working on
-            var element = e.target.ancestor(CSS.ACTIVITYLI);
+            var direction = (action === 'moveleft') ? -1 : 1;
 
             // And we need to determine the current and new indent level
-            var indentdiv = element.one(CSS.MODINDENTDIV);
+            var indentdiv = activity.one(SELECTOR.MODINDENTDIV);
             var indent = indentdiv.getAttribute('class').match(/mod-indent-(\d{1,})/);
 
             if (indent) {
@@ -313,16 +343,25 @@ YUI.add('moodle-course-toolboxes', function(Y) {
                 'class' : 'resource',
                 'field' : 'indent',
                 'value' : newindent,
-                'id'    : this.get_element_id(element)
+                'id'    : this.get_element_id(activity)
             };
-            var spinner = M.util.add_spinner(Y, element.one(CSS.SPINNERCOMMANDSPAN));
+            var commands = activity.one(SELECTOR.COMMANDSPAN);
+            var spinner = M.util.add_spinner(Y, commands).setStyles({
+                position: 'absolute',
+                top: 0
+            });
+            if (BODY.hasClass('dir-ltr')) {
+                spinner.setStyle('left', '100%');
+            }  else {
+                spinner.setStyle('right', '100%');
+            }
             this.send_request(data, spinner);
 
-            // Handle removal/addition of the moveleft button
+            // Handle removal/addition of the moveleft button.
             if (newindent == 0) {
-                element.one(CSS.MOVELEFT).remove();
+                button.addClass('hidden');
             } else if (newindent == 1 && oldindent == 0) {
-                this.add_moveleft(element);
+                button.ancestor('.menu').one('[data-action=moveleft]').removeClass('hidden');
             }
 
             // Handle massive indentation to match non-ajax display
@@ -333,12 +372,23 @@ YUI.add('moodle-course-toolboxes', function(Y) {
                 indentdiv.removeClass(CSS.MODINDENTHUGE);
             }
         },
-        delete_resource : function(e) {
+
+        /**
+         * Deletes the given activity or resource after confirmation.
+         *
+         * @protected
+         * @method delete_with_confirmation
+         * @param {EventFacade} ev The event that was fired.
+         * @param {Node} button The button that triggered this action.
+         * @param {Node} activity The activity node that this action will be performed on.
+         * @return Boolean
+         */
+        delete_with_confirmation : function(ev, button, activity) {
             // Prevent the default button action
-            e.preventDefault();
+            ev.preventDefault();
 
             // Get the element we're working on
-            var element   = e.target.ancestor(CSS.ACTIVITYLI);
+            var element   = activity
 
             // Create confirm string (different if element has or does not have name)
             var confirmstring = '';
@@ -365,23 +415,35 @@ YUI.add('moodle-course-toolboxes', function(Y) {
                 'id'    : this.get_element_id(element)
             };
             this.send_request(data);
+            if (M.core.actionmenu && M.core.actionmenu.instance) {
+                M.core.actionmenu.instance.hideMenu();
+            }
         },
-        toggle_hide_resource : function(e) {
+
+        /**
+         * Changes the visibility of this activity or resource.
+         *
+         * @protected
+         * @method change_visibility
+         * @param {EventFacade} ev The event that was fired.
+         * @param {Node} button The button that triggered this action.
+         * @param {Node} activity The activity node that this action will be performed on.
+         * @param {String} action The action that has been requested.
+         * @return Boolean
+         */
+        change_visibility : function(ev, button, activity, action) {
             // Prevent the default button action
-            e.preventDefault();
+            ev.preventDefault();
 
             // Return early if the current section is hidden
-            var section = e.target.ancestor(M.course.format.get_section_selector(Y));
+            var section = activity.ancestor(M.course.format.get_section_selector(Y));
             if (section && section.hasClass(CSS.SECTIONHIDDENCLASS)) {
                 return;
             }
 
             // Get the element we're working on
-            var element = e.target.ancestor(CSS.ACTIVITYLI);
-
-            var button = e.target.ancestor('a', true);
-
-            var value = this.toggle_hide_resource_ui(button);
+            var element = activity;
+            var value = this.handle_resource_dim(button, activity, action);
 
             // Send the request
             var data = {
@@ -390,238 +452,276 @@ YUI.add('moodle-course-toolboxes', function(Y) {
                 'value' : value,
                 'id'    : this.get_element_id(element)
             };
-            var spinner = M.util.add_spinner(Y, element.one(CSS.SPINNERCOMMANDSPAN));
+            var spinner = M.util.add_spinner(Y, element.one(SELECTOR.COMMANDSPAN));
             this.send_request(data, spinner);
             return false; // Need to return false to stop the delegate for the new state firing
         },
-        toggle_groupmode : function(e) {
-            // Prevent the default button action
-            e.preventDefault();
 
-            // Get the element we're working on
-            var element = e.target.ancestor(CSS.ACTIVITYLI);
+        /**
+         * Handles the UI aspect of dimming the activity or resource.
+         *
+         * @protected
+         * @method handle_resource_dim
+         * @param {Node} button The button that triggered the action.
+         * @param {Node} activity The activity node that this action will be performed on.
+         * @param {String} status Whether the activity was shown or hidden.
+         * @returns {number} 1 if we were changing to visible, 0 if we were hiding.
+         */
+        handle_resource_dim : function(button, activity, status) {
+            var toggleclass = CSS.DIMCLASS,
+                dimarea = activity.one('a'),
+                availabilityinfo = activity.one(CSS.AVAILABILITYINFODIV),
+                newstatus = (status === 'hide') ? 'show' : 'hide',
+                newstring = M.util.get_string(newstatus, 'moodle');
 
-            var button = e.target.ancestor('a', true);
-            var icon = button.one('img');
+            // Update button info.
+            button.one('img').setAttrs({
+                'alt' : newstring,
+                'src'   : M.util.image_url('t/' + newstatus)
+            });
+            button.set('title', newstring);
+            button.replaceClass('editing_'+status, 'editing_'+newstatus)
+            button.setData('action', newstatus);
+
+            // If activity is conditionally hidden, then don't toggle.
+            if (this.get_instance_name(activity) == null) {
+                toggleclass = CSS.DIMMEDTEXT;
+                dimarea = activity.all(SELECTOR.MODINDENTDIV + ' > div').item(1);
+            }
+            if (!dimarea.hasClass(CSS.CONDITIONALHIDDEN)) {
+                // Change the UI.
+                dimarea.toggleClass(toggleclass);
+                // We need to toggle dimming on the description too.
+                activity.all(SELECTOR.CONTENTAFTERLINK).toggleClass(CSS.DIMMEDTEXT);
+            }
+            // Toggle availablity info for conditional activities.
+            if (availabilityinfo) {
+                availabilityinfo.toggleClass(CSS.HIDE);
+            }
+            return (status === 'hide') ? 0 : 1;
+        },
+
+        /**
+         * Changes the groupmode of the activity to the next groupmode in the sequence.
+         *
+         * @protected
+         * @method change_groupmode
+         * @param {EventFacade} ev The event that was fired.
+         * @param {Node} button The button that triggered this action.
+         * @param {Node} activity The activity node that this action will be performed on.
+         * @param {String} action The action that has been requested.
+         * @return Boolean
+         */
+        change_groupmode : function(ev, button, activity, action) {
+            // Prevent the default button action.
+            ev.preventDefault();
 
             // Current Mode
-            var groupmode = button.getAttribute('groupmode');
-            groupmode++;
-            if (groupmode > 2) {
-                groupmode = 0;
-            }
-            button.setAttribute('groupmode', groupmode);
-
-            var newtitle = '';
-            var iconsrc = '';
-            switch (groupmode) {
-                case this.GROUPS_NONE:
-                    newtitle = 'groupsnone';
-                    iconsrc = M.util.image_url('t/groupn');
-                    break;
-                case this.GROUPS_SEPARATE:
-                    newtitle = 'groupsseparate';
-                    iconsrc = M.util.image_url('t/groups');
-                    break;
-                case this.GROUPS_VISIBLE:
-                    newtitle = 'groupsvisible';
-                    iconsrc = M.util.image_url('t/groupv');
-                    break;
+            var groupmode = parseInt(button.getData('nextgroupmode'), 10),
+                newtitle = '',
+                iconsrc = '',
+                newtitlestr,
+                data,
+                spinner,
+                nextgroupmode = groupmode + 1;
+
+            if (nextgroupmode > 2) {
+                nextgroupmode = 0;
             }
-            newtitle = M.util.get_string('clicktochangeinbrackets', 'moodle',
-                    M.util.get_string(newtitle, 'moodle'));
+
+            if (groupmode === this.GROUPS_NONE) {
+                newtitle = 'groupsnone';
+                iconsrc = M.util.image_url('t/groupn', 'moodle');
+            } else if (groupmode === this.GROUPS_SEPARATE) {
+                newtitle = 'groupsseparate';
+                iconsrc = M.util.image_url('t/groups', 'moodle');
+            } else if (groupmode === this.GROUPS_VISIBLE) {
+                newtitle = 'groupsvisible';
+                iconsrc = M.util.image_url('t/groupv', 'moodle');
+            }
+            newtitlestr = M.util.get_string(newtitle, 'moodle'),
+            newtitlestr = M.util.get_string('clicktochangeinbrackets', 'moodle', newtitlestr);
 
             // Change the UI
-            icon.setAttrs({
-                'alt' : newtitle,
+            button.one('img').setAttrs({
+                'alt' : newtitlestr,
                 'src' : iconsrc
             });
-            button.setAttribute('title', newtitle);
+            button.setAttribute('title', newtitlestr).setData('action', newtitle).setData('nextgroupmode', nextgroupmode);
 
             // And send the request
-            var data = {
+            data = {
                 'class' : 'resource',
                 'field' : 'groupmode',
                 'value' : groupmode,
-                'id'    : this.get_element_id(element)
+                'id'    : this.get_element_id(activity)
             };
-            var spinner = M.util.add_spinner(Y, element.one(CSS.SPINNERCOMMANDSPAN));
+
+            spinner = M.util.add_spinner(Y, activity.one(SELECTOR.COMMANDSPAN));
             this.send_request(data, spinner);
             return false; // Need to return false to stop the delegate for the new state firing
         },
-        /**
-         * Add the moveleft button
-         * This is required after moving left from an initial position of 0
-         *
-         * @param target The encapsulating <li> element
-         */
-        add_moveleft : function(target) {
-            var left_string = M.util.get_string('moveleft', 'moodle');
-            var moveimage = 't/left'; // ltr mode
-            if ( Y.one(document.body).hasClass('dir-rtl') ) {
-                moveimage = 't/right';
-            } else {
-                moveimage = 't/left';
-            }
-            var newicon = Y.Node.create('<img />')
-                .addClass(CSS.GENERICICONCLASS)
-                .setAttrs({
-                    'src'   : M.util.image_url(moveimage, 'moodle'),
-                    'alt'   : left_string
-                });
-            var moveright = target.one(CSS.MOVERIGHT);
-            var newlink = moveright.getAttribute('href').replace('indent=1', 'indent=-1');
-            var anchor = new Y.Node.create('<a />')
-                .setStyle('cursor', 'pointer')
-                .addClass(CSS.MOVELEFTCLASS)
-                .setAttribute('href', newlink)
-                .setAttribute('title', left_string);
-            anchor.appendChild(newicon);
-            moveright.insert(anchor, 'before');
-        },
+
         /**
          * Edit the title for the resource
+         *
+         * @protected
+         * @method edit_title
+         * @param {EventFacade} ev The event that was fired.
+         * @param {Node} button The button that triggered this action.
+         * @param {Node} activity The activity node that this action will be performed on.
+         * @param {String} action The action that has been requested.
+         * @return Boolean
          */
-        edit_resource_title : function(e) {
+        edit_title : function(ev, button, activity) {
             // Get the element we're working on
-            var element = e.target.ancestor(CSS.ACTIVITYLI);
-            var elementdiv = element.one('div');
-            var instancename  = element.one(CSS.INSTANCENAME);
-            var currenttitle = instancename.get('firstChild');
-            var oldtitle = currenttitle.get('data');
-            var titletext = oldtitle;
-            var editbutton = element.one('a.' + CSS.EDITTITLECLASS + ' img');
-
-            // Handle events for edit_resource_title
-            var listenevents = [];
-            var thisevent;
-
-            // Grab the anchor so that we can swap it with the edit form
-            var anchor = instancename.ancestor('a');
+            var activityid = this.get_element_id(activity),
+                instancename  = activity.one(SELECTOR.INSTANCENAME),
+                currenttitle = instancename.get('firstChild'),
+                oldtitle = currenttitle.get('data'),
+                titletext = oldtitle,
+                thisevent,
+                anchor = instancename.ancestor('a'),// Grab the anchor so that we can swap it with the edit form.
+                data = {
+                    'class'   : 'resource',
+                    'field'   : 'gettitle',
+                    'id'      : activityid
+                },
+                response = this.send_request(data);
 
-            var data = {
-                'class'   : 'resource',
-                'field'   : 'gettitle',
-                'id'      : this.get_element_id(element)
-            };
+            if (M.core.actionmenu && M.core.actionmenu.instance) {
+                M.core.actionmenu.instance.hideMenu();
+            }
 
             // Try to retrieve the existing string from the server
-            var response = this.send_request(data, editbutton);
             if (response.instancename) {
                 titletext = response.instancename;
             }
 
             // Create the editor and submit button
-            var editor = Y.Node.create('<input />')
-                .setAttrs({
-                    'name'  : 'title',
-                    'value' : titletext,
-                    'autocomplete' : 'off',
-                    'aria-describedby' : 'id_editinstructions',
-                    'maxLength' : '255'
-                })
-                .addClass('titleeditor');
-            var editform = Y.Node.create('<form />')
-                .addClass('activityinstance')
-                .setAttribute('action', '#');
-            var editinstructions = Y.Node.create('<span />')
-                .addClass('editinstructions')
-                .setAttrs({'id' : 'id_editinstructions'})
+            var editform = Y.Node.create('<form class="'+CSS.ACTIVITYINSTANCE+'" action="#" />');
+            var editinstructions = Y.Node.create('<span class="'+CSS.EDITINSTRUCTIONS+'" id="id_editinstructions" />')
                 .set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle'));
-            var activityicon = element.one('img.activityicon').cloneNode();
+            var editor = Y.Node.create('<input name="title" type="text" class="'+CSS.TITLEEDITOR+'" />').setAttrs({
+                'value' : titletext,
+                'autocomplete' : 'off',
+                'aria-describedby' : 'id_editinstructions',
+                'maxLength' : '255'
+            })
 
             // Clear the existing content and put the editor in
-            currenttitle.set('data', '');
-            editform.appendChild(activityicon);
+            editform.appendChild(activity.one(SELECTOR.ACTIVITYICON).cloneNode());
             editform.appendChild(editor);
+            editform.setData('anchor', anchor);
             anchor.replace(editform);
-            elementdiv.appendChild(editinstructions);
-            e.preventDefault();
+            activity.one('div').appendChild(editinstructions);
+            ev.preventDefault();
 
             // Focus and select the editor text
             editor.focus().select();
 
-            // Handle removal of the editor
-            var clear_edittitle = function() {
-                // Detach all listen events to prevent duplicate triggers
-                var thisevent;
-                while (thisevent = listenevents.shift()) {
-                    thisevent.detach();
-                }
+            // Cancel the edit if we lose focus or the escape key is pressed.
+            thisevent = editor.on('blur', this.edit_title_cancel, this, activity, false);
+            this.edittitleevents.push(thisevent);
+            thisevent = editor.on('key', this.edit_title_cancel, 'esc', this, activity, true);
+            this.edittitleevents.push(thisevent);
 
-                if (editinstructions) {
-                    // Convert back to anchor and remove instructions
-                    editform.replace(anchor);
-                    editinstructions.remove();
-                    editinstructions = null;
+            // Handle form submission.
+            thisevent = editform.on('submit', this.edit_title_submit, this, activity, oldtitle);
+            this.edittitleevents.push(thisevent);
+        },
+
+        /**
+         * Handles the submit event when editing the activity or resources title.
+         *
+         * @protected
+         * @method edit_title_submit
+         * @param {EventFacade} ev The event that triggered this.
+         * @param {Node} activity The activity whose title we are altering.
+         * @param {String} originaltitle The original title the activity or resource had.
+         */
+        edit_title_submit : function(ev, activity, originaltitle) {
+            // We don't actually want to submit anything
+            ev.preventDefault();
+
+            var newtitle = Y.Lang.trim(activity.one(SELECTOR.ACTIVITYFORM + ' ' + SELECTOR.ACTIVITYTITLE).get('value'));
+            this.edit_title_clear(activity);
+            var spinner = M.util.add_spinner(Y, activity.one(SELECTOR.INSTANCENAME));
+            if (newtitle != null && newtitle != "" && newtitle != originaltitle) {
+                var data = {
+                    'class'   : 'resource',
+                    'field'   : 'updatetitle',
+                    'title'   : newtitle,
+                    'id'      : this.get_element_id(activity)
+                };
+                var response = this.send_request(data, spinner);
+                if (response.instancename) {
+                    activity.one(SELECTOR.INSTANCENAME).setContent(response.instancename);
                 }
             }
+        },
 
-            // Handle cancellation of the editor
-            var cancel_edittitle = function(e) {
-                clear_edittitle();
-
-                // Set the title and anchor back to their previous settings
-                currenttitle.set('data', oldtitle);
-            };
+        /**
+         * Handles the cancel event when editing the activity or resources title.
+         *
+         * @protected
+         * @method edit_title_cancel
+         * @param {EventFacade} ev The event that triggered this.
+         * @param {Node} activity The activity whose title we are altering.
+         * @param {Boolean} preventdefault If true we should prevent the default action from occuring.
+         */
+        edit_title_cancel : function(ev, activity, preventdefault) {
+            if (preventdefault) {
+                ev.preventDefault();
+            }
+            this.edit_title_clear(activity);
+        },
 
-            // Cancel the edit if we lose focus or the escape key is pressed
-            thisevent = editor.on('blur', cancel_edittitle);
-            listenevents.push(thisevent);
-            thisevent = Y.one('document').on('keydown', function(e) {
-                if (e.keyCode === 27) {
-                    e.preventDefault();
-                    cancel_edittitle(e);
-                }
-            });
-            listenevents.push(thisevent);
-
-            // Handle form submission
-            thisevent = editform.on('submit', function(e) {
-                // We don't actually want to submit anything
-                e.preventDefault();
-
-                // Clear the edit title boxes
-                clear_edittitle();
-
-                // We only accept strings which have valid content
-                var newtitle = Y.Lang.trim(editor.get('value'));
-                if (newtitle != null && newtitle != "" && newtitle != titletext) {
-                    var data = {
-                        'class'   : 'resource',
-                        'field'   : 'updatetitle',
-                        'title'   : newtitle,
-                        'id'      : this.get_element_id(element)
-                    };
-                    var response = this.send_request(data, editbutton);
-                    if (response.instancename) {
-                        currenttitle.set('data', response.instancename);
-                    }
-                } else {
-                    // Invalid content. Set the title back to it's original contents
-                    currenttitle.set('data', oldtitle);
-                }
-            }, this);
-            listenevents.push(thisevent);
+        /**
+         * Handles clearing the editing UI and returning things to the original state they were in.
+         *
+         * @protected
+         * @method edit_title_clear
+         * @param {Node} activity  The activity whose title we were altering.
+         */
+        edit_title_clear : function(activity) {
+            // Detach all listen events to prevent duplicate triggers
+            var thisevent;
+            while (thisevent = this.edittitleevents.shift()) {
+                thisevent.detach();
+            }
+            var editform = activity.one(SELECTOR.ACTIVITYFORM),
+                instructions = activity.one('#id_editinstructions');
+            if (editform) {
+                editform.replace(editform.getData('anchor'));
+            }
+            if (instructions) {
+                instructions.remove();
+            }
         },
+
         /**
          * Set the visibility of the current resource (identified by the element)
          * to match the hidden parameter (this is not a toggle).
          * Only changes the visibility in the browser (no ajax update).
+         *
+         * @public This method is used by other modules.
+         * @method set_visibility_resource_ui
          * @param args An object with 'element' being the A node containing the resource
-         *             and 'visible' being the state that the visiblity should be set to.
-         * @return void
+         *             and 'visible' being the state that the visibility should be set to.
          */
         set_visibility_resource_ui: function(args) {
-            var element = args.element;
-            var shouldbevisible = args.visible;
-            var buttonnode = element.one(CSS.SHOW);
-            var visible = (buttonnode === null);
+            var element = args.element,
+                shouldbevisible = args.visible,
+                buttonnode = element.one(SELECTOR.SHOW),
+                visible = (buttonnode === null),
+                status = 'hide';
             if (visible) {
-                buttonnode = element.one(CSS.HIDE);
+                buttonnode = element.one(SELECTOR.HIDE);
+                status = 'show'
             }
             if (visible != shouldbevisible) {
-                this.toggle_hide_resource_ui(buttonnode);
+                this.handle_resource_dim(buttonnode, buttonnode.getData('activity'), status);
             }
         }
     }, {
@@ -651,9 +751,9 @@ YUI.add('moodle-course-toolboxes', function(Y) {
             M.course.coursebase.register_module(this);
 
             // Section Highlighting
-            Y.delegate('click', this.toggle_highlight, CSS.PAGECONTENT, CSS.SECTIONLI + ' ' + CSS.HIGHLIGHT, this);
+            Y.delegate('click', this.toggle_highlight, SELECTOR.PAGECONTENT, SELECTOR.SECTIONLI + ' ' + SELECTOR.HIGHLIGHT, this);
             // Section Visibility
-            Y.delegate('click', this.toggle_hide_section, CSS.PAGECONTENT, CSS.SECTIONLI + ' ' + CSS.SHOWHIDE, this);
+            Y.delegate('click', this.toggle_hide_section, SELECTOR.PAGECONTENT, SELECTOR.SECTIONLI + ' ' + SELECTOR.SHOWHIDE, this);
         },
         /**
          * Update any section areas within the scope of the specified
@@ -665,7 +765,7 @@ YUI.add('moodle-course-toolboxes', function(Y) {
         setup_for_section : function(baseselector) {
             // Left here for potential future use - not currently needed due to YUI delegation in initializer()
             /*if (!baseselector) {
-                var baseselector = CSS.PAGECONTENT;
+                var baseselector = SELECTOR.PAGECONTENT;
             }
 
             Y.all(baseselector).each(this._setup_for_section, this);*/
@@ -685,17 +785,19 @@ YUI.add('moodle-course-toolboxes', function(Y) {
             // The value to submit
             var value;
             // The status text for strings and images
-            var status;
+            var status,
+                oldstatus;
 
             if (!section.hasClass(CSS.SECTIONHIDDENCLASS)) {
                 section.addClass(CSS.SECTIONHIDDENCLASS);
                 value = 0;
                 status = 'show';
-
+                oldstatus = 'hide';
             } else {
                 section.removeClass(CSS.SECTIONHIDDENCLASS);
                 value = 1;
                 status = 'hide';
+                oldstatus = 'show';
             }
 
             var newstring = M.util.get_string(status + 'fromothers', 'format_' + this.get('format'));
@@ -718,17 +820,17 @@ YUI.add('moodle-course-toolboxes', function(Y) {
 
             var response = this.send_request(data, lightbox);
 
-            var activities = section.all(CSS.ACTIVITYLI);
+            var activities = section.all(SELECTOR.ACTIVITYLI);
             activities.each(function(node) {
-                if (node.one(CSS.SHOW)) {
-                    var button = node.one(CSS.SHOW);
+                if (node.one(SELECTOR.SHOW)) {
+                    var button = node.one(SELECTOR.SHOW);
                 } else {
-                    var button = node.one(CSS.HIDE);
+                    var button = node.one(SELECTOR.HIDE);
                 }
                 var activityid = this.get_element_id(node);
 
                 if (Y.Array.indexOf(response.resourcestotoggle, activityid) != -1) {
-                    this.toggle_hide_resource_ui(button);
+                    node.getData('toolbox').handle_resource_dim(button, node, oldstatus);
                 }
             }, this);
         },
@@ -747,16 +849,16 @@ YUI.add('moodle-course-toolboxes', function(Y) {
 
             // Set the current highlighted item text
             var old_string = M.util.get_string('markthistopic', 'moodle');
-            Y.one(CSS.PAGECONTENT)
-                .all(M.course.format.get_section_selector(Y) + '.current ' + CSS.HIGHLIGHT)
+            Y.one(SELECTOR.PAGECONTENT)
+                .all(M.course.format.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT)
                 .set('title', old_string);
-            Y.one(CSS.PAGECONTENT)
-                .all(M.course.format.get_section_selector(Y) + '.current ' + CSS.HIGHLIGHT + ' img')
+            Y.one(SELECTOR.PAGECONTENT)
+                .all(M.course.format.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT + ' img')
                 .set('alt', old_string)
                 .set('src', M.util.image_url('i/marker'));
 
             // Remove the highlighting from all sections
-            var allsections = Y.one(CSS.PAGECONTENT).all(M.course.format.get_section_selector(Y))
+            var allsections = Y.one(SELECTOR.PAGECONTENT).all(M.course.format.get_section_selector(Y))
                 .removeClass('current');
 
             // Then add it if required to the selected section
@@ -794,15 +896,28 @@ YUI.add('moodle-course-toolboxes', function(Y) {
     });
 
     M.course = M.course || {};
-
+    M.course.resource_toolbox = null;
     M.course.init_resource_toolbox = function(config) {
-        return new RESOURCETOOLBOX(config);
+        M.course.resource_toolbox = new RESOURCETOOLBOX(config);
+        return M.course.resource_toolbox;
     };
 
     M.course.init_section_toolbox = function(config) {
         return new SECTIONTOOLBOX(config);
     };
 
+    M.course.register_new_module = function(module) {
+        if (typeof module === 'string') {
+            module = Y.one(module);
+        }
+        if (M.course.resource_toolbox !== null) {
+            module.setData('toolbox', M.course.resource_toolbox);
+            module.all(SELECTOR.COMMANDSPAN+ ' ' + SELECTOR.ACTIVITYACTION).each(function(){
+                this.setData('activity', module);
+            });
+        }
+    }
+
 },
 '@VERSION@', {
     requires : ['base', 'node', 'io', 'moodle-course-coursebase']
index a23f9b5..3a3c26b 100644 (file)
@@ -90,12 +90,6 @@ class enrol_meta_handler {
 
         $context = context_course::instance($instance->courseid);
 
-        if (!$parentcontext = context_course::instance($instance->customint1, IGNORE_MISSING)) {
-            // linking to missing course is not possible
-            role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_meta'));
-            return;
-        }
-
         // list of enrolments in parent course (we ignore meta enrols in parents completely)
         list($enabled, $params) = $DB->get_in_or_equal(explode(',', $CFG->enrol_plugins_enabled), SQL_PARAMS_NAMED, 'e');
         $params['userid'] = $userid;
@@ -114,10 +108,8 @@ class enrol_meta_handler {
             return;
         }
 
-        if (!enrol_is_enabled('meta')) {
-            if ($ue) {
-                role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_meta'));
-            }
+        if (!$parentcontext = context_course::instance($instance->customint1, IGNORE_MISSING)) {
+            // Weird, we should not get here.
             return;
         }
 
@@ -172,9 +164,13 @@ class enrol_meta_handler {
             $ue->status = $parentstatus;
         }
 
+        $unenrolaction = $plugin->get_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES);
+
         // only active users in enabled instances are supposed to have roles (we can reassign the roles any time later)
         if ($ue->status != ENROL_USER_ACTIVE or $instance->status != ENROL_INSTANCE_ENABLED) {
-            if ($roles) {
+            if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND) {
+                // Always keep the roles.
+            } else if ($roles) {
                 role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_meta', 'itemid'=>$instance->id));
             }
             return;
@@ -187,6 +183,11 @@ class enrol_meta_handler {
             }
         }
 
+        if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND) {
+            // Always keep the roles.
+            return;
+        }
+
         // remove roles
         foreach ($roles as $rid) {
             if (!isset($parentroles[$rid])) {
@@ -206,25 +207,30 @@ class enrol_meta_handler {
      */
     protected static function user_not_supposed_to_be_here($instance, $ue, context_course $context, $plugin) {
         if (!$ue) {
-            // not enrolled yet - simple!
+            // Not enrolled yet - simple!
             return;
         }
 
         $userid = $ue->userid;
-        $unenrolaction = $plugin->get_config('unenrolaction', ENROL_EXT_REMOVED_UNENROL);
+        $unenrolaction = $plugin->get_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES);
 
         if ($unenrolaction == ENROL_EXT_REMOVED_UNENROL) {
-            // purges grades, group membership, preferences, etc. - admins were warned!
+            // Purges grades, group membership, preferences, etc. - admins were warned!
             $plugin->unenrol_user($instance, $userid);
-            return;
 
-        } else { // ENROL_EXT_REMOVED_SUSPENDNOROLES
-            // just suspend users and remove all roles (we can reassign the roles any time later)
+        } else if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND) {
             if ($ue->status != ENROL_USER_SUSPENDED) {
                 $plugin->update_user_enrol($instance, $userid, ENROL_USER_SUSPENDED);
-                role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_meta', 'itemid'=>$instance->id));
             }
-            return;
+
+        } else if ($unenrolaction == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
+            if ($ue->status != ENROL_USER_SUSPENDED) {
+                $plugin->update_user_enrol($instance, $userid, ENROL_USER_SUSPENDED);
+            }
+            role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_meta', 'itemid'=>$instance->id));
+
+        } else {
+            debugging('Unknown unenrol action '.$unenrolaction);
         }
     }
 
@@ -316,8 +322,10 @@ class enrol_meta_handler {
      * @return bool success
      */
     public static function user_unenrolled($ue) {
-
-        // keep unenrolling even if plugin disabled
+        if (!enrol_is_enabled('meta')) {
+            // This is slow, let enrol_meta_sync() deal with disabled plugin.
+            return true;
+        }
 
         if ($ue->enrol === 'meta') {
             // prevent circular dependencies - we can not sync meta enrolments recursively
@@ -360,7 +368,10 @@ class enrol_meta_handler {
     public static function course_deleted($course) {
         global $DB;
 
-        // NOTE: do not test if plugin enabled, we want to keep disabling instances with invalid course links
+        if (!enrol_is_enabled('meta')) {
+            // This is slow, let enrol_meta_sync() deal with disabled plugin.
+            return true;
+        }
 
         // does anything want to sync with this parent?
         if (!$enrols = $DB->get_records('enrol', array('customint1'=>$course->id, 'enrol'=>'meta'), 'courseid ASC, id ASC')) {
@@ -368,19 +379,29 @@ class enrol_meta_handler {
         }
 
         $plugin = enrol_get_plugin('meta');
+        $unenrolaction = $plugin->get_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES);
 
-        // hack the DB info for all courses first
-        foreach ($enrols as $enrol) {
-            $enrol->customint1 = 0;
-            $enrol->status = ENROL_INSTANCE_DISABLED;
-            $DB->update_record('enrol', $enrol);
-            $context = context_course::instance($enrol->courseid);
-            role_unassign_all(array('contextid'=>$context->id, 'component'=>'enrol_meta', 'itemid'=>$enrol->id));
+        if ($unenrolaction == ENROL_EXT_REMOVED_UNENROL) {
+            // Simple, just delete this instance which purges all enrolments,
+            // admins were warned that this is risky setting!
+            foreach ($enrols as $enrol) {
+                $plugin->delete_instance($enrol);
+            }
+            return true;
         }
 
-        // now trigger sync for each instance and purge caches
         foreach ($enrols as $enrol) {
-            $plugin->update_status($enrol, ENROL_INSTANCE_DISABLED);
+            $enrol->customint = 0;
+            $DB->update_record('enrol', $enrol);
+
+            if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND or $unenrolaction == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
+                // This makes all enrolments suspended very quickly.
+                $plugin->update_status($enrol, ENROL_INSTANCE_DISABLED);
+            }
+            if ($unenrolaction == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
+                $context = context_course::instance($enrol->courseid);
+                role_unassign_all(array('contextid'=>$context->id, 'component'=>'enrol_meta', 'itemid'=>$enrol->id));
+            }
         }
 
         return true;
@@ -419,7 +440,7 @@ function enrol_meta_sync($courseid = NULL, $verbose = false) {
 
     $meta = enrol_get_plugin('meta');
 
-    $unenrolaction = $meta->get_config('unenrolaction', ENROL_EXT_REMOVED_UNENROL);
+    $unenrolaction = $meta->get_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES);
     $skiproles     = $meta->get_config('nosyncroleids', '');
     $skiproles     = empty($skiproles) ? array() : explode(',', $skiproles);
     $syncall       = $meta->get_config('syncall', 1);
@@ -493,19 +514,24 @@ function enrol_meta_sync($courseid = NULL, $verbose = false) {
             if ($verbose) {
                 mtrace("  unenrolling: $ue->userid ==> $instance->courseid");
             }
-            continue;
 
-        } else { // ENROL_EXT_REMOVED_SUSPENDNOROLES
-            // just disable and ignore any changes
+        } else if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND) {
+            if ($ue->status != ENROL_USER_SUSPENDED) {
+                $meta->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED);
+                if ($verbose) {
+                    mtrace("  suspending: $ue->userid ==> $instance->courseid");
+                }
+            }
+
+        } else if ($unenrolaction == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
             if ($ue->status != ENROL_USER_SUSPENDED) {
                 $meta->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED);
                 $context = context_course::instance($instance->courseid);
-                role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$context->id, 'component'=>'enrol_meta'));
+                role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$context->id, 'component'=>'enrol_meta', 'itemid'=>$instance->id));
                 if ($verbose) {
                     mtrace("  suspending and removing all roles: $ue->userid ==> $instance->courseid");
                 }
             }
-            continue;
         }
     }
     $rs->close();
@@ -617,6 +643,7 @@ function enrol_meta_sync($courseid = NULL, $verbose = false) {
     } else {
         $notignored = "";
     }
+
     $sql = "SELECT ra.roleid, ra.userid, ra.contextid, ra.itemid, e.courseid
               FROM {role_assignments} ra
               JOIN {enrol} e ON (e.id = ra.itemid AND ra.component = 'enrol_meta' AND e.enrol = 'meta' $onecourse)
@@ -625,14 +652,16 @@ function enrol_meta_sync($courseid = NULL, $verbose = false) {
          LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = ra.userid AND ue.status = :activeuser)
              WHERE pra.id IS NULL OR ue.id IS NULL OR e.status <> :enabledinstance";
 
-    $rs = $DB->get_recordset_sql($sql, $params);
-    foreach($rs as $ra) {
-        role_unassign($ra->roleid, $ra->userid, $ra->contextid, 'enrol_meta', $ra->itemid);
-        if ($verbose) {
-            mtrace("  unassigning role: $ra->userid ==> $ra->courseid as ".$allroles[$ra->roleid]->shortname);
+    if ($unenrolaction != ENROL_EXT_REMOVED_SUSPEND) {
+        $rs = $DB->get_recordset_sql($sql, $params);
+        foreach($rs as $ra) {
+            role_unassign($ra->roleid, $ra->userid, $ra->contextid, 'enrol_meta', $ra->itemid);
+            if ($verbose) {
+                mtrace("  unassigning role: $ra->userid ==> $ra->courseid as ".$allroles[$ra->roleid]->shortname);
+            }
         }
+        $rs->close();
     }
-    $rs->close();
 
 
     // kick out or suspend users without synced roles if syncall disabled
index d2f5d93..3cbc4e1 100644 (file)
@@ -36,8 +36,10 @@ if ($ADMIN->fulltree) {
         $settings->add(new admin_setting_configcheckbox('enrol_meta/syncall', get_string('syncall', 'enrol_meta'), get_string('syncall_desc', 'enrol_meta'), 1));
 
         $options = array(
-            ENROL_EXT_REMOVED_UNENROL        => get_string('extremovedunenrol', 'enrol'),
-            ENROL_EXT_REMOVED_SUSPENDNOROLES => get_string('extremovedsuspendnoroles', 'enrol'));
+            ENROL_EXT_REMOVED_UNENROL        => get_string('extremovedunenrol', 'core_enrol'),
+            ENROL_EXT_REMOVED_SUSPEND        => get_string('extremovedsuspend', 'core_enrol'),
+            ENROL_EXT_REMOVED_SUSPENDNOROLES => get_string('extremovedsuspendnoroles', 'core_enrol'),
+        );
         $settings->add(new admin_setting_configselect('enrol_meta/unenrolaction', get_string('extremovedaction', 'enrol'), get_string('extremovedaction_help', 'enrol'), ENROL_EXT_REMOVED_SUSPENDNOROLES, $options));
     }
 }
diff --git a/enrol/meta/tests/plugin_test.php b/enrol/meta/tests/plugin_test.php
new file mode 100644 (file)
index 0000000..fdfec33
--- /dev/null
@@ -0,0 +1,442 @@
+<?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/>.
+
+/**
+ * Meta enrolment sync functional test.
+ *
+ * @package    enrol_meta
+ * @category   phpunit
+ * @copyright  2013 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+class enrol_meta_plugin_testcase extends advanced_testcase {
+
+    protected function enable_plugin() {
+        $enabled = enrol_get_plugins(true);
+        $enabled['meta'] = true;
+        $enabled = array_keys($enabled);
+        set_config('enrol_plugins_enabled', implode(',', $enabled));
+    }
+
+    protected function disable_plugin() {
+        $enabled = enrol_get_plugins(true);
+        unset($enabled['meta']);
+        $enabled = array_keys($enabled);
+        set_config('enrol_plugins_enabled', implode(',', $enabled));
+    }
+
+    protected function is_meta_enrolled($user, $enrol, $role = null) {
+        global $DB;
+
+        if (!$DB->record_exists('user_enrolments', array('enrolid'=>$enrol->id, 'userid'=>$user->id))) {
+            return false;
+        }
+
+        if ($role === null) {
+            return true;
+        }
+
+        return $this->has_role($user, $enrol, $role);
+    }
+
+    protected function has_role($user, $enrol, $role) {
+        global $DB;
+
+        $context = context_course::instance($enrol->courseid);
+
+        if ($role === false) {
+            if ($DB->record_exists('role_assignments', array('contextid'=>$context->id, 'userid'=>$user->id, 'component'=>'enrol_meta', 'itemid'=>$enrol->id))) {
+                return false;
+            }
+        } else if (!$DB->record_exists('role_assignments', array('contextid'=>$context->id, 'userid'=>$user->id, 'roleid'=>$role->id, 'component'=>'enrol_meta', 'itemid'=>$enrol->id))) {
+            return false;
+        }
+
+        return true;
+    }
+
+    public function test_sync() {
+        global $CFG, $DB;
+
+        $this->resetAfterTest(true);
+
+        $metalplugin = enrol_get_plugin('meta');
+        $manplugin = enrol_get_plugin('manual');
+
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        $user3 = $this->getDataGenerator()->create_user();
+        $user4 = $this->getDataGenerator()->create_user();
+        $user5 = $this->getDataGenerator()->create_user();
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
+        $course3 = $this->getDataGenerator()->create_course();
+        $course4 = $this->getDataGenerator()->create_course();
+        $manual1 = $DB->get_record('enrol', array('courseid'=>$course1->id, 'enrol'=>'manual'), '*', MUST_EXIST);
+        $manual2 = $DB->get_record('enrol', array('courseid'=>$course2->id, 'enrol'=>'manual'), '*', MUST_EXIST);
+        $manual3 = $DB->get_record('enrol', array('courseid'=>$course3->id, 'enrol'=>'manual'), '*', MUST_EXIST);
+        $manual4 = $DB->get_record('enrol', array('courseid'=>$course4->id, 'enrol'=>'manual'), '*', MUST_EXIST);
+
+        $student = $DB->get_record('role', array('shortname'=>'student'));
+        $teacher = $DB->get_record('role', array('shortname'=>'teacher'));
+        $manager = $DB->get_record('role', array('shortname'=>'manager'));
+
+        $this->disable_plugin();
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, $student->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course1->id, $student->id);
+        $this->getDataGenerator()->enrol_user($user3->id, $course1->id, 0);
+        $this->getDataGenerator()->enrol_user($user4->id, $course1->id, $teacher->id);
+        $this->getDataGenerator()->enrol_user($user5->id, $course1->id, $manager->id);
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course2->id, $student->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course2->id, $teacher->id);
+
+        $this->assertEquals(7, $DB->count_records('user_enrolments'));
+        $this->assertEquals(6, $DB->count_records('role_assignments'));
+
+        set_config('syncall', 0, 'enrol_meta');
+        set_config('nosyncroleids', $manager->id, 'enrol_meta');
+
+        require_once($CFG->dirroot.'/enrol/meta/locallib.php');
+
+        enrol_meta_sync(null, false);
+        $this->assertEquals(7, $DB->count_records('user_enrolments'));
+        $this->assertEquals(6, $DB->count_records('role_assignments'));
+
+        $this->enable_plugin();
+        enrol_meta_sync(null, false);
+        $this->assertEquals(7, $DB->count_records('user_enrolments'));
+        $this->assertEquals(6, $DB->count_records('role_assignments'));
+
+        $e1 = $metalplugin->add_instance($course3, array('customint1'=>$course1->id));
+        $e2 = $metalplugin->add_instance($course3, array('customint1'=>$course2->id));
+        $e3 = $metalplugin->add_instance($course4, array('customint1'=>$course2->id));
+        $enrol1 = $DB->get_record('enrol', array('id'=>$e1));
+        $enrol2 = $DB->get_record('enrol', array('id'=>$e2));
+        $enrol3 = $DB->get_record('enrol', array('id'=>$e3));
+
+        enrol_meta_sync($course4->id, false);
+        $this->assertEquals(9, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol3, $student));
+        $this->assertTrue($this->is_meta_enrolled($user2, $enrol3, $teacher));
+
+        enrol_meta_sync(null, false);
+        $this->assertEquals(14, $DB->count_records('user_enrolments'));
+        $this->assertEquals(13, $DB->count_records('role_assignments'));
+
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+        $this->assertTrue($this->is_meta_enrolled($user2, $enrol1, $student));
+        $this->assertFalse($this->is_meta_enrolled($user3, $enrol1));
+        $this->assertTrue($this->is_meta_enrolled($user4, $enrol1, $teacher));
+        $this->assertFalse($this->is_meta_enrolled($user5, $enrol1));
+
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol2, $student));
+        $this->assertTrue($this->is_meta_enrolled($user2, $enrol2, $teacher));
+
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol3, $student));
+        $this->assertTrue($this->is_meta_enrolled($user2, $enrol3, $teacher));
+
+        set_config('syncall', 1, 'enrol_meta');
+        enrol_meta_sync(null, false);
+        $this->assertEquals(16, $DB->count_records('user_enrolments'));
+        $this->assertEquals(13, $DB->count_records('role_assignments'));
+
+        $this->assertTrue($this->is_meta_enrolled($user3, $enrol1, false));
+        $this->assertTrue($this->is_meta_enrolled($user5, $enrol1, false));
+
+        $this->assertEquals(16, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->disable_plugin();
+        $manplugin->unenrol_user($manual1, $user1->id);
+        $manplugin->unenrol_user($manual2, $user1->id);
+
+        $this->assertEquals(14, $DB->count_records('user_enrolments'));
+        $this->assertEquals(11, $DB->count_records('role_assignments'));
+        $this->assertEquals(14, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+
+        $this->enable_plugin();
+
+        set_config('unenrolaction', ENROL_EXT_REMOVED_SUSPEND, 'enrol_meta');
+        enrol_meta_sync($course4->id, false);
+        $this->assertEquals(14, $DB->count_records('user_enrolments'));
+        $this->assertEquals(11, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol3, $student));
+        $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$enrol3->id, 'status'=>ENROL_USER_SUSPENDED, 'userid'=>$user1->id)));
+
+        enrol_meta_sync(null, false);
+        $this->assertEquals(14, $DB->count_records('user_enrolments'));
+        $this->assertEquals(11, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+        $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$enrol1->id, 'status'=>ENROL_USER_SUSPENDED, 'userid'=>$user1->id)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol2, $student));
+        $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$enrol2->id, 'status'=>ENROL_USER_SUSPENDED, 'userid'=>$user1->id)));
+
+        set_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES, 'enrol_meta');
+        enrol_meta_sync($course4->id, false);
+        $this->assertEquals(14, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol3, false));
+        $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$enrol3->id, 'status'=>ENROL_USER_SUSPENDED, 'userid'=>$user1->id)));
+
+        enrol_meta_sync(null, false);
+        $this->assertEquals(14, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, false));
+        $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$enrol1->id, 'status'=>ENROL_USER_SUSPENDED, 'userid'=>$user1->id)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol2, false));
+        $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$enrol2->id, 'status'=>ENROL_USER_SUSPENDED, 'userid'=>$user1->id)));
+
+        set_config('unenrolaction', ENROL_EXT_REMOVED_UNENROL, 'enrol_meta');
+        enrol_meta_sync($course4->id, false);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertFalse($this->is_meta_enrolled($user1, $enrol3));
+
+        enrol_meta_sync(null, false);
+        $this->assertEquals(11, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertFalse($this->is_meta_enrolled($user1, $enrol1));
+        $this->assertFalse($this->is_meta_enrolled($user1, $enrol2));
+
+
+        // Now try sync triggered by events.
+
+        set_config('syncall', 1, 'enrol_meta');
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, $student->id);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+
+        $manplugin->unenrol_user($manual1, $user1->id);
+        $this->assertEquals(11, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertFalse($this->is_meta_enrolled($user1, $enrol1));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(11, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertFalse($this->is_meta_enrolled($user1, $enrol1));
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 0);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, false));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, false));
+
+        $manplugin->unenrol_user($manual1, $user1->id);
+        $this->assertEquals(11, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertFalse($this->is_meta_enrolled($user1, $enrol1));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(11, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertFalse($this->is_meta_enrolled($user1, $enrol1));
+
+        set_config('syncall', 0, 'enrol_meta');
+        enrol_meta_sync(null, false);
+        $this->assertEquals(9, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(9, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertFalse($this->is_meta_enrolled($user1, $enrol1));
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 0);
+        $this->assertEquals(10, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(10, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertFalse($this->is_meta_enrolled($user1, $enrol1, $student));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(10, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(10, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertFalse($this->is_meta_enrolled($user1, $enrol1, $student));
+
+        role_assign($teacher->id, $user1->id, context_course::instance($course1->id)->id);
+        $this->assertEquals(11, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $teacher));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(11, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $teacher));
+
+        role_unassign($teacher->id, $user1->id, context_course::instance($course1->id)->id);
+        $this->assertEquals(10, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(10, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertFalse($this->is_meta_enrolled($user1, $enrol1, $student));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(10, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(10, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertFalse($this->is_meta_enrolled($user1, $enrol1, $student));
+
+        $manplugin->unenrol_user($manual1, $user1->id);
+        $this->assertEquals(9, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(9, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertFalse($this->is_meta_enrolled($user1, $enrol1));
+
+        set_config('syncall', 1, 'enrol_meta');
+        set_config('unenrolaction', ENROL_EXT_REMOVED_SUSPEND, 'enrol_meta');
+        enrol_meta_sync(null, false);
+        $this->assertEquals(11, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, $student->id);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+
+        $manplugin->update_user_enrol($manual1, $user1->id, ENROL_USER_SUSPENDED);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+
+        $manplugin->unenrol_user($manual1, $user1->id);
+        $this->assertEquals(12, $DB->count_records('user_enrolments'));
+        $this->assertEquals(9, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(12, $DB->count_records('user_enrolments'));
+        $this->assertEquals(9, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, $student->id);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+
+        set_config('syncall', 1, 'enrol_meta');
+        set_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES, 'enrol_meta');
+        enrol_meta_sync(null, false);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, $student->id);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+
+        $manplugin->unenrol_user($manual1, $user1->id);
+        $this->assertEquals(12, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, false));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(12, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(11, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, false));
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, $student->id);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        $this->assertTrue($this->is_meta_enrolled($user1, $enrol1, $student));
+
+
+        set_config('unenrolaction', ENROL_EXT_REMOVED_UNENROL, 'enrol_meta');
+        enrol_meta_sync(null, false);
+        $this->assertEquals(13, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(13, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+
+        delete_course($course1, false);
+        $this->assertEquals(3, $DB->count_records('user_enrolments'));
+        $this->assertEquals(3, $DB->count_records('role_assignments'));
+        $this->assertEquals(3, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(3, $DB->count_records('user_enrolments'));
+        $this->assertEquals(3, $DB->count_records('role_assignments'));
+        $this->assertEquals(3, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+
+        delete_course($course2, false);
+        $this->assertEquals(0, $DB->count_records('user_enrolments'));
+        $this->assertEquals(0, $DB->count_records('role_assignments'));
+        $this->assertEquals(0, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+        enrol_meta_sync(null, false);
+        $this->assertEquals(0, $DB->count_records('user_enrolments'));
+        $this->assertEquals(0, $DB->count_records('role_assignments'));
+        $this->assertEquals(0, $DB->count_records('user_enrolments', array('status'=>ENROL_USER_ACTIVE)));
+
+        delete_course($course3, false);
+        delete_course($course4, false);
+
+    }
+}
index 5f9eb61..3026134 100644 (file)
@@ -803,8 +803,8 @@ class core_files_renderer extends plugin_renderer_base {
      */
     private function fp_js_template_message() {
         $rv = '
-<div class="file-picker fp-msg">
-    <p class="{!}fp-msg-text"></p>
+<div class="file-picker fp-msg" role="alertdialog" aria-live="assertive" aria-labelledby="fp-msg-labelledby">
+    <p class="{!}fp-msg-text" id="fp-msg-labelledby"></p>
     <button class="{!}fp-msg-butok">'.get_string('ok').'</button>
 </div>';
         return preg_replace('/\{\!\}/', '', $rv);
index c5920ff..3526c58 100644 (file)
@@ -365,7 +365,8 @@ class grade_report_user extends grade_report {
                     $cm = $instances[$grade_object->iteminstance];
                     if (!$cm->uservisible) {
                         // Further checks are required to determine whether the activity is entirely hidden or just greyed out.
-                        if ($cm->is_user_access_restricted_by_group() || $cm->is_user_access_restricted_by_conditional_access()) {
+                        if ($cm->is_user_access_restricted_by_group() || $cm->is_user_access_restricted_by_conditional_access() ||
+                                $cm->is_user_access_restricted_by_capability()) {
                             $hide = true;
                         }
                     }
index e1de235..55fcb02 100644 (file)
@@ -30,6 +30,8 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+$string['cannotcreatedboninstall'] = '<p>No és pot crear la base de dades.</p> <p>La base de dades especificada no existeix i l\'usuari que heu proporcionat no té permís per a crear-la.</p>
+<p>L\'administrador del lloc hauria de verificar la configuració de la base de dades.</p>';
 $string['cannotcreatelangdir'] = 'No s\'ha pogut crear el directori d\'idiomes.';
 $string['cannotcreatetempdir'] = 'No s\'ha pogut crear el directori temporal';
 $string['cannotdownloadcomponents'] = 'No s\'han pogut baixar components';
@@ -39,6 +41,7 @@ $string['cannotsavemd5file'] = 'No s\'ha pogut desar el fitxer md5';
 $string['cannotsavezipfile'] = 'No s\'ha pogut desar el fitxer zip';
 $string['cannotunzipfile'] = 'No s\'ha pogut descomprimir el fitxer';
 $string['componentisuptodate'] = 'El component està al dia';
+$string['dmlexceptiononinstall'] = '<p>S\'ha produït un error de la base de dades [{$a->errorcode}].<br />{$a->debuginfo}</p>';
 $string['downloadedfilecheckfailed'] = 'Ha fallat la comprovació del fitxer baixat';
 $string['invalidmd5'] = 'L\'md5 no és vàlid. Torneu-ho a provar';
 $string['missingrequiredfield'] = 'Falta algun camp necessari';
index e6c88d1..b151a5e 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+$string['clianswerno'] = 'خ';
+$string['cliansweryes'] = 'ب';
+$string['cliincorrectvalueerror'] = 'خطا، مقدار نامناسب «{$a->value}» برای «{$a->option}»';
+$string['cliincorrectvalueretry'] = 'مقدار نامناسب، لطفا دوباره تلاش کنید';
+$string['clitypevalue'] = 'مقدار را وارد کنید';
+$string['clitypevaluedefault'] = 'مقدار را وارد کنید، برای استفاده از مقدار پیش‌فرض ({$a}) کلید Enter را فشار دهید';
+$string['cliunknowoption'] = 'گزینه‌های شناخته نشده:
+  {$a}
+لطفا از گزینه <span style="direction:ltr">--help</span> استفاده کنید.';
+$string['cliyesnoprompt'] = 'یکی از گزینه‌های ب (به معنای بلی) یا خ (به نشانه خیر) را تایپ کنید';
 $string['environmentrequireinstall'] = 'باید نصب و فعال باشد';
 $string['environmentrequireversion'] = 'نسخهٔ {$a->needed} لازم است و شما نسخهٔ {$a->current} را دارید';
index 0748cc8..9630a2a 100644 (file)
@@ -31,6 +31,6 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['language'] = 'زبان';
-$string['next'] = 'بعدÛ\8c';
+$string['next'] = 'اداÙ\85Ù\87';
 $string['previous'] = 'قبلی';
 $string['reload'] = 'بررسی مجدد';
index db41839..9c2d544 100644 (file)
@@ -89,7 +89,7 @@ $string['phpversionhelp'] = '<p>Для Moodle необходим PHP верси
 (В случае с версией 5.0.x можно также откатиться к версии 4.4.x)</p>';
 $string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
 $string['welcomep20'] = 'Вы видите эту страницу, потому что успешно установили и запустили на своем компьютере набор программ <strong>{$a->packname} {$a->packversion}</strong>. Поздравляем!';
-$string['welcomep30'] = 'Эта версия набора программ <strong>{$a->installername}</strong> включает следующие программы, необходимые для создания среды в которой будет работать <strong>Moodle</strong>:';
+$string['welcomep30'] = 'Эта версия набора программ <strong>{$a->installername}</strong> включает следующие программы, необходимые для создания среды, в которой будет работать <strong>Moodle</strong>:';
 $string['welcomep40'] = 'Также в этот набор входит <strong>Moodle {$a->moodlerelease} ({$a->moodleversion})</strong>.';
 $string['welcomep50'] = 'Порядок использования приложений, входящих в этот набор, регламентируется соответствующими лицензиями. Набор программ <strong>{$a->installername}</strong> является полностью <a href="http://ru.wikipedia.org/wiki/Открытое_программное_обеспечение">открытым </a> и распространяется на условиях лицензии <a href="http://www.gnu.org/copyleft/gpl.html">GPL</a>.';
 $string['welcomep60'] = 'На следующих страницах Вы сможете за несколько простых шагов настроить и установить <strong>Moodle</strong> на свой компьютер. Вы сможете принять настройки по умолчанию или изменить их в зависимости от своих потребностей.';
index 98c4c55..2eb56b1 100644 (file)
@@ -74,6 +74,8 @@ $string['badwordsconfig'] = 'Enter your list of bad words separated by commas.';
 $string['badwordsdefault'] = 'If the custom list is empty, a default list from the language pack will be used.';
 $string['badwordslist'] = 'Custom bad words list';
 $string['blockediplist'] = 'Blocked IP List';
+$string['blockeditingmenu'] = 'Block editing menus';
+$string['blockeditingmenu_desc'] = 'If enabled many of the block editing icons shown when editing is on will be displayed within a drop-down menu. This reduces the content on screen by hiding the icons until they are needed.';
 $string['blockinstances'] = 'Instances';
 $string['blockmultiple'] = 'Multiple';
 $string['blockprotect'] = 'Protect instances';
@@ -728,6 +730,8 @@ $string['mobile'] = 'Mobile';
 $string['mobilecssurl'] = 'CSS';
 $string['modchooserdefault'] = 'Activity chooser default';
 $string['modeditdefaults'] = 'Default values for activity settings';
+$string['modeditingmenu'] = 'Activitiy editing menus';
+$string['modeditingmenu_desc'] = 'If enabled many of the activity editing icons shown when viewing a course with editing on will be displayed within a drop-down menu. This reduces the content on screen when editing a course by hiding the icons until they are needed.';
 $string['modsettings'] = 'Manage activities';
 $string['modulesecurity'] = 'Module security';
 $string['multilangforceold'] = 'Force old multilang syntax: &lt;span&gt; without the class="multilang" and &lt;lang&gt;';
@@ -770,6 +774,7 @@ $string['notifyloginthreshold'] = 'Threshold for email notifications';
 $string['notloggedinroleid'] = 'Role for visitors';
 $string['numberofmissingstrings'] = 'Number of missing strings: {$a}';
 $string['numberofstrings'] = 'Total number of strings: {$a->strings}<br />Missing: {$a->missing} ({$a->missingpercent}&nbsp;%)';
+$string['opcacherecommended'] = 'PHP opcode caching improves performance and lowers memory requirements, OPcache extension is recommended and fully supported.';
 $string['opensslrecommended'] = 'Installing the optional OpenSSL library is highly recommended -- it enables Moodle Networking functionality.';
 $string['opentogoogle'] = 'Open to Google';
 $string['optionalmaintenancemessage'] = 'Optional maintenance message';
index 559821e..7374550 100644 (file)
@@ -134,7 +134,7 @@ function behat_error_handler($errno, $errstr, $errfile, $errline, $errcontext) {
     }
 
     // Wrapping the output.
-    echo '<div class="phpdebugmessage">' . PHP_EOL;
+    echo '<div class="phpdebugmessage" data-rel="phpdebugmessage">' . PHP_EOL;
     echo "$errnostr: $errstr in $errfile on line $errline" . PHP_EOL;
     echo '</div>';
 
index 1a7b909..8d687b2 100644 (file)
@@ -1037,36 +1037,53 @@ class block_manager {
 
         if ($this->page->user_can_edit_blocks()) {
             // Move icon.
-            $controls[] = array('url' => $actionurl . '&bui_moveid=' . $block->instance->id,
-                    'icon' => 't/move', 'caption' => get_string('moveblock', 'block', $blocktitle),
-                    'class' => 'editing_move');
+            $str = new lang_string('moveblock', 'block', $blocktitle);
+            $controls[] = new action_menu_link_primary(
+                new moodle_url($actionurl, array('bui_moveid' => $block->instance->id)),
+                new pix_icon('t/move', $str, 'moodle', array('class' => 'iconsmall', 'title' => '')),
+                $str,
+                array('class' => 'editing_move')
+            );
+
         }
 
         if ($this->page->user_can_edit_blocks() || $block->user_can_edit()) {
             // Edit config icon - always show - needed for positioning UI.
-            $controls[] = array('url' => $actionurl . '&bui_editid=' . $block->instance->id,
-                    'icon' => 't/edit', 'caption' => get_string('configureblock', 'block', $blocktitle),
-                    'class' => 'editing_edit');
+            $str = new lang_string('configureblock', 'block', $blocktitle);
+            $controls[] = new action_menu_link_primary(
+                new moodle_url($actionurl, array('bui_editid' => $block->instance->id)),
+                new pix_icon('t/edit', $str, 'moodle', array('class' => 'iconsmall', 'title' => '')),
+                $str,
+                array('class' => 'editing_edit')
+            );
+
         }
 
         if ($this->user_can_delete_block($block)) {
             // Delete icon.
-            $controls[] = array('url' => $actionurl . '&bui_deleteid=' . $block->instance->id,
-                    'icon' => 't/delete', 'caption' => get_string('deleteblock', 'block', $blocktitle),
-                    'class' => 'editing_delete');
+            $str = new lang_string('deleteblock', 'block', $blocktitle);
+            $controls[] = new action_menu_link_secondary(
+                new moodle_url($actionurl, array('bui_deleteid' => $block->instance->id)),
+                new pix_icon('t/delete', $str, 'moodle', array('class' => 'iconsmall', 'title' => '')),
+                $str,
+                array('class' => 'editing_delete')
+            );
         }
 
         if ($this->page->user_can_edit_blocks() && $block->instance_can_be_hidden()) {
             // Show/hide icon.
             if ($block->instance->visible) {
-                $controls[] = array('url' => $actionurl . '&bui_hideid=' . $block->instance->id,
-                        'icon' => 't/hide', 'caption' => get_string('hideblock', 'block', $blocktitle),
-                        'class' => 'editing_hide');
+                $str = new lang_string('hideblock', 'block', $blocktitle);
+                $url = new moodle_url($actionurl, array('bui_hideid' => $block->instance->id));
+                $icon = new pix_icon('t/hide', $str, 'moodle', array('class' => 'iconsmall', 'title' => ''));
+                $attributes = array('class' => 'editing_hide');
             } else {
-                $controls[] = array('url' => $actionurl . '&bui_showid=' . $block->instance->id,
-                        'icon' => 't/show', 'caption' => get_string('showblock', 'block', $blocktitle),
-                        'class' => 'editing_show');
+                $str = new lang_string('showblock', 'block', $blocktitle);
+                $url = new moodle_url($actionurl, array('bui_showid' => $block->instance->id));
+                $icon = new pix_icon('t/show', $str, 'moodle', array('class' => 'iconsmall', 'title' => ''));
+                $attributes = array('class' => 'editing_show');
             }
+            $controls[] = new action_menu_link_primary($url, $icon, $str, $attributes);
         }
 
         // Assign roles icon.
@@ -1077,12 +1094,17 @@ class block_manager {
             $return = $this->page->url->out(false);
             $return = str_replace($CFG->wwwroot . '/', '', $return);
 
-            $controls[] = array('url' => $CFG->wwwroot . '/' . $CFG->admin .
-                    '/roles/assign.php?contextid=' . $block->context->id . '&returnurl=' . urlencode($return),
-                    'icon' => 't/assignroles', 'caption' => get_string('assignrolesinblock', 'block', $blocktitle),
-                    'class' => 'editing_roles');
+            $rolesurl = new moodle_url('/admin/roles/assign.php', array('contextid'=>$block->context->id,
+                                                                         'returnurl'=>$return));
+            // Delete icon.
+            $str = new lang_string('assignrolesinblock', 'block', $blocktitle);
+            $controls[] = new action_menu_link_secondary(
+                $rolesurl,
+                new pix_icon('t/assignroles', $str, 'moodle', array('class' => 'iconsmall', 'title' => '')),
+                $str,
+                array('class' => 'editing_roles')
+            );
         }
-
         return $controls;
     }
 
diff --git a/lib/classes/minify.php b/lib/classes/minify.php
new file mode 100644 (file)
index 0000000..19d734f
--- /dev/null
@@ -0,0 +1,149 @@
+<?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/>.
+
+/**
+ * JS and CSS compression.
+ *
+ * @package    core
+ * @copyright  2013 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Collection of JS and CSS compression methods.
+ */
+class core_minify {
+    /**
+     * Minify JS code.
+     *
+     * @param string $content
+     * @return string minified JS code
+     */
+    public static function js($content) {
+        global $CFG;
+        require_once("$CFG->libdir/minify/lib/JSMinPlus.php");
+
+        try {
+            ob_start(); // JSMinPlus just echos errors, weird...
+            $compressed = JSMinPlus::minify($content);
+            if ($compressed !== false) {
+                ob_end_clean();
+                return $compressed;
+            }
+            $error = ob_get_clean();
+
+        } catch (Exception $e) {
+            ob_end_clean();
+            $error = $e->getMessage();
+        }
+
+        $return = <<<EOD
+
+try {console.log('Error: Minimisation of JavaScript failed!');} catch (e) {}
+
+// Error: $error
+// Problem detected during JavaScript minimisation, please review the following code
+// =================================================================================
+
+
+EOD;
+
+        return $return.$content;
+    }
+
+    /**
+     * Minify JS files.
+     *
+     * @param array $files
+     * @return string minified JS code
+     */
+    public static function js_files(array $files) {
+        if (empty($files)) {
+            return '';
+        }
+
+        $compressed = array();
+        foreach ($files as $file) {
+            $content = file_get_contents($file);
+            if ($content === false) {
+                $compressed[] = "\n\n// Cannot read JS file ".basename(dirname(dirname($file))).'/'.basename(dirname($file)).'/'.basename($file)."\n\n";
+                continue;
+            }
+            $compressed[] = self::js($content);
+        }
+
+        return implode("\n", $compressed);
+    }
+
+    /**
+     * Minify CSS code.
+     *
+     * @param string $content
+     * @return string minified CSS
+     */
+    public static function css($content) {
+        global $CFG;
+        require_once("$CFG->libdir/minify/lib/Minify/CSS/Compressor.php");
+
+        $error = 'unknown';
+        try {
+            $compressed = Minify_CSS_Compressor::process($content);
+            if ($compressed !== false) {
+                return $compressed;
+            }
+
+        } catch (Exception $e) {
+            $error = $e->getMessage();
+        }
+
+        $return = <<<EOD
+
+/* Error: $error */
+/* Problem detected during CSS minimisation, please review the following code */
+/* ========================================================================== */
+
+
+EOD;
+
+        return $return.$content;
+    }
+
+    /**
+     * Minify CSS files.
+     *
+     * @param array $files
+     * @return string minified CSS code
+     */
+    public static function css_files(array $files) {
+        if (empty($files)) {
+            return '';
+        }
+
+        $compressed = array();
+        foreach ($files as $file) {
+            $content = file_get_contents($file);
+            if ($content === false) {
+                $compressed[] = "\n\n/* Cannot read CSS file ".basename(dirname(dirname($file))).'/'.basename(dirname($file)).'/'.basename($file)."*/\n\n";
+                continue;
+            }
+            $compressed[] = self::css($content);
+        }
+
+        return implode("\n", $compressed);
+    }
+}
index 37c5f3f..585ce63 100644 (file)
@@ -25,7 +25,7 @@
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-// NOTE: do not verify MOODLE_INTERNAL here, this is used from themes too
+defined('MOODLE_INTERNAL') || die();
 
 /**
  * Stores CSS in a file at the given path.
 function css_store_css(theme_config $theme, $csspath, array $cssfiles, $chunk = false, $chunkurl = null) {
     global $CFG;
 
+    $css = '';
+    foreach ($cssfiles as $file) {
+        $css .= file_get_contents($file)."\n";
+    }
+
     // Check if both the CSS optimiser is enabled and the theme supports it.
     if (!empty($CFG->enablecssoptimiser) && $theme->supportscssoptimisation) {
         // This is an experimental feature introduced in Moodle 2.3
@@ -51,10 +56,6 @@ function css_store_css(theme_config $theme, $csspath, array $cssfiles, $chunk =
         // the CSS before it is cached removing excess styles and rules and stripping
         // out any extraneous content such as comments and empty rules.
         $optimiser = new css_optimiser;
-        $css = '';
-        foreach ($cssfiles as $file) {
-            $css .= file_get_contents($file)."\n";
-        }
         $css = $theme->post_process($css);
         $css = $optimiser->process($css);
 
@@ -73,7 +74,8 @@ function css_store_css(theme_config $theme, $csspath, array $cssfiles, $chunk =
         // However it has the distinct disadvantage of having to minify the CSS
         // before running the post process functions. Potentially things may break
         // here if theme designers try to push things with CSS post processing.
-        $css = $theme->post_process(css_minify_css($cssfiles));
+        $css = $theme->post_process($css);
+        $css = core_minify::css($css);
     }
 
     clearstatcache();
@@ -294,80 +296,6 @@ function css_send_css_not_found() {
     die('CSS was not found, sorry.');
 }
 
-/**
- * Uses the minify library to compress CSS.
- *
- * This is used if $CFG->enablecssoptimiser has been turned off. This was
- * the original CSS optimisation library.
- * It removes whitespace and shrinks things but does no apparent optimisation.
- * Note the minify library is still being used for JavaScript.
- *
- * @param array $files An array of files to minify
- * @return string The minified CSS
- */
-function css_minify_css($files) {
-    global $CFG;
-
-    if (empty($files)) {
-        return '';
-    }
-
-    // We do not really want any 304 here!
-    // There does not seem to be any better way to prevent them here.
-    unset($_SERVER['HTTP_IF_NONE_MATCH']);
-    unset($_SERVER['HTTP_IF_MODIFIED_SINCE']);
-
-    set_include_path($CFG->libdir . '/minify/lib' . PATH_SEPARATOR . get_include_path());
-    require_once('Minify.php');
-
-    if (0 === stripos(PHP_OS, 'win')) {
-        Minify::setDocRoot(); // IIS may need help
-    }
-    // disable all caching, we do it in moodle
-    Minify::setCache(null, false);
-
-    $options = array(
-        // JSMin is not GNU GPL compatible, use the plus version instead.
-        'minifiers' => array(Minify::TYPE_JS => array('JSMinPlus', 'minify')),
-        'bubbleCssImports' => false,
-        // Don't gzip content we just want text for storage
-        'encodeOutput' => false,
-        // Maximum age to cache, not used but required
-        'maxAge' => (60*60*24*20),
-        // The files to minify
-        'files' => $files,
-        // Turn orr URI rewriting
-        'rewriteCssUris' => false,
-        // This returns the CSS rather than echoing it for display
-        'quiet' => true
-    );
-
-    $error = 'unknown';
-    try {
-        $result = Minify::serve('Files', $options);
-        if ($result['success'] and $result['statusCode'] == 200) {
-            return $result['content'];
-        }
-    } catch (Exception $e) {
-        $error = $e->getMessage();
-        $error = str_replace("\r", ' ', $error);
-        $error = str_replace("\n", ' ', $error);
-    }
-
-    // minification failed - try to inform the theme developer and include the non-minified version
-    $css = <<<EOD
-/* Error: $error */
-/* Problem detected during theme CSS minimisation, please review the following code */
-/* ================================================================================ */
-
-
-EOD;
-    foreach ($files as $cssfile) {
-        $css .= file_get_contents($cssfile)."\n";
-    }
-    return $css;
-}
-
 /**
  * Determines if the given value is a valid CSS colour.
  *
index ffb2877..347c824 100644 (file)
@@ -81,6 +81,7 @@ $definitions = array(
     // This caches the html purifier cleaned text. This is done because the text is usually cleaned once for every user
     // and context combo. Text caching handles caching for the combination, this cache is responsible for caching the
     // cleaned text which is shareable.
+    // NOTE: this data may be safely stored in local caches on cluster nodes.
     'htmlpurifier' => array(
         'mode' => cache_store::MODE_APPLICATION,
     ),
index a580d0a..ee19a96 100644 (file)
@@ -2306,5 +2306,12 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2013071500.02);
     }
 
+    if ($oldversion < 2013072600.01) {
+        upgrade_mssql_nvarcharmax();
+        upgrade_mssql_varbinarymax();
+
+        upgrade_main_savepoint(true, 2013072600.01);
+    }
+
     return true;
 }
index 6edc5d8..3d89eb9 100644 (file)
@@ -164,3 +164,109 @@ function upgrade_mysql_fix_unsigned_and_lob_columns() {
         $pbar->update($i, $tablecount, "Converted unsigned/lob columns in MySQL database - $i/$tablecount.");
     }
 }
+
+/**
+ * Migrate NTEXT to NVARCHAR(MAX).
+ */
+function upgrade_mssql_nvarcharmax() {
+    global $DB;
+
+    if ($DB->get_dbfamily() !== 'mssql') {
+        return;
+    }
+
+    $pbar = new progress_bar('mssqlconvertntext', 500, true);
+
+    $prefix = $DB->get_prefix();
+    $tables = $DB->get_tables(false);
+
+    $tablecount = count($tables);
+    $i = 0;
+    foreach ($tables as $table) {
+        $i++;
+
+        $columns = array();
+
+        $sql = "SELECT column_name
+                  FROM INFORMATION_SCHEMA.COLUMNS
+                 WHERE table_name = '{{$table}}' AND UPPER(data_type) = 'NTEXT'";
+        $rs = $DB->get_recordset_sql($sql);
+        foreach ($rs as $column) {
+            $columns[] = $column->column_name;
+        }
+        $rs->close();
+
+        if ($columns) {
+            // Set appropriate timeout - 1 minute per thousand of records should be enough, min 60 minutes just in case.
+            $count = $DB->count_records($table, array());
+            $timeout = ($count/1000)*60;
+            $timeout = ($timeout < 60*60) ? 60*60 : (int)$timeout;
+            upgrade_set_timeout($timeout);
+
+            $updates = array();
+            foreach ($columns as $column) {
+                // Change the definition.
+                $sql = "ALTER TABLE {$prefix}$table ALTER COLUMN $column NVARCHAR(MAX)";
+                $DB->change_database_structure($sql);
+                $updates[] = "$column = $column";
+            }
+
+            // Now force the migration of text data to new optimised storage.
+            $sql = "UPDATE {{$table}} SET ".implode(', ', $updates);
+            $DB->execute($sql);
+        }
+
+        $pbar->update($i, $tablecount, "Converted NTEXT to NVARCHAR(MAX) columns in MS SQL Server database - $i/$tablecount.");
+    }
+}
+
+/**
+ * Migrate IMAGE to VARBINARY(MAX).
+ */
+function upgrade_mssql_varbinarymax() {
+    global $DB;
+
+    if ($DB->get_dbfamily() !== 'mssql') {
+        return;
+    }
+
+    $pbar = new progress_bar('mssqlconvertimage', 500, true);
+
+    $prefix = $DB->get_prefix();
+    $tables = $DB->get_tables(false);
+
+    $tablecount = count($tables);
+    $i = 0;
+    foreach ($tables as $table) {
+        $i++;
+
+        $columns = array();
+
+        $sql = "SELECT column_name
+                  FROM INFORMATION_SCHEMA.COLUMNS
+                 WHERE table_name = '{{$table}}' AND UPPER(data_type) = 'IMAGE'";
+        $rs = $DB->get_recordset_sql($sql);
+        foreach ($rs as $column) {
+            $columns[] = $column->column_name;
+        }
+        $rs->close();
+
+        if ($columns) {
+            // Set appropriate timeout - 1 minute per thousand of records should be enough, min 60 minutes just in case.
+            $count = $DB->count_records($table, array());
+            $timeout = ($count/1000)*60;
+            $timeout = ($timeout < 60*60) ? 60*60 : (int)$timeout;
+            upgrade_set_timeout($timeout);
+
+            foreach ($columns as $column) {
+                // Change the definition.
+                $sql = "ALTER TABLE {$prefix}$table ALTER COLUMN $column VARBINARY(MAX)";
+                $DB->change_database_structure($sql);
+            }
+
+            // Binary columns should not be used, do not waste time optimising the storage.
+        }
+
+        $pbar->update($i, $tablecount, "Converted IMAGE to VARBINARY(MAX) columns in MS SQL Server database - $i/$tablecount.");
+    }
+}
index bbf769d..ccd2d81 100644 (file)
@@ -221,10 +221,10 @@ class mssql_sql_generator extends sql_generator {
                 $dbtype .= '(' . $xmldb_length . ')';
                 break;
             case XMLDB_TYPE_TEXT:
-                $dbtype = 'NTEXT';
+                $dbtype = 'NVARCHAR(MAX)';
                 break;
             case XMLDB_TYPE_BINARY:
-                $dbtype = 'IMAGE';
+                $dbtype = 'VARBINARY(MAX)';
                 break;
             case XMLDB_TYPE_DATETIME:
                 $dbtype = 'DATETIME';
index ff33108..fa2d30a 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+/**
+ * Minify JavaScript files.
+ *
+ * @deprecated since 2.6
+ *
+ * @param array $files
+ * @return string
+ */
+function js_minify($files) {
+    debugging('js_minify() is deprecated, use core_minify::js_files() or core_minify::js() instead.');
+    return core_minify::js_files($files);
+}
+
+/**
+ * Minify CSS files.
+ *
+ * @deprecated since 2.6
+ *
+ * @param array $files
+ * @return string
+ */
+function css_minify_css($files) {
+    debugging('css_minify_css() is deprecated, use core_minify::css_files() or core_minify::css() instead.');
+    return core_minify::css_files($files);
+}
+
 /**
  * List all core subsystems and their location
  *
index 862b055..2191b89 100644 (file)
@@ -464,9 +464,15 @@ class mssql_native_moodle_database extends moodle_database {
             // id columns being auto_incremnt are PK by definition
             $info->primary_key = ($info->name == 'id' && $info->meta_type == 'R' && $info->auto_increment);
 
-            // Put correct length for character and LOB types
-            $info->max_length = $info->meta_type == 'C' ? $rawcolumn->char_max_length : $rawcolumn->max_length;
-            $info->max_length = ($info->meta_type == 'X' || $info->meta_type == 'B') ? -1 : $info->max_length;
+            if ($info->meta_type === 'C' and $rawcolumn->char_max_length == -1) {
+                // This is NVARCHAR(MAX), not a normal NVARCHAR.
+                $info->max_length = -1;
+                $info->meta_type = 'X';
+            } else {
+                // Put correct length for character and LOB types
+                $info->max_length = $info->meta_type == 'C' ? $rawcolumn->char_max_length : $rawcolumn->max_length;
+                $info->max_length = ($info->meta_type == 'X' || $info->meta_type == 'B') ? -1 : $info->max_length;
+            }
 
             // Scale
             $info->scale = $rawcolumn->scale ? $rawcolumn->scale : false;
@@ -573,6 +579,7 @@ class mssql_native_moodle_database extends moodle_database {
                 $type = 'X';
                 break;
             case 'IMAGE':
+            case 'VARBINARY':
             case 'VARBINARY(MAX)':
                 $type = 'B';
                 break;
index 13c46e4..b5062b1 100644 (file)
@@ -528,9 +528,15 @@ class sqlsrv_native_moodle_database extends moodle_database {
             // id columns being auto_incremnt are PK by definition
             $info->primary_key = ($info->name == 'id' && $info->meta_type == 'R' && $info->auto_increment);
 
-            // Put correct length for character and LOB types
-            $info->max_length = $info->meta_type == 'C' ? $rawcolumn->char_max_length : $rawcolumn->max_length;
-            $info->max_length = ($info->meta_type == 'X' || $info->meta_type == 'B') ? -1 : $info->max_length;
+            if ($info->meta_type === 'C' and $rawcolumn->char_max_length == -1) {
+                // This is NVARCHAR(MAX), not a normal NVARCHAR.
+                $info->max_length = -1;
+                $info->meta_type = 'X';
+            } else {
+                // Put correct length for character and LOB types
+                $info->max_length = $info->meta_type == 'C' ? $rawcolumn->char_max_length : $rawcolumn->max_length;
+                $info->max_length = ($info->meta_type == 'X' || $info->meta_type == 'B') ? -1 : $info->max_length;
+            }
 
             // Scale
             $info->scale = $rawcolumn->scale ? $rawcolumn->scale : false;
@@ -645,6 +651,7 @@ class sqlsrv_native_moodle_database extends moodle_database {
            break;
 
           case 'IMAGE':
+          case 'VARBINARY':
           case 'VARBINARY(MAX)':
            $type = 'B';
            break;
index 071c481..141a6ee 100644 (file)
@@ -83,7 +83,7 @@ if ($mimetype === 'application/x-javascript' && $allowcache) {
 
     // If it doesn't exist, minify it and save to that location.
     if (!file_exists($cachefile)) {
-        $content = js_minify(array($file));
+        $content = core_minify::js_files(array($file));
         js_write_cache_file_content($cachefile, $content);
     }
 
index 0f510ff..9175492 100644 (file)
@@ -12,4 +12,6 @@ Upgrade procedure:
 2/ bump up version.php
 3/ update lib/thirdpartylibs.xml
 4/ reimplement patch in MDL-23646
+5/ add in "DOM.setStyle(ifr, 'width',DOM.getSize(ifrcon).w); // Resize iframe" (without quotes)
+   after "DOM.setStyle(ifr, 'height',DOM.getSize(ifr).h + dy); // Resize iframe"
 5/ reminify the js manually (I used uglifyjs)
index 3646b35..13a65a4 100644 (file)
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\r
  * THE SOFTWARE.\r
  * Based on TinyMCE Wordpress plugin (Kitchen Sink)\r
+ * \r
+ * Changes V1.1.1 --> V1.2\r
+ * \r
+ * heeae made some modifications and posted his work on Sourceforge. I thought the cookie support \r
+ * was really handy so I updated the script.\r
+ * http://sourceforge.net/tracker/?func=detail&atid=738747&aid=2904683&group_id=103281\r
+ *\r
+ * 1. Added cookie support.\r
+ * 2. Some optimization\r
+ * 3. Bug fix of fire ifr.clientHeight in FF 3 ( to DOM.getStyles)\r
+ *\r
+ * Thanks heeae!\r
+ * \r
+ * Changes V1.1 --> V1.1.1\r
+ *\r
+ * Bugfix for Firefox 3.6. Caused error while loading script.\r
+ *\r
+ * Added lines 72 - 76:\r
+ *\r
+ * obj = ed.controlManager.get(tbIds[j]);\r
+ * if(typeof obj =="undefined") {\r
+ *             continue;\r
+ * }\r
+ * id = obj.id;\r
+ *\r
+ * instead of:\r
+ *\r
+ * try {\r
+ *             id = ed.controlManager.get(tbIds[j]).id;\r
+ * }\r
+ * catch(e) {\r
+ * //if(typeof id == "undefined") continue;\r
+ *             continue;\r
+ * }\r
+ *\r
+ * Thanks Anton for fixing this bug\r
+ * \r
  */\r
-!function(){var DOM=tinymce.DOM;tinymce.PluginManager.requireLangPack("pdw");tinymce.create("tinymce.plugins.pdw",{init:function(ed,url){var t=this,tbIds=new Array,toolbars=new Array,i;toolbars=ed.settings.pdw_toggle_toolbars.split(",");for(i=0;i<toolbars.length;i++){tbIds[i]=ed.getParam("","toolbar"+toolbars[i].replace(" ",""))}ed.addCommand("mcePDWToggleToolbars",function(){var cm=ed.controlManager,id,j,Cookie=tinymce.util.Cookie,Toggle_PDW,Toggle=Cookie.getHash("TinyMCE_toggle")||new Object;for(j=0;j<tbIds.length;j++){obj=ed.controlManager.get(tbIds[j]);if(typeof obj=="undefined"){continue}id=obj.id;if(DOM.isHidden(id)){Toggle_PDW=0;var e=document.getElementById(id);if(e){e.style.display="table";t._resizeIframe(ed,tbIds[j],-26)}}else{Toggle_PDW=1;var e=document.getElementById(id);if(e){e.style.display="none"}t._resizeIframe(ed,tbIds[j],26)}}cm.setActive("pdw_toggle",Toggle_PDW);ed.settings.pdw_toggle_on=Toggle_PDW;Toggle[ed.id]=Toggle_PDW;Cookie.setHash("TinyMCE_toggle",Toggle)});ed.addButton("pdw_toggle",{title:ed.getLang("pdw.desc",0),cmd:"mcePDWToggleToolbars",image:url+"/img/toolbars.gif"});ed.onPostRender.add(function(){var toggle=tinymce.util.Cookie.getHash("TinyMCE_toggle")||new Object;var run=false;if(toggle[ed.id]==null){run=ed.settings.pdw_toggle_on==1?true:false}else if(toggle[ed.id]==1){run=true}if(run){var cm=ed.controlManager,tdId,id;for(i=0;i<toolbars.length;i++){tbId=ed.getParam("","toolbar"+toolbars[i].replace(" ",""));id=ed.controlManager.get(tbId).id;cm.setActive("pdw_toggle",1);DOM.hide(id);t._resizeIframe(ed,tbId,26)}}})},_resizeIframe:function(ed,tb_id,dy){var ifr=ed.getContentAreaContainer().firstChild;DOM.setStyle(ifr,"height",DOM.getSize(ifr).h+dy);ed.theme.deltaHeight+=dy},getInfo:function(){return{longname:"PDW Toggle Toolbars",author:"Guido Neele",authorurl:"http://www.neele.name/",infourl:"http://www.neele.name/pdw_toggle_toolbars",version:"1.2"}}});tinymce.PluginManager.add("pdw",tinymce.plugins.pdw)}();
\ No newline at end of file
+\r
+(function() {\r
+       var DOM = tinymce.DOM;\r
+       tinymce.PluginManager.requireLangPack('pdw');\r
+       \r
+       tinymce.create('tinymce.plugins.pdw', {\r
+               /**\r
+                * Initializes the plugin, this will be executed after the plugin has been created.\r
+                * This call is done before the editor instance has finished it's initialization so use the onInit event\r
+                * of the editor instance to intercept that event.\r
+                *\r
+                * @param {tinymce.Editor} ed Editor instance that the plugin is initialized in.\r
+                * @param {string} url Absolute URL to where the plugin is located.\r
+                */\r
+               init : function(ed, url) {\r
+                       var t = this, tbIds = new Array(), toolbars = new Array(), i;\r
+                       \r
+                       // Split toolbars\r
+                       toolbars = (ed.settings.pdw_toggle_toolbars).split(',');\r
+                       \r
+                       for(i = 0; i < toolbars.length; i++){\r
+                               tbIds[i] = ed.getParam('', 'toolbar' + (toolbars[i]).replace(' ',''));\r
+                       }\r
+                       \r
+                       // Register the command so that it can be invoked by using tinyMCE.activeEditor.execCommand('mceExample');\r
+                       ed.addCommand('mcePDWToggleToolbars', function() {\r
+                       \r
+                               var cm = ed.controlManager, id, j, Cookie = tinymce.util.Cookie, Toggle_PDW, Toggle = Cookie.getHash("TinyMCE_toggle") || new Object();\r
+                               for(j = 0; j < tbIds.length; j++){\r
+                                       \r
+                                       obj = ed.controlManager.get(tbIds[j]);\r
+                    if(typeof obj =="undefined") {\r
+                        continue;\r
+                    }\r
+                    id = obj.id;\r
+                                       \r
+                                       if (DOM.isHidden(id)) {\r
+                                               Toggle_PDW = 0;\r
+                        var e = document.getElementById(id);\r
+                        if (e) {\r
+                            e.style.display = 'table';\r
+                            t._resizeIframe(ed, tbIds[j], -26);\r
+                        }\r
+                                               \r
+                                       } else {\r
+                                               Toggle_PDW = 1;\r
+                        var e = document.getElementById(id);\r
+                        if (e) {\r
+                            e.style.display = 'none';\r
+                        }\r
+                                               t._resizeIframe(ed, tbIds[j], 26);\r
+                                       }\r
+                               }\r
+                               cm.setActive('pdw_toggle', Toggle_PDW);\r
+                               ed.settings.pdw_toggle_on = Toggle_PDW;\r
+                               Toggle[ed.id] = Toggle_PDW;\r
+                               Cookie.setHash("TinyMCE_toggle", Toggle);\r
+                       });\r
+                       \r
+                       // Register pdw_toggle button\r
+                       ed.addButton('pdw_toggle', {\r
+                               title : ed.getLang('pdw.desc', 0),\r
+                               cmd : 'mcePDWToggleToolbars',\r
+                               image : url + '/img/toolbars.gif'\r
+                       });\r
+                       \r
+                       ed.onPostRender.add(function(){\r
+                               var toggle = tinymce.util.Cookie.getHash("TinyMCE_toggle") || new Object();\r
+                               var run = false;\r
+                               \r
+                               // Check if value is stored in cookie\r
+                               if(toggle[ed.id] == null){\r
+                                       // No cookie so check if the setting pdw_toggle_on is set to 1 then hide toolbars and set button active\r
+                                       run = ed.settings.pdw_toggle_on == 1 ? true : false;\r
+                               } else if(toggle[ed.id] == 1){\r
+                                       run = true;\r
+                               }\r
+                       \r
+                               if (run) {\r
+\r
+                                       var cm = ed.controlManager, tdId, id;\r
+                                       \r
+                                       for(i = 0; i < toolbars.length; i++){\r
+                                               tbId = ed.getParam('', 'toolbar' + (toolbars[i]).replace(' ',''));\r
+                                               id = ed.controlManager.get(tbId).id;\r
+                                               cm.setActive('pdw_toggle', 1);\r
+                                               DOM.hide(id);\r
+                                               t._resizeIframe(ed, tbId, 26);\r
+                                       }\r
+                               }\r
+                       });\r
+               },\r
+               \r
+               // Resizes the iframe by a relative height value\r
+               _resizeIframe : function(ed, tb_id, dy) {\r
+                   var ifr = ed.getContentAreaContainer().firstChild;\r
+                   var ifrcon = ed.getContentAreaContainer();\r
+                       \r
+                   DOM.setStyle(ifr, 'height',DOM.getSize(ifr).h + dy); // Resize iframe\r
+                   DOM.setStyle(ifr, 'width',DOM.getSize(ifrcon).w); // Resize iframe\r
+                       ed.theme.deltaHeight += dy; // For resize cookie\r
+               },\r
+\r
+               /**\r
+                * Returns information about the plugin as a name/value array.\r
+                * The current keys are longname, author, authorurl, infourl and version.\r
+                *\r
+                * @return {Object} Name/value array containing information about the plugin.\r
+                */\r
+               getInfo : function() {\r
+                       return {\r
+                               longname : 'PDW Toggle Toolbars',\r
+                               author : 'Guido Neele',\r
+                               authorurl : 'http://www.neele.name/',\r
+                               infourl : 'http://www.neele.name/pdw_toggle_toolbars',\r
+                               version : "1.2"\r
+                       };\r
+               }\r
+       });\r
+\r
+       // Register plugin\r
+       tinymce.PluginManager.add('pdw', tinymce.plugins.pdw);\r
+})();\r
diff --git a/lib/editor/tinymce/plugins/pdw/tinymce/editor_plugin_src.js b/lib/editor/tinymce/plugins/pdw/tinymce/editor_plugin_src.js
deleted file mode 100644 (file)
index 6a1c707..0000000
+++ /dev/null
@@ -1,182 +0,0 @@
-/**\r
- * PDW Toggle Toolbars v1.2\r
- * Url: http://www.neele.name\r
- * Author: Guido Neele\r
- * \r
- * Permission is hereby granted, free of charge, to any person obtaining a copy\r
- * of this software and associated documentation files (the "Software"), to deal\r
- * in the Software without restriction, including without limitation the rights\r
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\r
- * copies of the Software, and to permit persons to whom the Software is\r
- * furnished to do so, subject to the following conditions:\r
- * \r
- * The above copyright notice and this permission notice shall be included in\r
- * all copies or substantial portions of the Software.\r
- * \r
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\r
- * THE SOFTWARE.\r
- * Based on TinyMCE Wordpress plugin (Kitchen Sink)\r
- * \r
- * Changes V1.1.1 --> V1.2\r
- * \r
- * heeae made some modifications and posted his work on Sourceforge. I thought the cookie support \r
- * was really handy so I updated the script.\r
- * http://sourceforge.net/tracker/?func=detail&atid=738747&aid=2904683&group_id=103281\r
- *\r
- * 1. Added cookie support.\r
- * 2. Some optimization\r
- * 3. Bug fix of fire ifr.clientHeight in FF 3 ( to DOM.getStyles)\r
- *\r
- * Thanks heeae!\r
- * \r
- * Changes V1.1 --> V1.1.1\r
- *\r
- * Bugfix for Firefox 3.6. Caused error while loading script.\r
- *\r
- * Added lines 72 - 76:\r
- *\r
- * obj = ed.controlManager.get(tbIds[j]);\r
- * if(typeof obj =="undefined") {\r
- *             continue;\r
- * }\r
- * id = obj.id;\r
- *\r
- * instead of:\r
- *\r
- * try {\r
- *             id = ed.controlManager.get(tbIds[j]).id;\r
- * }\r
- * catch(e) {\r
- * //if(typeof id == "undefined") continue;\r
- *             continue;\r
- * }\r
- *\r
- * Thanks Anton for fixing this bug\r
- * \r
- */\r
-\r
-(function() {\r
-       var DOM = tinymce.DOM;\r
-       tinymce.PluginManager.requireLangPack('pdw');\r
-       \r
-       tinymce.create('tinymce.plugins.pdw', {\r
-               /**\r
-                * Initializes the plugin, this will be executed after the plugin has been created.\r
-                * This call is done before the editor instance has finished it's initialization so use the onInit event\r
-                * of the editor instance to intercept that event.\r
-                *\r
-                * @param {tinymce.Editor} ed Editor instance that the plugin is initialized in.\r
-                * @param {string} url Absolute URL to where the plugin is located.\r
-                */\r
-               init : function(ed, url) {\r
-                       var t = this, tbIds = new Array(), toolbars = new Array(), i;\r
-                       \r
-                       // Split toolbars\r
-                       toolbars = (ed.settings.pdw_toggle_toolbars).split(',');\r
-                       \r
-                       for(i = 0; i < toolbars.length; i++){\r
-                               tbIds[i] = ed.getParam('', 'toolbar' + (toolbars[i]).replace(' ',''));\r
-                       }\r
-                       \r
-                       // Register the command so that it can be invoked by using tinyMCE.activeEditor.execCommand('mceExample');\r
-                       ed.addCommand('mcePDWToggleToolbars', function() {\r
-                       \r
-                               var cm = ed.controlManager, id, j, Cookie = tinymce.util.Cookie, Toggle_PDW, Toggle = Cookie.getHash("TinyMCE_toggle") || new Object();\r
-                               for(j = 0; j < tbIds.length; j++){\r
-                                       \r
-                                       obj = ed.controlManager.get(tbIds[j]);\r
-                    if(typeof obj =="undefined") {\r
-                        continue;\r
-                    }\r
-                    id = obj.id;\r
-                                       \r
-                                       if (DOM.isHidden(id)) {\r
-                                               Toggle_PDW = 0;\r
-                        var e = document.getElementById(id);\r
-                        if (e) {\r
-                            e.style.display = 'table';\r
-                            t._resizeIframe(ed, tbIds[j], -26);\r
-                        }\r
-                                               \r
-                                       } else {\r
-                                               Toggle_PDW = 1;\r
-                        var e = document.getElementById(id);\r
-                        if (e) {\r
-                            e.style.display = 'none';\r
-                        }\r
-                                               t._resizeIframe(ed, tbIds[j], 26);\r
-                                       }\r
-                               }\r
-                               cm.setActive('pdw_toggle', Toggle_PDW);\r
-                               ed.settings.pdw_toggle_on = Toggle_PDW;\r
-                               Toggle[ed.id] = Toggle_PDW;\r
-                               Cookie.setHash("TinyMCE_toggle", Toggle);\r
-                       });\r
-                       \r
-                       // Register pdw_toggle button\r
-                       ed.addButton('pdw_toggle', {\r
-                               title : ed.getLang('pdw.desc', 0),\r
-                               cmd : 'mcePDWToggleToolbars',\r
-                               image : url + '/img/toolbars.gif'\r
-                       });\r
-                       \r
-                       ed.onPostRender.add(function(){\r
-                               var toggle = tinymce.util.Cookie.getHash("TinyMCE_toggle") || new Object();\r
-                               var run = false;\r
-                               \r
-                               // Check if value is stored in cookie\r
-                               if(toggle[ed.id] == null){\r
-                                       // No cookie so check if the setting pdw_toggle_on is set to 1 then hide toolbars and set button active\r
-                                       run = ed.settings.pdw_toggle_on == 1 ? true : false;\r
-                               } else if(toggle[ed.id] == 1){\r
-                                       run = true;\r
-                               }\r
-                       \r
-                               if (run) {\r
-\r
-                                       var cm = ed.controlManager, tdId, id;\r
-                                       \r
-                                       for(i = 0; i < toolbars.length; i++){\r
-                                               tbId = ed.getParam('', 'toolbar' + (toolbars[i]).replace(' ',''));\r
-                                               id = ed.controlManager.get(tbId).id;\r
-                                               cm.setActive('pdw_toggle', 1);\r
-                                               DOM.hide(id);\r
-                                               t._resizeIframe(ed, tbId, 26);\r
-                                       }\r
-                               }\r
-                       });\r
-               },\r
-               \r
-               // Resizes the iframe by a relative height value\r
-               _resizeIframe : function(ed, tb_id, dy) {\r
-                       var ifr = ed.getContentAreaContainer().firstChild;\r
-                       \r
-                       DOM.setStyle(ifr, 'height',DOM.getSize(ifr).h + dy); // Resize iframe\r
-                       ed.theme.deltaHeight += dy; // For resize cookie\r
-               },\r
-\r
-               /**\r
-                * Returns information about the plugin as a name/value array.\r
-                * The current keys are longname, author, authorurl, infourl and version.\r
-                *\r
-                * @return {Object} Name/value array containing information about the plugin.\r
-                */\r
-               getInfo : function() {\r
-                       return {\r
-                               longname : 'PDW Toggle Toolbars',\r
-                               author : 'Guido Neele',\r
-                               authorurl : 'http://www.neele.name/',\r
-                               infourl : 'http://www.neele.name/pdw_toggle_toolbars',\r
-                               version : "1.2"\r
-                       };\r
-               }\r
-       });\r
-\r
-       // Register plugin\r
-       tinymce.PluginManager.add('pdw', tinymce.plugins.pdw);\r
-})();\r
index 6a5fa6e..4304c6e 100644 (file)
@@ -318,10 +318,9 @@ class zip_archive extends file_archive {
         }
         if (substr($fileinfo->pathname, -9) === 'Thumbs.db') {
             $stream = $this->za->getStream($fileinfo->pathname);
-            $info = unpack('Nsiga/Nsigb', fread($stream, 8));
-            $signature = fread($stream, 8);
+            $info = base64_encode(fread($stream, 8));
             fclose($stream);
-            if ($info['siga'] === 0xd0cf11e0 && $info['sigb'] === 0xa1b11ae1) {
+            if ($info === '0M8R4KGxGuE=') {
                 // It's an OLE Compound File - so it's almost certainly a Windows thumbnail cache.
                 return true;
             }
index 528bf58..0c71af1 100644 (file)
@@ -464,22 +464,19 @@ function groups_get_course_groupmode($course) {
  * overrides activity setting if groupmodeforce enabled.
  *
  * @category group
- * @param cm_info $cm the course module object. Only the ->course and ->groupmode need to be set.
+ * @param cm_info|stdClass $cm the course module object. Only the ->course and ->groupmode need to be set.
  * @param stdClass $course object optional course object to improve perf
  * @return int group mode
  */
 function groups_get_activity_groupmode($cm, $course=null) {
-    global $COURSE, $DB;
-
-    // get course object (reuse COURSE if possible)
     if (isset($course->id) and $course->id == $cm->course) {
         //ok
-    } else if ($cm->course == $COURSE->id) {
-        $course = $COURSE;
+    } else if (isset($cm->coursegroupmode) && isset($cm->coursegroupmodeforce)) {
+        // This is an instance of cm_info (or clone) and already has the necessary course fields in it.
+        return empty($cm->coursegroupmodeforce) ? $cm->groupmode : $cm->coursegroupmode;
     } else {
-        if (!$course = $DB->get_record('course', array('id'=>$cm->course))) {
-            print_error('invalidcourseid');
-        }
+        // Get course object (reuse $COURSE if possible).
+        $course = get_course($cm->course, false);
     }
 
     return empty($course->groupmodeforce) ? $cm->groupmode : $course->groupmode;
index 998b896..efc172e 100644 (file)
@@ -30,6 +30,7 @@ define('NO_DEBUG_DISPLAY', true);
 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/minify.php");
 
 if ($slashargument = min_get_slash_argument()) {
     $slashargument = ltrim($slashargument, '/');
@@ -90,7 +91,7 @@ if ($rev > 0 and $rev < (time() + 60*60)) {
         js_send_cached($candidate, $etag);
 
     } else {
-        js_write_cache_file_content($candidate, js_minify($jsfiles));
+        js_write_cache_file_content($candidate, core_minify::js_files($jsfiles));
         // verify nothing failed in cache file creation
         clearstatcache();
         if (file_exists($candidate)) {
index bbfb895..d3114c1 100644 (file)
@@ -23,7 +23,7 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-//NOTE: do not verify MOODLE_INTERNAL here, this is used from themes too
+defined('MOODLE_INTERNAL') || die();
 
 /**
  * Send javascript file content with as much caching as possible
@@ -93,70 +93,6 @@ function js_send_unmodified($lastmodified, $etag) {
     die;
 }
 
-/**
- * Minify javascript files
- * @param array $files
- * @return string
- */
-function js_minify($files) {
-    // setup include path
-    set_include_path(__DIR__ . '/minify/lib' . PATH_SEPARATOR . get_include_path());
-    require_once('Minify.php');
-
-    if (empty($files)) {
-        return '';
-    }
-
-    if (0 === stripos(PHP_OS, 'win')) {
-        Minify::setDocRoot(); // IIS may need help
-    }
-    // disable all caching, we do it in moodle
-    Minify::setCache(null, false);
-
-    $options = array(
-        // JSMin is not GNU GPL compatible, use the plus version instead.
-        'minifiers' => array(Minify::TYPE_JS => array('JSMinPlus', 'minify')),
-        'bubbleCssImports' => false,
-        // Don't gzip content we just want text for storage
-        'encodeOutput' => false,
-        // Maximum age to cache, not used but required
-        'maxAge' => 1800,
-        // The files to minify
-        'files' => $files,
-        // Turn orr URI rewriting
-        'rewriteCssUris' => false,
-        // This returns the CSS rather than echoing it for display
-        'quiet' => true
-    );
-
-    $error = 'unknown';
-    try {
-        $result = Minify::serve('Files', $options);
-        if ($result['success']) {
-            return $result['content'];
-        }
-    } catch (Exception $e) {
-        $error = $e->getMessage();
-        $error = str_replace("\r", ' ', $error);
-        $error = str_replace("\n", ' ', $error);
-    }
-
-    // minification failed - try to inform the theme developer and include the non-minified version
-    $js = <<<EOD
-try {console.log('Error: Minimisation of javascript failed!');} catch (e) {}
-
-// Error: $error
-// Problem detected during javascript minimisation, please review the following code
-// =================================================================================
-
-
-EOD;
-    foreach ($files as $jsfile) {
-        $js .= file_get_contents($jsfile)."\n";
-    }
-    return $js;
-}
-
 /**
  * Create cache file for JS content
  * @param string $file full file path to cache file
index 90e557f..bf8fc20 100644 (file)
@@ -26,15 +26,19 @@ $min_uploaderHoursBehind = 0;
 $min_libPath = dirname(__FILE__) . '/lib';
 // do not change zlib compression or buffering here
 
-// TODO: locking setting, caching setting
-
 return; // end of moodle modification
 
 
 /**
  * Allow use of the Minify URI Builder app. Only set this to true while you need it.
- **/
-$min_enableBuilder = true;
+ */
+$min_enableBuilder = false;
+
+/**
+ * If non-empty, the Builder will be protected with HTTP Digest auth.
+ * The username is "admin".
+ */
+$min_builderPassword = 'admin';
 
 
 /**
@@ -124,15 +128,10 @@ $min_serveOptions['maxAge'] = 1800;
 
 
 /**
- * To use Google's Closure Compiler API (falling back to JSMin on failure),
- * uncomment the following lines:
+ * To use Google's Closure Compiler API to minify Javascript (falling back to JSMin
+ * on failure), uncomment the following line:
  */
-/*function closureCompiler($js) {
-    require_once 'Minify/JS/ClosureCompiler.php';
-    return Minify_JS_ClosureCompiler::minify($js);
-}
-$min_serveOptions['minifiers']['application/x-javascript'] = 'closureCompiler';
-//*/
+//$min_serveOptions['minifiers']['application/x-javascript'] = array('Minify_JS_ClosureCompiler', 'minify');
 
 
 /**
diff --git a/lib/minify/lib/CSSmin.php b/lib/minify/lib/CSSmin.php
new file mode 100644 (file)
index 0000000..a60b4ac
--- /dev/null
@@ -0,0 +1,758 @@
+<?php
+
+/*!
+ * cssmin.php rev ebaf67b 12/06/2013
+ * Author: Tubal Martin - http://tubalmartin.me/
+ * Repo: https://github.com/tubalmartin/YUI-CSS-compressor-PHP-port
+ *
+ * This is a PHP port of the CSS minification tool distributed with YUICompressor, 
+ * itself a port of the cssmin utility by Isaac Schlueter - http://foohack.com/
+ * Permission is hereby granted to use the PHP version under the same
+ * conditions as the YUICompressor.
+ */
+
+/*!
+ * YUI Compressor
+ * http://developer.yahoo.com/yui/compressor/
+ * Author: Julien Lecomte - http://www.julienlecomte.net/
+ * Copyright (c) 2013 Yahoo! Inc. All rights reserved.
+ * The copyrights embodied in the content of this file are licensed
+ * by Yahoo! Inc. under the BSD (revised) open source license.
+ */
+
+class CSSmin
+{
+    const NL = '___YUICSSMIN_PRESERVED_NL___';
+    const TOKEN = '___YUICSSMIN_PRESERVED_TOKEN_';
+    const COMMENT = '___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_';
+    const CLASSCOLON = '___YUICSSMIN_PSEUDOCLASSCOLON___';
+    const QUERY_FRACTION = '___YUICSSMIN_QUERY_FRACTION___';
+
+    private $comments;
+    private $preserved_tokens;
+    private $memory_limit;
+    private $max_execution_time;
+    private $pcre_backtrack_limit;
+    private $pcre_recursion_limit;
+    private $raise_php_limits;
+
+    /**
+     * @param bool|int $raise_php_limits
+     * If true, PHP settings will be raised if needed
+     */
+    public function __construct($raise_php_limits = TRUE)
+    {
+        // Set suggested PHP limits
+        $this->memory_limit = 128 * 1048576; // 128MB in bytes
+        $this->max_execution_time = 60; // 1 min
+        $this->pcre_backtrack_limit = 1000 * 1000;
+        $this->pcre_recursion_limit =  500 * 1000;
+
+        $this->raise_php_limits = (bool) $raise_php_limits;
+    }
+
+    /**
+     * Minify a string of CSS
+     * @param string $css
+     * @param int|bool $linebreak_pos
+     * @return string
+     */
+    public function run($css = '', $linebreak_pos = FALSE)
+    {
+        if (empty($css)) {
+            return '';
+        }
+
+        if ($this->raise_php_limits) {
+            $this->do_raise_php_limits();
+        }
+
+        $this->comments = array();
+        $this->preserved_tokens = array();
+
+        $start_index = 0;
+        $length = strlen($css);
+
+        $css = $this->extract_data_urls($css);
+
+        // collect all comment blocks...
+        while (($start_index = $this->index_of($css, '/*', $start_index)) >= 0) {
+            $end_index = $this->index_of($css, '*/', $start_index + 2);
+            if ($end_index < 0) {
+                $end_index = $length;
+            }
+            $comment_found = $this->str_slice($css, $start_index + 2, $end_index);
+            $this->comments[] = $comment_found;
+            $comment_preserve_string = self::COMMENT . (count($this->comments) - 1) . '___';
+            $css = $this->str_slice($css, 0, $start_index + 2) . $comment_preserve_string . $this->str_slice($css, $end_index);
+            // Set correct start_index: Fixes issue #2528130
+            $start_index = $end_index + 2 + strlen($comment_preserve_string) - strlen($comment_found);
+        }
+
+        // preserve strings so their content doesn't get accidentally minified
+        $css = preg_replace_callback('/(?:"(?:[^\\\\"]|\\\\.|\\\\)*")|'."(?:'(?:[^\\\\']|\\\\.|\\\\)*')/S", array($this, 'replace_string'), $css);
+
+        // Let's divide css code in chunks of 25.000 chars aprox.
+        // Reason: PHP's PCRE functions like preg_replace have a "backtrack limit"
+        // of 100.000 chars by default (php < 5.3.7) so if we're dealing with really
+        // long strings and a (sub)pattern matches a number of chars greater than
+        // the backtrack limit number (i.e. /(.*)/s) PCRE functions may fail silently
+        // returning NULL and $css would be empty.
+        $charset = '';
+        $charset_regexp = '/(@charset)( [^;]+;)/i';
+        $css_chunks = array();
+        $css_chunk_length = 25000; // aprox size, not exact
+        $start_index = 0;
+        $i = $css_chunk_length; // save initial iterations
+        $l = strlen($css);
+
+
+        // if the number of characters is 25000 or less, do not chunk
+        if ($l <= $css_chunk_length) {
+            $css_chunks[] = $css;
+        } else {
+            // chunk css code securely
+            while ($i < $l) {
+                $i += 50; // save iterations. 500 checks for a closing curly brace }
+                if ($l - $start_index <= $css_chunk_length || $i >= $l) {
+                    $css_chunks[] = $this->str_slice($css, $start_index);
+                    break;
+                }
+                if ($css[$i - 1] === '}' && $i - $start_index > $css_chunk_length) {
+                    // If there are two ending curly braces }} separated or not by spaces,
+                    // join them in the same chunk (i.e. @media blocks)
+                    $next_chunk = substr($css, $i);
+                    if (preg_match('/^\s*\}/', $next_chunk)) {
+                        $i = $i + $this->index_of($next_chunk, '}') + 1;
+                    }
+
+                    $css_chunks[] = $this->str_slice($css, $start_index, $i);
+                    $start_index = $i;
+                }
+            }
+        }
+
+        // Minify each chunk
+        for ($i = 0, $n = count($css_chunks); $i < $n; $i++) {
+            $css_chunks[$i] = $this->minify($css_chunks[$i], $linebreak_pos);
+            // Keep the first @charset at-rule found
+            if (empty($charset) && preg_match($charset_regexp, $css_chunks[$i], $matches)) {
+                $charset = strtolower($matches[1]) . $matches[2];
+            }
+            // Delete all @charset at-rules
+            $css_chunks[$i] = preg_replace($charset_regexp, '', $css_chunks[$i]);
+        }
+
+        // Update the first chunk and push the charset to the top of the file.
+        $css_chunks[0] = $charset . $css_chunks[0];
+
+        return implode('', $css_chunks);
+    }
+
+    /**
+     * Sets the memory limit for this script
+     * @param int|string $limit
+     */
+    public function set_memory_limit($limit)
+    {
+        $this->memory_limit = $this->normalize_int($limit);
+    }
+
+    /**
+     * Sets the maximum execution time for this script
+     * @param int|string $seconds
+     */
+    public function set_max_execution_time($seconds)
+    {
+        $this->max_execution_time = (int) $seconds;
+    }
+
+    /**
+     * Sets the PCRE backtrack limit for this script
+     * @param int $limit
+     */
+    public function set_pcre_backtrack_limit($limit)
+    {
+        $this->pcre_backtrack_limit = (int) $limit;
+    }
+
+    /**
+     * Sets the PCRE recursion limit for this script
+     * @param int $limit
+     */
+    public function set_pcre_recursion_limit($limit)
+    {
+        $this->pcre_recursion_limit = (int) $limit;
+    }
+
+    /**
+     * Try to configure PHP to use at least the suggested minimum settings
+     */
+    private function do_raise_php_limits()
+    {
+        $php_limits = array(
+            'memory_limit' => $this->memory_limit,
+            'max_execution_time' => $this->max_execution_time,
+            'pcre.backtrack_limit' => $this->pcre_backtrack_limit,
+            'pcre.recursion_limit' =>  $this->pcre_recursion_limit
+        );
+
+        // If current settings are higher respect them.
+        foreach ($php_limits as $name => $suggested) {
+            $current = $this->normalize_int(ini_get($name));
+            // memory_limit exception: allow -1 for "no memory limit".
+            if ($current > -1 && ($suggested == -1 || $current < $suggested)) {
+                ini_set($name, $suggested);
+            }
+        }
+    }
+
+    /**
+     * Does bulk of the minification
+     * @param string $css
+     * @param int|bool $linebreak_pos
+     * @return string
+     */
+    private function minify($css, $linebreak_pos)
+    {
+        // strings are safe, now wrestle the comments
+        for ($i = 0, $max = count($this->comments); $i < $max; $i++) {
+
+            $token = $this->comments[$i];
+            $placeholder = '/' . self::COMMENT . $i . '___/';
+
+            // ! in the first position of the comment means preserve
+            // so push to the preserved tokens keeping the !
+            if (substr($token, 0, 1) === '!') {
+                $this->preserved_tokens[] = $token;
+                $token_tring = self::TOKEN . (count($this->preserved_tokens) - 1) . '___';
+                $css = preg_replace($placeholder, $token_tring, $css, 1);
+                // Preserve new lines for /*! important comments
+                $css = preg_replace('/\s*[\n\r\f]+\s*(\/\*'. $token_tring .')/S', self::NL.'$1', $css);
+                $css = preg_replace('/('. $token_tring .'\*\/)\s*[\n\r\f]+\s*/', '$1'.self::NL, $css);
+                continue;
+            }
+
+            // \ in the last position looks like hack for Mac/IE5
+            // shorten that to /*\*/ and the next one to /**/
+            if (substr($token, (strlen($token) - 1), 1) === '\\') {
+                $this->preserved_tokens[] = '\\';
+                $css = preg_replace($placeholder,  self::TOKEN . (count($this->preserved_tokens) - 1) . '___', $css, 1);
+                $i = $i + 1; // attn: advancing the loop
+                $this->preserved_tokens[] = '';
+                $css = preg_replace('/' . self::COMMENT . $i . '___/',  self::TOKEN . (count($this->preserved_tokens) - 1) . '___', $css, 1);
+                continue;
+            }
+
+            // keep empty comments after child selectors (IE7 hack)
+            // e.g. html >/**/ body
+            if (strlen($token) === 0) {
+                $start_index = $this->index_of($css, $this->str_slice($placeholder, 1, -1));
+                if ($start_index > 2) {
+                    if (substr($css, $start_index - 3, 1) === '>') {
+                        $this->preserved_tokens[] = '';
+                        $css = preg_replace($placeholder,  self::TOKEN . (count($this->preserved_tokens) - 1) . '___', $css, 1);
+                    }
+                }
+            }
+
+            // in all other cases kill the comment
+            $css = preg_replace('/\/\*' . $this->str_slice($placeholder, 1, -1) . '\*\//', '', $css, 1);
+        }
+
+
+        // Normalize all whitespace strings to single spaces. Easier to work with that way.
+        $css = preg_replace('/\s+/', ' ', $css);
+
+        // Shorten & preserve calculations calc(...) since spaces are important
+        $css = preg_replace_callback('/calc(\(((?:[^\(\)]+|(?1))*)\))/i', array($this, 'replace_calc'), $css);
+
+        // Replace positive sign from numbers preceded by : or a white-space before the leading space is removed
+        // +1.2em to 1.2em, +.8px to .8px, +2% to 2%
+        $css = preg_replace('/((?<!\\\\)\:|\s)\+(\.?\d+)/S', '$1$2', $css);
+
+        // Remove leading zeros from integer and float numbers preceded by : or a white-space
+        // 000.6 to .6, -0.8 to -.8, 0050 to 50, -01.05 to -1.05
+        $css = preg_replace('/((?<!\\\\)\:|\s)(\-?)0+(\.?\d+)/S', '$1$2$3', $css);
+
+        // Remove trailing zeros from float numbers preceded by : or a white-space
+        // -6.0100em to -6.01em, .0100 to .01, 1.200px to 1.2px
+        $css = preg_replace('/((?<!\\\\)\:|\s)(\-?)(\d?\.\d+?)0+([^\d])/S', '$1$2$3$4', $css);
+
+        // Remove trailing .0 -> -9.0 to -9
+        $css = preg_replace('/((?<!\\\\)\:|\s)(\-?\d+)\.0([^\d])/S', '$1$2$3', $css);
+
+        // Replace 0 length numbers with 0
+        $css = preg_replace('/((?<!\\\\)\:|\s)\-?\.?0+([^\d])/S', '${1}0$2', $css);
+
+        // Remove the spaces before the things that should not have spaces before them.
+        // But, be careful not to turn "p :link {...}" into "p:link{...}"
+        // Swap out any pseudo-class colons with the token, and then swap back.
+        $css = preg_replace_callback('/(?:^|\})(?:(?:[^\{\:])+\:)+(?:[^\{]*\{)/', array($this, 'replace_colon'), $css);
+        
+        // Remove spaces before the things that should not have spaces before them.
+        $css = preg_replace('/\s+([\!\{\}\;\:\>\+\(\)\]\~\=,])/', '$1', $css);
+
+        // Restore spaces for !important
+        $css = preg_replace('/\!important/i', ' !important', $css);
+
+        // bring back the colon
+        $css = preg_replace('/' . self::CLASSCOLON . '/', ':', $css);
+
+        // retain space for special IE6 cases
+        $css = preg_replace_callback('/\:first\-(line|letter)(\{|,)/i', array($this, 'lowercase_pseudo_first'), $css);
+
+        // no space after the end of a preserved comment
+        $css = preg_replace('/\*\/ /', '*/', $css);
+
+        // lowercase some popular @directives
+        $css = preg_replace_callback('/@(font-face|import|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?keyframe|media|page|namespace)/i', array($this, 'lowercase_directives'), $css);
+
+        // lowercase some more common pseudo-elements
+        $css = preg_replace_callback('/:(active|after|before|checked|disabled|empty|enabled|first-(?:child|of-type)|focus|hover|last-(?:child|of-type)|link|only-(?:child|of-type)|root|:selection|target|visited)/i', array($this, 'lowercase_pseudo_elements'), $css);
+
+        // lowercase some more common functions
+        $css = preg_replace_callback('/:(lang|not|nth-child|nth-last-child|nth-last-of-type|nth-of-type|(?:-(?:moz|webkit)-)?any)\(/i', array($this, 'lowercase_common_functions'), $css);
+
+        // lower case some common function that can be values
+        // NOTE: rgb() isn't useful as we replace with #hex later, as well as and() is already done for us
+        $css = preg_replace_callback('/([:,\( ]\s*)(attr|color-stop|from|rgba|to|url|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?(?:calc|max|min|(?:repeating-)?(?:linear|radial)-gradient)|-webkit-gradient)/iS', array($this, 'lowercase_common_functions_values'), $css);
+        
+        // Put the space back in some cases, to support stuff like
+        // @media screen and (-webkit-min-device-pixel-ratio:0){
+        $css = preg_replace('/\band\(/i', 'and (', $css);
+
+        // Remove the spaces after the things that should not have spaces after them.
+        $css = preg_replace('/([\!\{\}\:;\>\+\(\[\~\=,])\s+/S', '$1', $css);
+
+        // remove unnecessary semicolons
+        $css = preg_replace('/;+\}/', '}', $css);
+
+        // Fix for issue: #2528146
+        // Restore semicolon if the last property is prefixed with a `*` (lte IE7 hack)
+        // to avoid issues on Symbian S60 3.x browsers.
+        $css = preg_replace('/(\*[a-z0-9\-]+\s*\:[^;\}]+)(\})/', '$1;$2', $css);
+
+        // Replace 0 length units 0(px,em,%) with 0.
+        $css = preg_replace('/(^|[^0-9])(?:0?\.)?0(?:em|ex|ch|rem|vw|vh|vm|vmin|cm|mm|in|px|pt|pc|%|deg|g?rad|m?s|k?hz)/iS', '${1}0', $css);
+
+        // Replace 0 0; or 0 0 0; or 0 0 0 0; with 0.
+        $css = preg_replace('/\:0(?: 0){1,3}(;|\}| \!)/', ':0$1', $css);
+
+        // Fix for issue: #2528142
+        // Replace text-shadow:0; with text-shadow:0 0 0;
+        $css = preg_replace('/(text-shadow\:0)(;|\}| \!)/i', '$1 0 0$2', $css);
+
+        // Replace background-position:0; with background-position:0 0;
+        // same for transform-origin
+        // Changing -webkit-mask-position: 0 0 to just a single 0 will result in the second parameter defaulting to 50% (center)
+        $css = preg_replace('/(background\-position|webkit-mask-position|(?:webkit|moz|o|ms|)\-?transform\-origin)\:0(;|\}| \!)/iS', '$1:0 0$2', $css);
+
+        // Shorten colors from rgb(51,102,153) to #336699, rgb(100%,0%,0%) to #ff0000 (sRGB color space)
+        // Shorten colors from hsl(0, 100%, 50%) to #ff0000 (sRGB color space)
+        // This makes it more likely that it'll get further compressed in the next step.
+        $css = preg_replace_callback('/rgb\s*\(\s*([0-9,\s\-\.\%]+)\s*\)(.{1})/i', array($this, 'rgb_to_hex'), $css);
+        $css = preg_replace_callback('/hsl\s*\(\s*([0-9,\s\-\.\%]+)\s*\)(.{1})/i', array($this, 'hsl_to_hex'), $css);
+
+        // Shorten colors from #AABBCC to #ABC or short color name.
+        $css = $this->compress_hex_colors($css);
+
+        // border: none to border:0, outline: none to outline:0
+        $css = preg_replace('/(border\-?(?:top|right|bottom|left|)|outline)\:none(;|\}| \!)/iS', '$1:0$2', $css);
+
+        // shorter opacity IE filter
+        $css = preg_replace('/progid\:DXImageTransform\.Microsoft\.Alpha\(Opacity\=/i', 'alpha(opacity=', $css);
+
+        // Find a fraction that is used for Opera's -o-device-pixel-ratio query
+        // Add token to add the "\" back in later
+        $css = preg_replace('/\(([a-z\-]+):([0-9]+)\/([0-9]+)\)/i', '($1:$2'. self::QUERY_FRACTION .'$3)', $css);
+
+        // Remove empty rules.
+        $css = preg_replace('/[^\};\{\/]+\{\}/S', '', $css);
+
+        // Add "/" back to fix Opera -o-device-pixel-ratio query
+        $css = preg_replace('/'. self::QUERY_FRACTION .'/', '/', $css);
+
+        // Some source control tools don't like it when files containing lines longer
+        // than, say 8000 characters, are checked in. The linebreak option is used in
+        // that case to split long lines after a specific column.
+        if ($linebreak_pos !== FALSE && (int) $linebreak_pos >= 0) {
+            $linebreak_pos = (int) $linebreak_pos;
+            $start_index = $i = 0;
+            while ($i < strlen($css)) {
+                $i++;
+                if ($css[$i - 1] === '}' && $i - $start_index > $linebreak_pos) {
+                    $css = $this->str_slice($css, 0, $i) . "\n" . $this->str_slice($css, $i);
+                    $start_index = $i;
+                }
+            }
+        }
+
+        // Replace multiple semi-colons in a row by a single one
+        // See SF bug #1980989
+        $css = preg_replace('/;;+/', ';', $css);
+
+        // Restore new lines for /*! important comments
+        $css = preg_replace('/'. self::NL .'/', "\n", $css);
+
+        // Lowercase all uppercase properties
+        $css = preg_replace_callback('/(\{|\;)([A-Z\-]+)(\:)/', array($this, 'lowercase_properties'), $css);
+
+        // restore preserved comments and strings
+        for ($i = 0, $max = count($this->preserved_tokens); $i < $max; $i++) {
+            $css = preg_replace('/' . self::TOKEN . $i . '___/', $this->preserved_tokens[$i], $css, 1);
+        }
+
+        // Trim the final string (for any leading or trailing white spaces)
+        return trim($css);
+    }
+
+    /**
+     * Utility method to replace all data urls with tokens before we start
+     * compressing, to avoid performance issues running some of the subsequent
+     * regexes against large strings chunks.
+     *
+     * @param string $css
+     * @return string
+     */
+    private function extract_data_urls($css)
+    {
+        // Leave data urls alone to increase parse performance.
+        $max_index = strlen($css) - 1;
+        $append_index = $index = $last_index = $offset = 0;
+        $sb = array();
+        $pattern = '/url\(\s*(["\']?)data\:/i';
+
+        // Since we need to account for non-base64 data urls, we need to handle
+        // ' and ) being part of the data string. Hence switching to indexOf,
+        // to determine whether or not we have matching string terminators and
+        // handling sb appends directly, instead of using matcher.append* methods.
+
+        while (preg_match($pattern, $css, $m, 0, $offset)) {
+            $index = $this->index_of($css, $m[0], $offset);
+            $last_index = $index + strlen($m[0]);
+            $start_index = $index + 4; // "url(".length()
+            $end_index = $last_index - 1;
+            $terminator = $m[1]; // ', " or empty (not quoted)
+            $found_terminator = FALSE;
+
+            if (strlen($terminator) === 0) {
+                $terminator = ')';
+            }
+
+            while ($found_terminator === FALSE && $end_index+1 <= $max_index) {
+                $end_index = $this->index_of($css, $terminator, $end_index + 1);
+
+                // endIndex == 0 doesn't really apply here
+                if ($end_index > 0 && substr($css, $end_index - 1, 1) !== '\\') {
+                    $found_terminator = TRUE;
+                    if (')' != $terminator) {
+                        $end_index = $this->index_of($css, ')', $end_index);
+                    }
+                }
+            }
+
+            // Enough searching, start moving stuff over to the buffer
+            $sb[] = $this->str_slice($css, $append_index, $index);
+
+            if ($found_terminator) {
+                $token = $this->str_slice($css, $start_index, $end_index);
+                $token = preg_replace('/\s+/', '', $token);
+                $this->preserved_tokens[] = $token;
+
+                $preserver = 'url(' . self::TOKEN . (count($this->preserved_tokens) - 1) . '___)';
+                $sb[] = $preserver;
+
+                $append_index = $end_index + 1;
+            } else {
+                // No end terminator found, re-add the whole match. Should we throw/warn here?
+                $sb[] = $this->str_slice($css, $index, $last_index);
+                $append_index = $last_index;
+            }
+
+            $offset = $last_index;
+        }
+
+        $sb[] = $this->str_slice($css, $append_index);
+
+        return implode('', $sb);
+    }
+
+    /**
+     * Utility method to compress hex color values of the form #AABBCC to #ABC or short color name.
+     *
+     * DOES NOT compress CSS ID selectors which match the above pattern (which would break things).
+     * e.g. #AddressForm { ... }
+     *
+     * DOES NOT compress IE filters, which have hex color values (which would break things).
+     * e.g. filter: chroma(color="#FFFFFF");
+     *
+     * DOES NOT compress invalid hex values.
+     * e.g. background-color: #aabbccdd
+     *
+     * @param string $css
+     * @return string
+     */
+    private function compress_hex_colors($css)
+    {
+        // Look for hex colors inside { ... } (to avoid IDs) and which don't have a =, or a " in front of them (to avoid filters)
+        $pattern = '/(\=\s*?["\']?)?#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])(\}|[^0-9a-f{][^{]*?\})/iS';
+        $_index = $index = $last_index = $offset = 0;
+        $sb = array();
+        // See: http://ajaxmin.codeplex.com/wikipage?title=CSS%20Colors
+        $short_safe = array(
+            '#808080' => 'gray',
+            '#008000' => 'green',
+            '#800000' => 'maroon',
+            '#000080' => 'navy',
+            '#808000' => 'olive',
+            '#ffa500' => 'orange',
+            '#800080' => 'purple',
+            '#c0c0c0' => 'silver',
+            '#008080' => 'teal',
+            '#f00' => 'red'
+        );
+
+        while (preg_match($pattern, $css, $m, 0, $offset)) {
+            $index = $this->index_of($css, $m[0], $offset);
+            $last_index = $index + strlen($m[0]);
+            $is_filter = $m[1] !== null && $m[1] !== '';
+
+            $sb[] = $this->str_slice($css, $_index, $index);
+
+            if ($is_filter) {
+                // Restore, maintain case, otherwise filter will break
+                $sb[] = $m[1] . '#' . $m[2] . $m[3] . $m[4] . $m[5] . $m[6] . $m[7];
+            } else {
+                if (strtolower($m[2]) == strtolower($m[3]) &&
+                    strtolower($m[4]) == strtolower($m[5]) &&
+                    strtolower($m[6]) == strtolower($m[7])) {
+                    // Compress.
+                    $hex = '#' . strtolower($m[3] . $m[5] . $m[7]);
+                } else {
+                    // Non compressible color, restore but lower case.
+                    $hex = '#' . strtolower($m[2] . $m[3] . $m[4] . $m[5] . $m[6] . $m[7]);
+                }
+                // replace Hex colors to short safe color names
+                $sb[] = array_key_exists($hex, $short_safe) ? $short_safe[$hex] : $hex;
+            }
+
+            $_index = $offset = $last_index - strlen($m[8]);
+        }
+
+        $sb[] = $this->str_slice($css, $_index);
+
+        return implode('', $sb);
+    }
+
+    /* CALLBACKS
+     * ---------------------------------------------------------------------------------------------
+     */
+
+    private function replace_string($matches)
+    {
+        $match = $matches[0];
+        $quote = substr($match, 0, 1);
+        // Must use addcslashes in PHP to avoid parsing of backslashes
+        $match = addcslashes($this->str_slice($match, 1, -1), '\\');
+
+        // maybe the string contains a comment-like substring?
+        // one, maybe more? put'em back then
+        if (($pos = $this->index_of($match, self::COMMENT)) >= 0) {
+            for ($i = 0, $max = count($this->comments); $i < $max; $i++) {
+                $match = preg_replace('/' . self::COMMENT . $i . '___/', $this->comments[$i], $match, 1);
+            }
+        }
+
+        // minify alpha opacity in filter strings
+        $match = preg_replace('/progid\:DXImageTransform\.Microsoft\.Alpha\(Opacity\=/i', 'alpha(opacity=', $match);
+
+        $this->preserved_tokens[] = $match;
+        return $quote . self::TOKEN . (count($this->preserved_tokens) - 1) . '___' . $quote;
+    }
+
+    private function replace_colon($matches)
+    {
+        return preg_replace('/\:/', self::CLASSCOLON, $matches[0]);
+    }
+
+    private function replace_calc($matches)
+    {
+        $this->preserved_tokens[] = trim(preg_replace('/\s*([\*\/\(\),])\s*/', '$1', $matches[2]));
+        return 'calc('. self::TOKEN . (count($this->preserved_tokens) - 1) . '___' . ')';
+    }
+
+    private function rgb_to_hex($matches)
+    {
+        // Support for percentage values rgb(100%, 0%, 45%);
+        if ($this->index_of($matches[1], '%') >= 0){
+            $rgbcolors = explode(',', str_replace('%', '', $matches[1]));
+            for ($i = 0; $i < count($rgbcolors); $i++) {
+                $rgbcolors[$i] = $this->round_number(floatval($rgbcolors[$i]) * 2.55);
+            }
+        } else {
+            $rgbcolors = explode(',', $matches[1]);
+        }
+
+        // Values outside the sRGB color space should be clipped (0-255)
+        for ($i = 0; $i < count($rgbcolors); $i++) {
+            $rgbcolors[$i] = $this->clamp_number(intval($rgbcolors[$i], 10), 0, 255);
+            $rgbcolors[$i] = sprintf("%02x", $rgbcolors[$i]);
+        }
+
+        // Fix for issue #2528093
+        if (!preg_match('/[\s\,\);\}]/', $matches[2])){
+            $matches[2] = ' ' . $matches[2];
+        }
+
+        return '#' . implode('', $rgbcolors) . $matches[2];
+    }
+
+    private function hsl_to_hex($matches)
+    {
+        $values = explode(',', str_replace('%', '', $matches[1]));
+        $h = floatval($values[0]);
+        $s = floatval($values[1]);
+        $l = floatval($values[2]);
+
+        // Wrap and clamp, then fraction!
+        $h = ((($h % 360) + 360) % 360) / 360;
+        $s = $this->clamp_number($s, 0, 100) / 100;
+        $l = $this->clamp_number($l, 0, 100) / 100;
+
+        if ($s == 0) {
+            $r = $g = $b = $this->round_number(255 * $l);
+        } else {
+            $v2 = $l < 0.5 ? $l * (1 + $s) : ($l + $s) - ($s * $l);
+            $v1 = (2 * $l) - $v2;
+            $r = $this->round_number(255 * $this->hue_to_rgb($v1, $v2, $h + (1/3)));
+            $g = $this->round_number(255 * $this->hue_to_rgb($v1, $v2, $h));
+            $b = $this->round_number(255 * $this->hue_to_rgb($v1, $v2, $h - (1/3)));
+        }
+
+        return $this->rgb_to_hex(array('', $r.','.$g.','.$b, $matches[2]));
+ &