Merge branch 'MDL-67075-redis-session-locks' of https://github.com/brendanheywood...
authorAndrew Nicols <andrew@nicols.co.uk>
Thu, 30 Apr 2020 08:05:05 +0000 (16:05 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Thu, 30 Apr 2020 08:05:05 +0000 (16:05 +0800)
223 files changed:
.eslintignore
.stylelintignore
admin/renderer.php
admin/settings/development.php
admin/tool/messageinbound/lang/en/tool_messageinbound.php
admin/tool/recyclebin/tests/behat/basic_functionality.feature
admin/tool/task/lang/en/tool_task.php
admin/tool/xmldb/lang/en/tool_xmldb.php
blocks/site_main_menu/tests/behat/add_url.feature
cache/classes/helper.php
cache/classes/loaders.php
cache/classes/store.php
cache/tests/cache_test.php
cache/upgrade.txt
composer.json
composer.lock
contentbank/classes/content.php
contentbank/classes/contentbank.php
contentbank/contenttype/h5p/lang/en/contenttype_h5p.php
contentbank/contenttype/h5p/tests/behat/admin_upload_content.feature
contentbank/index.php
contentbank/templates/bankcontent.mustache
contentbank/templates/toolbar.mustache
contentbank/tests/behat/delete_content.feature
contentbank/tests/contentbank_test.php
course/classes/management/helper.php
course/tests/behat/activity_chooser.feature
grade/grading/form/guide/lang/en/gradingform_guide.php
grade/grading/form/rubric/renderer.php
grade/grading/form/rubric/styles.css
h5p/h5plib/v124/lang/en/h5plib_v124.php
install/lang/ps/langconfig.php
lang/en/admin.php
lang/en/cache.php
lang/en/contentbank.php
lang/en/course.php
lang/en/enrol.php
lang/en/error.php
lang/en/h5p.php
lang/en/moodle.php
lang/en/repository.php
lang/en/role.php
lib/amd/build/chart_base.min.js
lib/amd/build/chart_base.min.js.map
lib/amd/build/chart_output_chartjs.min.js
lib/amd/build/chart_output_chartjs.min.js.map
lib/amd/build/chart_series.min.js
lib/amd/build/chart_series.min.js.map
lib/amd/build/custom_interaction_events.min.js
lib/amd/build/custom_interaction_events.min.js.map
lib/amd/src/chart_base.js
lib/amd/src/chart_output_chartjs.js
lib/amd/src/chart_series.js
lib/amd/src/custom_interaction_events.js
lib/antivirus/clamav/lang/en/antivirus_clamav.php
lib/behat/classes/behat_config_util.php
lib/behat/lib.php
lib/classes/chart_base.php
lib/classes/chart_series.php
lib/classes/component.php
lib/classes/plugin_manager.php
lib/classes/task/manager.php
lib/classes/task/messaging_cleanup_task.php
lib/db/upgrade.php
lib/filelib.php
lib/filestorage/file_storage.php
lib/filestorage/file_system.php
lib/ldaplib.php
lib/moodlelib.php
lib/outputrenderers.php
lib/plist/LICENSE [new file with mode: 0644]
lib/plist/README.md [new file with mode: 0644]
lib/plist/classes/CFPropertyList/CFBinaryPropertyList.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/CFPropertyList.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/CFType.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/CFTypeDetector.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/IOException.php [new file with mode: 0644]
lib/plist/classes/CFPropertyList/PListException.php [new file with mode: 0644]
lib/plist/readme_moodle.txt [new file with mode: 0644]
lib/table/amd/build/dynamic.min.js
lib/table/amd/build/dynamic.min.js.map
lib/table/amd/build/local/dynamic/repository.min.js
lib/table/amd/build/local/dynamic/repository.min.js.map
lib/table/amd/build/local/dynamic/selectors.min.js
lib/table/amd/build/local/dynamic/selectors.min.js.map
lib/table/amd/src/dynamic.js
lib/table/amd/src/local/dynamic/repository.js
lib/table/amd/src/local/dynamic/selectors.js
lib/table/classes/external/dynamic/fetch.php
lib/table/tests/external/dynamic/fetch_test.php
lib/tablelib.php
lib/tcpdf/readme_moodle.txt
lib/tcpdf/tcpdf.php
lib/templates/single_select.mustache
lib/templates/url_select.mustache
lib/tests/other/chartjstestpage.php
lib/thirdpartylibs.xml
lib/typo3/class.t3lib_cs.php
lib/typo3/readme_moodle.txt
lib/upgrade.txt
message/amd/build/message_preferences.min.js [new file with mode: 0644]
message/amd/build/message_preferences.min.js.map [new file with mode: 0644]
message/amd/src/message_preferences.js [new file with mode: 0644]
message/output/airnotifier/lang/en/message_airnotifier.php
message/output/lib.php
message/output/popup/db/upgrade.php
message/output/popup/message_output_popup.php
message/output/popup/tests/messaging_cleanup_test.php [new file with mode: 0644]
message/output/popup/version.php
message/templates/message_preferences.mustache [new file with mode: 0644]
message/templates/message_preferences_component.mustache [new file with mode: 0644]
message/templates/message_preferences_notification_processor.mustache [new file with mode: 0644]
message/tests/behat/message_preferences.feature [new file with mode: 0644]
message/upgrade.txt
mod/assign/lang/en/assign.php
mod/forum/report/summary/templates/bulk_action_menu.mustache
mod/h5pactivity/lang/en/h5pactivity.php
mod/h5pactivity/pix/icon.png
mod/h5pactivity/pix/icon.svg
mod/h5pactivity/tests/behat/add_h5pactivity.feature
mod/h5pactivity/tests/behat/sending_attempt.feature
mod/h5pactivity/tests/privacy_test.php
mod/lesson/lang/en/lesson.php
mod/lti/lang/en/lti.php
mod/quiz/accessrule/safebrowser/rule.php [deleted file]
mod/quiz/accessrule/safebrowser/tests/rule_test.php [deleted file]
mod/quiz/accessrule/seb/amd/build/managetemplates.min.js [new file with mode: 0644]
mod/quiz/accessrule/seb/amd/build/managetemplates.min.js.map [new file with mode: 0644]
mod/quiz/accessrule/seb/amd/src/managetemplates.js [new file with mode: 0644]
mod/quiz/accessrule/seb/backup/moodle2/backup_quizaccess_seb_subplugin.class.php [new file with mode: 0644]
mod/quiz/accessrule/seb/backup/moodle2/restore_quizaccess_seb_subplugin.class.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/access_manager.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/config_key.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/access_prevented.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/template_created.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/template_deleted.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/template_disabled.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/template_enabled.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/event/template_updated.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/helper.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/hideif_rule.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/link_generator.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/local/form/template.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/local/table/template_list.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/privacy/provider.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/property_list.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/quiz_settings.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/settings_provider.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/template.php [new file with mode: 0644]
mod/quiz/accessrule/seb/classes/template_controller.php [new file with mode: 0644]
mod/quiz/accessrule/seb/config.php [new file with mode: 0644]
mod/quiz/accessrule/seb/db/access.php [new file with mode: 0644]
mod/quiz/accessrule/seb/db/caches.php [new file with mode: 0644]
mod/quiz/accessrule/seb/db/install.php [new file with mode: 0644]
mod/quiz/accessrule/seb/db/install.xml [new file with mode: 0644]
mod/quiz/accessrule/seb/db/upgrade.php [moved from mod/quiz/accessrule/safebrowser/lang/en/quizaccess_safebrowser.php with 53% similarity]
mod/quiz/accessrule/seb/lang/en/quizaccess_seb.php [new file with mode: 0644]
mod/quiz/accessrule/seb/lib.php [new file with mode: 0644]
mod/quiz/accessrule/seb/rule.php [new file with mode: 0644]
mod/quiz/accessrule/seb/settings.php [new file with mode: 0644]
mod/quiz/accessrule/seb/template.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/behat/edit_form.feature [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/generator/behat_quizaccess_seb_generator.php [moved from mod/quiz/accessrule/safebrowser/classes/privacy/provider.php with 53% similarity]
mod/quiz/accessrule/seb/tests/generator/lib.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/access_manager_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/backup_restore_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/base.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/config_key_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/event_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/helper_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/hideif_rule_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/link_generator_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/privacy_provider_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/property_list_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/quiz_settings_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/rule_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/JSON_unencrypted_mac_001.txt [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/encrypted.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/simpleunencrypted.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/simpleunencryptedwithoutoriginator.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/unencrypted.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/unencrypted_mac_001.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/sample_data/unencrypted_win_223.seb [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/settings_provider_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/tests/phpunit/template_test.php [new file with mode: 0644]
mod/quiz/accessrule/seb/version.php [moved from mod/quiz/accessrule/safebrowser/version.php with 68% similarity]
mod/quiz/accessrule/timelimit/lang/en/quizaccess_timelimit.php
mod/quiz/backup/moodle2/restore_quiz_stepslib.php
mod/quiz/lang/en/quiz.php
mod/quiz/report/responses/tests/fixtures/questions00.csv
mod/quiz/tests/attempt_walkthrough_from_csv_test.php
mod/quiz/tests/behat/attempt_begin.feature
mod/workshop/tests/behat/file_type_restriction.feature
question/type/calculated/questiontype.php
question/type/calculated/tests/questiontype_test.php
question/type/ddimageortext/lang/en/qtype_ddimageortext.php
question/type/ddmarker/lang/en/qtype_ddmarker.php
question/type/multianswer/questiontype.php
question/type/multianswer/tests/questiontype_test.php
question/type/numerical/db/upgradelib.php
question/type/questiontypebase.php
question/type/random/questiontype.php
question/type/random/tests/questiontype_test.php
question/type/truefalse/questiontype.php
question/type/truefalse/tests/questiontype_test.php
theme/boost/scss/moodle/contentbank.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/grade.scss
theme/boost/scss/preset/default.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css
user/amd/build/participants.min.js
user/amd/build/participants.min.js.map
user/amd/build/repository.min.js [new file with mode: 0644]
user/amd/build/repository.min.js.map [new file with mode: 0644]
user/amd/build/status_field.min.js
user/amd/build/status_field.min.js.map
user/amd/src/participants.js
user/amd/src/repository.js [new file with mode: 0644]
user/amd/src/status_field.js
user/classes/table/participants.php
user/tests/behat/course_preference.feature
version.php

index 2ef8a38..b9c0b6a 100644 (file)
@@ -65,6 +65,7 @@ lib/php-jwt/
 lib/babel-polyfill/
 lib/polyfills/
 lib/emoji-data/
+lib/plist/
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
 media/player/videojs/videojs/
index f00e696..5d9e5c1 100644 (file)
@@ -66,6 +66,7 @@ lib/php-jwt/
 lib/babel-polyfill/
 lib/polyfills/
 lib/emoji-data/
+lib/plist/
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
 media/player/videojs/videojs/
index f904352..5931a26 100644 (file)
@@ -813,7 +813,7 @@ class core_admin_renderer extends plugin_renderer_base {
             }
         }
 
-        $updateinfo .= $this->container_start('checkforupdates');
+        $updateinfo .= $this->container_start('checkforupdates mt-1');
         $fetchurl = new moodle_url('/admin/index.php', array('fetchupdates' => 1, 'sesskey' => sesskey(), 'cache' => 0));
         $updateinfo .= $this->single_button($fetchurl, get_string('checkforupdates', 'core_plugin'));
         if ($fetch) {
@@ -902,7 +902,7 @@ class core_admin_renderer extends plugin_renderer_base {
      */
     protected function moodle_available_update_info(\core\update\info $updateinfo) {
 
-        $boxclasses = 'moodleupdateinfo';
+        $boxclasses = 'moodleupdateinfo mb-2';
         $info = array();
 
         if (isset($updateinfo->release)) {
@@ -922,7 +922,8 @@ class core_admin_renderer extends plugin_renderer_base {
         }
 
         if (isset($updateinfo->download)) {
-            $info[] = html_writer::link($updateinfo->download, get_string('download'), array('class' => 'info download'));
+            $info[] = html_writer::link($updateinfo->download, get_string('download'),
+                array('class' => 'info download btn btn-secondary'));
         }
 
         if (isset($updateinfo->url)) {
@@ -930,9 +931,9 @@ class core_admin_renderer extends plugin_renderer_base {
                 array('class' => 'info more'));
         }
 
-        $box  = $this->output->box_start($boxclasses);
-        $box .= $this->output->box(implode(html_writer::tag('span', ' ', array('class' => 'separator')), $info), '');
-        $box .= $this->output->box_end();
+        $box  = $this->output->container_start($boxclasses);
+        $box .= $this->output->container(implode(html_writer::tag('span', ' | ', array('class' => 'separator')), $info), '');
+        $box .= $this->output->container_end();
 
         return $box;
     }
index f8a5ceb..46206fc 100644 (file)
@@ -10,7 +10,6 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $temp = new admin_settingpage('experimentalsettings', new lang_string('experimentalsettings', 'admin'));
     //TODO: Re-enable cc-import once re-implemented in 2.0.x
     //$temp->add(new admin_setting_configcheckbox('enableimsccimport', new lang_string('enable_cc_import', 'imscc'), new lang_string('enable_cc_import_description', 'imscc'), 0));
-    $temp->add(new admin_setting_configcheckbox('enablesafebrowserintegration', new lang_string('enablesafebrowserintegration', 'admin'), new lang_string('configenablesafebrowserintegration', 'admin'), 0));
 
     $temp->add(new admin_setting_configcheckbox('dndallowtextandlinks', new lang_string('dndallowtextandlinks', 'admin'), new lang_string('configdndallowtextandlinks', 'admin'), 0));
 
index 8daab15..fd0b465 100644 (file)
@@ -24,7 +24,7 @@
 
 $string['classname'] = 'Class name';
 $string['component'] = 'Component';
-$string['configmessageinboundhost'] = 'The address of the server that Moodle should check mail against. To specify a non-default port, use [server]:[port], for example mail.example.com:587. If a port isn\'t specified, the default port for the type of mail server will be used.';
+$string['configmessageinboundhost'] = 'The address of the server that Moodle should check mail against. To specify a non-default port, use [server]:[port], for example mail.example.com:993. If a port isn\'t specified, the default port for the type of mail server will be used.';
 $string['defaultexpiration'] = 'Default address expiry period';
 $string['defaultexpiration_help'] = 'When an email address is generated by the handler, it can be set to automatically expire after a period of time, so that it can no longer be used. It is advisable to set an expiry period.';
 $string['description'] = 'Description';
index 4d0c7f2..ce2d8fd 100644 (file)
@@ -127,3 +127,36 @@ Feature: Basic recycle bin functionality
     And I press "Yes"
     And I should see "Recycle bin has been emptied"
     And I should see "There are no items in the recycle bin."
+
+  @javascript
+  Scenario: Show recycle bin on category action menu
+    Given I log in as "admin"
+    And I navigate to "Courses >  Manage courses and categories" in site administration
+    And I click on "Actions menu" "link"
+    And I click on "Recycle bin" "link"
+    Then I should see "There are no items in the recycle bin."
+
+  @javascript
+  Scenario: Not show recycle bin empty on category action menu whit autohide enable
+    Given I log in as "admin"
+    And the following config values are set as admin:
+      | categorybinenable | 0 | tool_recyclebin |
+    And I navigate to "Courses >  Manage courses and categories" in site administration
+    And I click on "Actions menu" "link"
+    Then I should not see "Recycle bin"
+
+  @javascript
+  Scenario: Show recycle bin not empty on category action menu whit autohide enable
+    Given I log in as "admin"
+    And the following config values are set as admin:
+      | autohide | 1 | tool_recyclebin |
+    And I navigate to "Courses >  Manage courses and categories" in site administration
+    And I click on "Actions menu" "link"
+    Then I should not see "Recycle bin"
+    And I click on "delete" action for "Course 2" in management course listing
+    And I press "Delete"
+    And I should see "Deleting C2"
+    And I should see "C2 has been completely deleted"
+    And I press "Continue"
+    When I click on "Actions menu" "link"
+    Then I should see "Recycle bin"
index 9ccd292..299603f 100644 (file)
  */
 
 $string['asap'] = 'ASAP';
-$string['adhocempty'] = 'Adhoc task queue is empty';
-$string['adhocqueuesize'] = 'Adhoc task queue has {$a} tasks';
+$string['adhocempty'] = 'Ad hoc task queue is empty';
+$string['adhocqueuesize'] = 'Ad hoc task queue has {$a} tasks';
 $string['adhocqueueold'] = 'Oldest task is {$a->age} which is more than {$a->max}';
 $string['backtoscheduledtasks'] = 'Back to scheduled tasks';
 $string['blocking'] = 'Blocking';
 $string['cannotfindthepathtothecli'] = 'Cannot find the path to the PHP CLI executable so task execution aborted. Set the \'Path to PHP CLI\' setting in Site administration / Server / System paths.';
-$string['checkadhocqueue'] = 'Adhoc task queue';
+$string['checkadhocqueue'] = 'Ad hoc task queue';
 $string['checkcronrunning'] = 'Cron running';
 $string['checkmaxfaildelay'] = 'Tasks max fail delay';
 $string['clearfaildelay_confirm'] = 'Are you sure you want to clear the fail delay for task \'{$a}\'? After clearing the delay, the task will run according to its normal schedule.';
@@ -58,7 +58,7 @@ $string['runpattern'] = 'Run pattern';
 $string['scheduledtasks'] = 'Scheduled tasks';
 $string['scheduledtaskchangesdisabled'] = 'Modifications to the list of scheduled tasks have been prevented in Moodle configuration';
 $string['taskdisabled'] = 'Task disabled';
-$string['taskfailures'] = 'There are {$a} task(s) failing';
+$string['taskfailures'] = '{$a} task(s) failing';
 $string['tasklogs'] = 'Task logs';
 $string['tasknofailures'] = 'There are no tasks failing';
 $string['taskscheduleday'] = 'Day';
index 77ababc..e879835 100644 (file)
@@ -158,7 +158,7 @@ $string['newtablefrommysql'] = 'New table from MySQL';
 $string['new_table_from_mysql'] = 'New table from MySQL';
 $string['nofieldsspecified'] = 'No fields specified';
 $string['nomasterprimaryuniquefound'] = 'The column(s) that your foreign key references must be included in a primary or unique KEY in the referenced table. Note that the column being in a UNIQUE INDEX is not good enough.';
-$string['nomissingorextraindexesfound'] = 'No missing or extra indexes have been found, your DB doesn\'t need further actions.';
+$string['nomissingorextraindexesfound'] = 'No missing or extra indexes have been found, so no further action is required.';
 $string['noreffieldsspecified'] = 'No reference fields specified';
 $string['noreftablespecified'] = 'Specified reference table not found';
 $string['noviolatedforeignkeysfound'] = 'No violated foreign keys found';
index 1b2a7df..ac42715 100644 (file)
@@ -16,4 +16,4 @@ Feature: Add URL to main menu block
       | External URL | http://www.google.com |
       | id_display | In pop-up |
     Then "google" "link" should exist in the "Main menu" "block"
-    And "Add an activity or resource" "button" should exist in the "Main menu" "block"
+    And "Add an activity" "button" should exist in the "Main menu" "block"
index 2776339..dc4821b 100644 (file)
@@ -361,20 +361,23 @@ class cache_helper {
     /**
      * Ensure that the stats array is ready to collect information for the given store and definition.
      * @param string $store
+     * @param string $storeclass
      * @param string $definition A string that identifies the definition.
      * @param int $mode One of cache_store::MODE_*. Since 2.9.
      */
-    protected static function ensure_ready_for_stats($store, $definition, $mode = cache_store::MODE_APPLICATION) {
+    protected static function ensure_ready_for_stats($store, $storeclass, $definition, $mode = cache_store::MODE_APPLICATION) {
         // This function is performance-sensitive, so exit as quickly as possible
         // if we do not need to do anything.
         if (isset(self::$stats[$definition]['stores'][$store])) {
             return;
         }
+
         if (!array_key_exists($definition, self::$stats)) {
             self::$stats[$definition] = array(
                 'mode' => $mode,
                 'stores' => array(
                     $store => array(
+                        'class' => $storeclass,
                         'hits' => 0,
                         'misses' => 0,
                         'sets' => 0,
@@ -383,6 +386,7 @@ class cache_helper {
             );
         } else if (!array_key_exists($store, self::$stats[$definition]['stores'])) {
             self::$stats[$definition]['stores'][$store] = array(
+                'class' => $storeclass,
                 'hits' => 0,
                 'misses' => 0,
                 'sets' => 0,
@@ -418,15 +422,22 @@ class cache_helper {
      * In Moodle 2.9 the $definition argument changed from accepting only a string to accepting a string or a
      * cache_definition instance. It is preferable to pass a cache definition instance.
      *
+     * In Moodle 3.9 the first argument changed to also accept a cache_store.
+     *
      * @internal
-     * @param cache_definition $store
+     * @param string|cache_store $store
      * @param cache_definition $definition You used to be able to pass a string here, however that is deprecated please pass the
      *      actual cache_definition object now.
      * @param int $hits The number of hits to record (by default 1)
      */
     public static function record_cache_hit($store, $definition, $hits = 1) {
+        $storeclass = '';
+        if ($store instanceof cache_store) {
+            $storeclass = get_class($store);
+            $store = $store->my_name();
+        }
         list($definitionstr, $mode) = self::get_definition_stat_id_and_mode($definition);
-        self::ensure_ready_for_stats($store, $definitionstr, $mode);
+        self::ensure_ready_for_stats($store, $storeclass, $definitionstr, $mode);
         self::$stats[$definitionstr]['stores'][$store]['hits'] += $hits;
     }
 
@@ -436,15 +447,22 @@ class cache_helper {
      * In Moodle 2.9 the $definition argument changed from accepting only a string to accepting a string or a
      * cache_definition instance. It is preferable to pass a cache definition instance.
      *
+     * In Moodle 3.9 the first argument changed to also accept a cache_store.
+     *
      * @internal
-     * @param string $store
+     * @param string|cache_store $store
      * @param cache_definition $definition You used to be able to pass a string here, however that is deprecated please pass the
      *      actual cache_definition object now.
      * @param int $misses The number of misses to record (by default 1)
      */
     public static function record_cache_miss($store, $definition, $misses = 1) {
+        $storeclass = '';
+        if ($store instanceof cache_store) {
+            $storeclass = get_class($store);
+            $store = $store->my_name();
+        }
         list($definitionstr, $mode) = self::get_definition_stat_id_and_mode($definition);
-        self::ensure_ready_for_stats($store, $definitionstr, $mode);
+        self::ensure_ready_for_stats($store, $storeclass, $definitionstr, $mode);
         self::$stats[$definitionstr]['stores'][$store]['misses'] += $misses;
     }
 
@@ -454,15 +472,22 @@ class cache_helper {
      * In Moodle 2.9 the $definition argument changed from accepting only a string to accepting a string or a
      * cache_definition instance. It is preferable to pass a cache definition instance.
      *
+     * In Moodle 3.9 the first argument changed to also accept a cache_store.
+     *
      * @internal
-     * @param string $store
+     * @param string|cache_store $store
      * @param cache_definition $definition You used to be able to pass a string here, however that is deprecated please pass the
      *      actual cache_definition object now.
      * @param int $sets The number of sets to record (by default 1)
      */
     public static function record_cache_set($store, $definition, $sets = 1) {
+        $storeclass = '';
+        if ($store instanceof cache_store) {
+            $storeclass = get_class($store);
+            $store = $store->my_name();
+        }
         list($definitionstr, $mode) = self::get_definition_stat_id_and_mode($definition);
-        self::ensure_ready_for_stats($store, $definitionstr, $mode);
+        self::ensure_ready_for_stats($store, $storeclass, $definitionstr, $mode);
         self::$stats[$definitionstr]['stores'][$store]['sets'] += $sets;
     }
 
index 8cd7914..6236cb0 100644 (file)
@@ -414,7 +414,7 @@ class cache implements cache_loader {
         $setaftervalidation = false;
         if ($result === false) {
             if ($this->perfdebug) {
-                cache_helper::record_cache_miss($this->storetype, $this->definition);
+                cache_helper::record_cache_miss($this->store, $this->definition);
             }
             if ($this->loader !== false) {
                 // We must pass the original (unparsed) key to the next loader in the chain.
@@ -426,7 +426,7 @@ class cache implements cache_loader {
             }
             $setaftervalidation = ($result !== false);
         } else if ($this->perfdebug) {
-            cache_helper::record_cache_hit($this->storetype, $this->definition);
+            cache_helper::record_cache_hit($this->store, $this->definition);
         }
         // 5. Validate strictness.
         if ($strictness === MUST_EXIST && $result === false) {
@@ -580,8 +580,8 @@ class cache implements cache_loader {
                     $hits++;
                 }
             }
-            cache_helper::record_cache_hit($this->storetype, $this->definition, $hits);
-            cache_helper::record_cache_miss($this->storetype, $this->definition, $misses);
+            cache_helper::record_cache_hit($this->store, $this->definition, $hits);
+            cache_helper::record_cache_miss($this->store, $this->definition, $misses);
         }
 
         // Return the result. Phew!
@@ -607,7 +607,7 @@ class cache implements cache_loader {
      */
     public function set($key, $data) {
         if ($this->perfdebug) {
-            cache_helper::record_cache_set($this->storetype, $this->definition);
+            cache_helper::record_cache_set($this->store, $this->definition);
         }
         if ($this->loader !== false) {
             // We have a loader available set it there as well.
@@ -762,7 +762,7 @@ class cache implements cache_loader {
         }
         $successfullyset = $this->store->set_many($data);
         if ($this->perfdebug && $successfullyset) {
-            cache_helper::record_cache_set($this->storetype, $this->definition, $successfullyset);
+            cache_helper::record_cache_set($this->store, $this->definition, $successfullyset);
         }
         return $successfullyset;
     }
@@ -1112,7 +1112,7 @@ class cache implements cache_loader {
         }
         if ($result !== false) {
             if ($this->perfdebug) {
-                cache_helper::record_cache_hit('** static acceleration **', $this->definition);
+                cache_helper::record_cache_hit(cache_store::STATIC_ACCEL, $this->definition);
             }
             if ($this->staticaccelerationsize > 1 && $this->staticaccelerationcount > 1) {
                 // Check to see if this is the last item on the static acceleration keys array.
@@ -1126,7 +1126,7 @@ class cache implements cache_loader {
             return $result;
         } else {
             if ($this->perfdebug) {
-                cache_helper::record_cache_miss('** static acceleration **', $this->definition);
+                cache_helper::record_cache_miss(cache_store::STATIC_ACCEL, $this->definition);
             }
             return false;
         }
@@ -1830,7 +1830,7 @@ class cache_session extends cache {
         // 4. Load if from the loader/datasource if we don't already have it.
         if ($result === false) {
             if ($this->perfdebug) {
-                cache_helper::record_cache_miss($this->storetype, $this->get_definition());
+                cache_helper::record_cache_miss($this->get_store(), $this->get_definition());
             }
             if ($this->get_loader() !== false) {
                 // We must pass the original (unparsed) key to the next loader in the chain.
@@ -1845,7 +1845,7 @@ class cache_session extends cache {
                 $this->set($key, $result);
             }
         } else if ($this->perfdebug) {
-            cache_helper::record_cache_hit($this->storetype, $this->get_definition());
+            cache_helper::record_cache_hit($this->get_store(), $this->get_definition());
         }
         // 5. Validate strictness.
         if ($strictness === MUST_EXIST && $result === false) {
@@ -1889,7 +1889,7 @@ class cache_session extends cache {
             $loader->set($key, $data);
         }
         if ($this->perfdebug) {
-            cache_helper::record_cache_set($this->storetype, $this->get_definition());
+            cache_helper::record_cache_set($this->get_store(), $this->get_definition());
         }
         if (is_object($data) && $data instanceof cacheable_object) {
             $data = new cache_cached_object($data);
@@ -2019,8 +2019,8 @@ class cache_session extends cache {
                     $hits++;
                 }
             }
-            cache_helper::record_cache_hit($this->storetype, $this->get_definition(), $hits);
-            cache_helper::record_cache_miss($this->storetype, $this->get_definition(), $misses);
+            cache_helper::record_cache_hit($this->get_store(), $this->get_definition(), $hits);
+            cache_helper::record_cache_miss($this->get_store(), $this->get_definition(), $misses);
         }
         return $return;
 
@@ -2097,7 +2097,7 @@ class cache_session extends cache {
         }
         $successfullyset = $this->get_store()->set_many($data);
         if ($this->perfdebug && $successfullyset) {
-            cache_helper::record_cache_set($this->storetype, $this->get_definition(), $successfullyset);
+            cache_helper::record_cache_set($this->store, $this->get_definition(), $successfullyset);
         }
         return $successfullyset;
     }
index 4fcb03f..a2cfe3e 100644 (file)
@@ -144,6 +144,10 @@ abstract class cache_store implements cache_store_interface {
      * Request caches. Static caches really.
      */
     const MODE_REQUEST = 4;
+    /**
+     * Static caches.
+     */
+    const STATIC_ACCEL = '** static accel. **';
 
     /**
      * Constructs an instance of the cache store.
index 9c0f1d9..0e6b203 100644 (file)
@@ -2092,15 +2092,15 @@ class core_cache_testcase extends advanced_testcase {
         $this->assertFalse($request->get('missMe'));
 
         $endstats = cache_helper::get_stats();
-        $this->assertEquals(2, $endstats[$applicationid]['stores']['cachestore_file']['misses']);
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['hits']);
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['sets']);
-        $this->assertEquals(3, $endstats[$sessionid]['stores']['cachestore_session']['misses']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['hits']);
-        $this->assertEquals(1, $endstats[$sessionid]['stores']['cachestore_session']['sets']);
-        $this->assertEquals(4, $endstats[$requestid]['stores']['cachestore_static']['misses']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['hits']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['sets']);
+        $this->assertEquals(2, $endstats[$applicationid]['stores']['default_application']['misses']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['hits']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['sets']);
+        $this->assertEquals(3, $endstats[$sessionid]['stores']['default_session']['misses']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['hits']);
+        $this->assertEquals(1, $endstats[$sessionid]['stores']['default_session']['sets']);
+        $this->assertEquals(4, $endstats[$requestid]['stores']['default_request']['misses']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['hits']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['sets']);
 
         $startstats = cache_helper::get_stats();
 
@@ -2116,24 +2116,24 @@ class core_cache_testcase extends advanced_testcase {
         $this->assertTrue($request->set('setMe4', 4));
 
         $endstats = cache_helper::get_stats();
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['misses'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['misses']);
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['hits'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['hits']);
-        $this->assertEquals(2, $endstats[$applicationid]['stores']['cachestore_file']['sets'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['sets']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['misses'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['misses']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['hits'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['hits']);
-        $this->assertEquals(3, $endstats[$sessionid]['stores']['cachestore_session']['sets'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['sets']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['misses'] -
-            $startstats[$requestid]['stores']['cachestore_static']['misses']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['hits'] -
-            $startstats[$requestid]['stores']['cachestore_static']['hits']);
-        $this->assertEquals(4, $endstats[$requestid]['stores']['cachestore_static']['sets'] -
-            $startstats[$requestid]['stores']['cachestore_static']['sets']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['misses'] -
+                             $startstats[$applicationid]['stores']['default_application']['misses']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['hits'] -
+                             $startstats[$applicationid]['stores']['default_application']['hits']);
+        $this->assertEquals(2, $endstats[$applicationid]['stores']['default_application']['sets'] -
+                             $startstats[$applicationid]['stores']['default_application']['sets']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['misses'] -
+                             $startstats[$sessionid]['stores']['default_session']['misses']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['hits'] -
+                             $startstats[$sessionid]['stores']['default_session']['hits']);
+        $this->assertEquals(3, $endstats[$sessionid]['stores']['default_session']['sets'] -
+                             $startstats[$sessionid]['stores']['default_session']['sets']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['misses'] -
+                             $startstats[$requestid]['stores']['default_request']['misses']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['hits'] -
+                             $startstats[$requestid]['stores']['default_request']['hits']);
+        $this->assertEquals(4, $endstats[$requestid]['stores']['default_request']['sets'] -
+                             $startstats[$requestid]['stores']['default_request']['sets']);
 
         $startstats = cache_helper::get_stats();
 
@@ -2149,24 +2149,24 @@ class core_cache_testcase extends advanced_testcase {
         $this->assertEquals($request->get('setMe4'), 4);
 
         $endstats = cache_helper::get_stats();
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['misses'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['misses']);
-        $this->assertEquals(2, $endstats[$applicationid]['stores']['cachestore_file']['hits'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['hits']);
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['sets'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['sets']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['misses'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['misses']);
-        $this->assertEquals(3, $endstats[$sessionid]['stores']['cachestore_session']['hits'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['hits']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['sets'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['sets']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['misses'] -
-            $startstats[$requestid]['stores']['cachestore_static']['misses']);
-        $this->assertEquals(4, $endstats[$requestid]['stores']['cachestore_static']['hits'] -
-            $startstats[$requestid]['stores']['cachestore_static']['hits']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['sets'] -
-            $startstats[$requestid]['stores']['cachestore_static']['sets']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['misses'] -
+                             $startstats[$applicationid]['stores']['default_application']['misses']);
+        $this->assertEquals(2, $endstats[$applicationid]['stores']['default_application']['hits'] -
+                             $startstats[$applicationid]['stores']['default_application']['hits']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['sets'] -
+                             $startstats[$applicationid]['stores']['default_application']['sets']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['misses'] -
+                             $startstats[$sessionid]['stores']['default_session']['misses']);
+        $this->assertEquals(3, $endstats[$sessionid]['stores']['default_session']['hits'] -
+                             $startstats[$sessionid]['stores']['default_session']['hits']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['sets'] -
+                             $startstats[$sessionid]['stores']['default_session']['sets']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['misses'] -
+                             $startstats[$requestid]['stores']['default_request']['misses']);
+        $this->assertEquals(4, $endstats[$requestid]['stores']['default_request']['hits'] -
+                             $startstats[$requestid]['stores']['default_request']['hits']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['sets'] -
+                             $startstats[$requestid]['stores']['default_request']['sets']);
 
         $startstats = cache_helper::get_stats();
 
@@ -2176,24 +2176,24 @@ class core_cache_testcase extends advanced_testcase {
         $request->get_many(array('setMe1', 'setMe2', 'setMe3', 'setMe4'));
 
         $endstats = cache_helper::get_stats();
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['misses'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['misses']);
-        $this->assertEquals(2, $endstats[$applicationid]['stores']['cachestore_file']['hits'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['hits']);
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['cachestore_file']['sets'] -
-            $startstats[$applicationid]['stores']['cachestore_file']['sets']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['misses'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['misses']);
-        $this->assertEquals(3, $endstats[$sessionid]['stores']['cachestore_session']['hits'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['hits']);
-        $this->assertEquals(0, $endstats[$sessionid]['stores']['cachestore_session']['sets'] -
-            $startstats[$sessionid]['stores']['cachestore_session']['sets']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['misses'] -
-            $startstats[$requestid]['stores']['cachestore_static']['misses']);
-        $this->assertEquals(4, $endstats[$requestid]['stores']['cachestore_static']['hits'] -
-            $startstats[$requestid]['stores']['cachestore_static']['hits']);
-        $this->assertEquals(0, $endstats[$requestid]['stores']['cachestore_static']['sets'] -
-            $startstats[$requestid]['stores']['cachestore_static']['sets']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['misses'] -
+                             $startstats[$applicationid]['stores']['default_application']['misses']);
+        $this->assertEquals(2, $endstats[$applicationid]['stores']['default_application']['hits'] -
+                             $startstats[$applicationid]['stores']['default_application']['hits']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['sets'] -
+                             $startstats[$applicationid]['stores']['default_application']['sets']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['misses'] -
+                             $startstats[$sessionid]['stores']['default_session']['misses']);
+        $this->assertEquals(3, $endstats[$sessionid]['stores']['default_session']['hits'] -
+                             $startstats[$sessionid]['stores']['default_session']['hits']);
+        $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['sets'] -
+                             $startstats[$sessionid]['stores']['default_session']['sets']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['misses'] -
+                             $startstats[$requestid]['stores']['default_request']['misses']);
+        $this->assertEquals(4, $endstats[$requestid]['stores']['default_request']['hits'] -
+                             $startstats[$requestid]['stores']['default_request']['hits']);
+        $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['sets'] -
+                             $startstats[$requestid]['stores']['default_request']['sets']);
     }
 
     public function test_static_cache() {
@@ -2225,8 +2225,8 @@ class core_cache_testcase extends advanced_testcase {
 
         // Check that the static acceleration worked, even on empty arrays and the number 0.
         $endstats = cache_helper::get_stats();
-        $this->assertEquals(0, $endstats[$applicationid]['stores']['** static acceleration **']['misses']);
-        $this->assertEquals(3, $endstats[$applicationid]['stores']['** static acceleration **']['hits']);
+        $this->assertEquals(0, $endstats[$applicationid]['stores']['** static accel. **']['misses']);
+        $this->assertEquals(3, $endstats[$applicationid]['stores']['** static accel. **']['hits']);
     }
 
     public function test_performance_debug_off() {
index eaf6344..076bd53 100644 (file)
@@ -1,6 +1,9 @@
 This files describes API changes in /cache/stores/* - cache store plugins.
 Information provided here is intended especially for developers.
 
+=== 3.9 ===
+* The record_cache_hit/miss/set methods now take a cache_store instead of a cache_definition object
+
 === 3.8 ===
 * The Redis cache store can now make use of the Zstandard compression algorithm (see MDL-66428).
 
index 9a3f1d0..3b08693 100644 (file)
@@ -13,7 +13,7 @@
     "require-dev": {
         "phpunit/phpunit": "7.5.*",
         "phpunit/dbunit": "4.0.*",
-        "moodlehq/behat-extension": "3.39.2",
+        "moodlehq/behat-extension": "3.39.3",
         "mikey179/vfsstream": "^1.6",
         "instaclick/php-webdriver": "dev-local as 1.x-dev"
     }
index 7fd4234..11dbff6 100644 (file)
@@ -4,42 +4,44 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "a2329ee2d14a351b74f99322f42722da",
+    "content-hash": "b1953ceec577434625a7aee12f650daa",
     "packages": [],
     "packages-dev": [
         {
             "name": "behat/behat",
-            "version": "v3.5.0",
+            "version": "v3.6.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Behat/Behat.git",
-                "reference": "e4bce688be0c2029dc1700e46058d86428c63cab"
+                "reference": "9bfe195b4745c32e068af03fa4df9558b4916d30"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Behat/Behat/zipball/e4bce688be0c2029dc1700e46058d86428c63cab",
-                "reference": "e4bce688be0c2029dc1700e46058d86428c63cab",
+                "url": "https://api.github.com/repos/Behat/Behat/zipball/9bfe195b4745c32e068af03fa4df9558b4916d30",
+                "reference": "9bfe195b4745c32e068af03fa4df9558b4916d30",
                 "shasum": ""
             },
             "require": {
-                "behat/gherkin": "^4.5.1",
+                "behat/gherkin": "^4.6.0",
                 "behat/transliterator": "^1.2",
                 "container-interop/container-interop": "^1.2",
                 "ext-mbstring": "*",
                 "php": ">=5.3.3",
                 "psr/container": "^1.0",
-                "symfony/class-loader": "~2.1||~3.0",
-                "symfony/config": "~2.3||~3.0||~4.0",
-                "symfony/console": "~2.7.40||^2.8.33||~3.3.15||^3.4.3||^4.0.3",
-                "symfony/dependency-injection": "~2.1||~3.0||~4.0",
-                "symfony/event-dispatcher": "~2.1||~3.0||~4.0",
-                "symfony/translation": "~2.3||~3.0||~4.0",
-                "symfony/yaml": "~2.1||~3.0||~4.0"
+                "symfony/config": "^2.7.51 || ^3.0 || ^4.0 || ^5.0",
+                "symfony/console": "^2.7.51 || ^2.8.33 || ^3.3.15 || ^3.4.3 || ^4.0.3 || ^5.0",
+                "symfony/dependency-injection": "^2.7.51 || ^3.0 || ^4.0 || ^5.0",
+                "symfony/event-dispatcher": "^2.7.51 || ^3.0 || ^4.0 || ^5.0",
+                "symfony/translation": "^2.7.51 || ^3.0 || ^4.0 || ^5.0",
+                "symfony/yaml": "^2.7.51 || ^3.0 || ^4.0 || ^5.0"
             },
             "require-dev": {
                 "herrera-io/box": "~1.6.1",
-                "phpunit/phpunit": "^4.8.36|^6.3",
-                "symfony/process": "~2.5|~3.0|~4.0"
+                "phpunit/phpunit": "^4.8.36 || ^6.3",
+                "symfony/process": "~2.5 || ^3.0 || ^4.0 || ^5.0"
+            },
+            "suggest": {
+                "ext-dom": "Needed to output test results in JUnit format."
             },
             "bin": [
                 "bin/behat"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.5.x-dev"
+                    "dev-master": "3.6.x-dev"
                 }
             },
             "autoload": {
-                "psr-0": {
-                    "Behat\\Behat": "src/",
-                    "Behat\\Testwork": "src/"
+                "psr-4": {
+                    "Behat\\Behat\\": "src/Behat/Behat/",
+                    "Behat\\Testwork\\": "src/Behat/Testwork/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
@@ -83,7 +85,7 @@
                 "symfony",
                 "testing"
             ],
-            "time": "2018-08-10T18:56:51+00:00"
+            "time": "2020-02-06T09:54:48+00:00"
         },
         {
             "name": "behat/gherkin",
         },
         {
             "name": "guzzlehttp/guzzle",
-            "version": "6.5.2",
+            "version": "6.5.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/guzzle.git",
-                "reference": "43ece0e75098b7ecd8d13918293029e555a50f82"
+                "reference": "aab4ebd862aa7d04f01a4b51849d657db56d882e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/43ece0e75098b7ecd8d13918293029e555a50f82",
-                "reference": "43ece0e75098b7ecd8d13918293029e555a50f82",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/aab4ebd862aa7d04f01a4b51849d657db56d882e",
+                "reference": "aab4ebd862aa7d04f01a4b51849d657db56d882e",
                 "shasum": ""
             },
             "require": {
                 "ext-json": "*",
                 "guzzlehttp/promises": "^1.0",
                 "guzzlehttp/psr7": "^1.6.1",
-                "php": ">=5.5"
+                "php": ">=5.5",
+                "symfony/polyfill-intl-idn": "^1.11"
             },
             "require-dev": {
                 "ext-curl": "*",
                 "psr/log": "^1.1"
             },
             "suggest": {
-                "ext-intl": "Required for Internationalized Domain Name (IDN) support",
                 "psr/log": "Required for using the Log middleware"
             },
             "type": "library",
                 "rest",
                 "web service"
             ],
-            "time": "2019-12-23T11:57:10+00:00"
+            "time": "2020-04-18T10:38:46+00:00"
         },
         {
             "name": "guzzlehttp/promises",
         },
         {
             "name": "moodlehq/behat-extension",
-            "version": "v3.39.2",
+            "version": "v3.39.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/moodlehq/moodle-behat-extension.git",
-                "reference": "7a2df2124ba8a85ccf21e517d18c78f932bdbbce"
+                "reference": "d05ea443ff24f90edb9b31c92e4dfe67c58a0b4b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/7a2df2124ba8a85ccf21e517d18c78f932bdbbce",
-                "reference": "7a2df2124ba8a85ccf21e517d18c78f932bdbbce",
+                "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/d05ea443ff24f90edb9b31c92e4dfe67c58a0b4b",
+                "reference": "d05ea443ff24f90edb9b31c92e4dfe67c58a0b4b",
                 "shasum": ""
             },
             "require": {
-                "behat/behat": "3.5.*",
+                "behat/behat": "3.6.*",
                 "behat/mink": "~1.8",
-                "behat/mink-extension": "~2.2",
+                "behat/mink-extension": "~2.3",
                 "behat/mink-goutte-driver": "~1.2",
-                "behat/mink-selenium2-driver": "~1.3",
+                "behat/mink-selenium2-driver": "~1.4",
                 "php": ">=7.2.0",
-                "symfony/process": "2.8.*"
+                "symfony/process": "^4.0 || ^5.0"
             },
             "type": "library",
             "autoload": {
                 "Behat",
                 "moodle"
             ],
-            "time": "2020-04-09T16:06:14+00:00"
+            "time": "2020-04-20T09:32:44+00:00"
         },
         {
             "name": "myclabs/deep-copy",
             "time": "2017-02-14T16:28:37+00:00"
         },
         {
-            "name": "psr/http-message",
-            "version": "1.0.1",
+            "name": "psr/event-dispatcher",
+            "version": "1.0.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/php-fig/http-message.git",
-                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
+                "url": "https://github.com/php-fig/event-dispatcher.git",
+                "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
-                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
+                "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0",
+                "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.0"
+                "php": ">=7.2.0"
             },
             "type": "library",
             "extra": {
             },
             "autoload": {
                 "psr-4": {
-                    "Psr\\Http\\Message\\": "src/"
+                    "Psr\\EventDispatcher\\": "src/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                     "homepage": "http://www.php-fig.org/"
                 }
             ],
-            "description": "Common interface for HTTP messages",
-            "homepage": "https://github.com/php-fig/http-message",
+            "description": "Standard interfaces for event handling.",
             "keywords": [
-                "http",
-                "http-message",
+                "events",
                 "psr",
-                "psr-7",
-                "request",
-                "response"
+                "psr-14"
             ],
-            "time": "2016-08-06T14:39:51+00:00"
+            "time": "2019-01-08T18:20:26+00:00"
         },
         {
-            "name": "psr/log",
-            "version": "1.1.3",
+            "name": "psr/http-message",
+            "version": "1.0.1",
             "source": {
                 "type": "git",
-                "url": "https://github.com/php-fig/log.git",
-                "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc"
+                "url": "https://github.com/php-fig/http-message.git",
+                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc",
-                "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc",
+                "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
+                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.1.x-dev"
+                    "dev-master": "1.0.x-dev"
                 }
             },
             "autoload": {
                 "psr-4": {
-                    "Psr\\Log\\": "Psr/Log/"
+                    "Psr\\Http\\Message\\": "src/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                     "homepage": "http://www.php-fig.org/"
                 }
             ],
-            "description": "Common interface for logging libraries",
-            "homepage": "https://github.com/php-fig/log",
+            "description": "Common interface for HTTP messages",
+            "homepage": "https://github.com/php-fig/http-message",
             "keywords": [
-                "log",
+                "http",
+                "http-message",
                 "psr",
-                "psr-3"
+                "psr-7",
+                "request",
+                "response"
             ],
-            "time": "2020-03-23T09:12:05+00:00"
+            "time": "2016-08-06T14:39:51+00:00"
         },
         {
             "name": "ralouphie/getallheaders",
             ],
             "time": "2020-03-28T10:15:50+00:00"
         },
-        {
-            "name": "symfony/class-loader",
-            "version": "v3.4.39",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/class-loader.git",
-                "reference": "e4636a4f23f157278a19e5db160c63de0da297d8"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/class-loader/zipball/e4636a4f23f157278a19e5db160c63de0da297d8",
-                "reference": "e4636a4f23f157278a19e5db160c63de0da297d8",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^5.5.9|>=7.0.8"
-            },
-            "require-dev": {
-                "symfony/finder": "~2.8|~3.0|~4.0",
-                "symfony/polyfill-apcu": "~1.1"
-            },
-            "suggest": {
-                "symfony/polyfill-apcu": "For using ApcClassLoader on HHVM"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "3.4-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Component\\ClassLoader\\": ""
-                },
-                "exclude-from-classmap": [
-                    "/Tests/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Fabien Potencier",
-                    "email": "fabien@symfony.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Symfony ClassLoader Component",
-            "homepage": "https://symfony.com",
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2020-03-15T09:38:08+00:00"
-        },
         {
             "name": "symfony/config",
             "version": "v4.4.7",
             ],
             "description": "Symfony Config Component",
             "homepage": "https://symfony.com",
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
             "time": "2020-03-27T16:54:36+00:00"
         },
         {
             "name": "symfony/console",
-            "version": "v3.3.18",
+            "version": "v5.0.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "af7ec995de93671c03cc1b4e3176c8588bc79dcc"
+                "reference": "5fa1caadc8cdaa17bcfb25219f3b53fe294a9935"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/af7ec995de93671c03cc1b4e3176c8588bc79dcc",
-                "reference": "af7ec995de93671c03cc1b4e3176c8588bc79dcc",
+                "url": "https://api.github.com/repos/symfony/console/zipball/5fa1caadc8cdaa17bcfb25219f3b53fe294a9935",
+                "reference": "5fa1caadc8cdaa17bcfb25219f3b53fe294a9935",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5.9|>=7.0.8",
-                "symfony/debug": "~2.8|~3.0",
-                "symfony/polyfill-mbstring": "~1.0"
+                "php": "^7.2.5",
+                "symfony/polyfill-mbstring": "~1.0",
+                "symfony/polyfill-php73": "^1.8",
+                "symfony/service-contracts": "^1.1|^2"
             },
             "conflict": {
-                "symfony/dependency-injection": "<3.3"
+                "symfony/dependency-injection": "<4.4",
+                "symfony/event-dispatcher": "<4.4",
+                "symfony/lock": "<4.4",
+                "symfony/process": "<4.4"
+            },
+            "provide": {
+                "psr/log-implementation": "1.0"
             },
             "require-dev": {
                 "psr/log": "~1.0",
-                "symfony/config": "~3.3",
-                "symfony/dependency-injection": "~3.3",
-                "symfony/event-dispatcher": "~2.8|~3.0",
-                "symfony/filesystem": "~2.8|~3.0",
-                "symfony/process": "~2.8|~3.0"
+                "symfony/config": "^4.4|^5.0",
+                "symfony/dependency-injection": "^4.4|^5.0",
+                "symfony/event-dispatcher": "^4.4|^5.0",
+                "symfony/lock": "^4.4|^5.0",
+                "symfony/process": "^4.4|^5.0",
+                "symfony/var-dumper": "^4.4|^5.0"
             },
             "suggest": {
                 "psr/log": "For using the console logger",
                 "symfony/event-dispatcher": "",
-                "symfony/filesystem": "",
+                "symfony/lock": "",
                 "symfony/process": ""
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "5.0-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony Console Component",
             "homepage": "https://symfony.com",
-            "time": "2018-01-29T09:02:23+00:00"
+            "time": "2020-03-30T11:42:42+00:00"
         },
         {
             "name": "symfony/css-selector",
             ],
             "time": "2020-03-27T16:56:45+00:00"
         },
-        {
-            "name": "symfony/debug",
-            "version": "v3.4.39",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/debug.git",
-                "reference": "ce9f3b5e8e1c50f849fded59b3a1b6bc3562ec29"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/debug/zipball/ce9f3b5e8e1c50f849fded59b3a1b6bc3562ec29",
-                "reference": "ce9f3b5e8e1c50f849fded59b3a1b6bc3562ec29",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^5.5.9|>=7.0.8",
-                "psr/log": "~1.0"
-            },
-            "conflict": {
-                "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2"
-            },
-            "require-dev": {
-                "symfony/http-kernel": "~2.8|~3.0|~4.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "3.4-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Symfony\\Component\\Debug\\": ""
-                },
-                "exclude-from-classmap": [
-                    "/Tests/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Fabien Potencier",
-                    "email": "fabien@symfony.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Symfony Debug Component",
-            "homepage": "https://symfony.com",
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2020-03-23T10:22:40+00:00"
-        },
         {
             "name": "symfony/dependency-injection",
-            "version": "v3.3.18",
+            "version": "v4.4.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dependency-injection.git",
-                "reference": "54243abc4e1a1a15e274e391bd6f7090b44711f1"
+                "reference": "755b18859be26b90f4bf63753432d3387458bf31"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/54243abc4e1a1a15e274e391bd6f7090b44711f1",
-                "reference": "54243abc4e1a1a15e274e391bd6f7090b44711f1",
+                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/755b18859be26b90f4bf63753432d3387458bf31",
+                "reference": "755b18859be26b90f4bf63753432d3387458bf31",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5.9|>=7.0.8",
-                "psr/container": "^1.0"
+                "php": "^7.1.3",
+                "psr/container": "^1.0",
+                "symfony/service-contracts": "^1.1.6|^2"
             },
             "conflict": {
-                "symfony/config": "<3.3.7",
-                "symfony/finder": "<3.3",
-                "symfony/yaml": "<3.3"
+                "symfony/config": "<4.3|>=5.0",
+                "symfony/finder": "<3.4",
+                "symfony/proxy-manager-bridge": "<3.4",
+                "symfony/yaml": "<3.4"
             },
             "provide": {
-                "psr/container-implementation": "1.0"
+                "psr/container-implementation": "1.0",
+                "symfony/service-implementation": "1.0"
             },
             "require-dev": {
-                "symfony/config": "~3.3",
-                "symfony/expression-language": "~2.8|~3.0",
-                "symfony/yaml": "~3.3"
+                "symfony/config": "^4.3",
+                "symfony/expression-language": "^3.4|^4.0|^5.0",
+                "symfony/yaml": "^3.4|^4.0|^5.0"
             },
             "suggest": {
                 "symfony/config": "",
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.4-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony DependencyInjection Component",
             "homepage": "https://symfony.com",
-            "time": "2018-01-29T09:02:23+00:00"
+            "time": "2020-03-30T10:09:30+00:00"
         },
         {
             "name": "symfony/dom-crawler",
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v3.4.39",
+            "version": "v5.0.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "9d4e22943b73acc1ba50595b7de1a01fe9dbad48"
+                "reference": "24f40d95385774ed5c71dbf014edd047e2f2f3dc"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9d4e22943b73acc1ba50595b7de1a01fe9dbad48",
-                "reference": "9d4e22943b73acc1ba50595b7de1a01fe9dbad48",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/24f40d95385774ed5c71dbf014edd047e2f2f3dc",
+                "reference": "24f40d95385774ed5c71dbf014edd047e2f2f3dc",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5.9|>=7.0.8"
+                "php": "^7.2.5",
+                "symfony/event-dispatcher-contracts": "^2"
             },
             "conflict": {
-                "symfony/dependency-injection": "<3.3"
+                "symfony/dependency-injection": "<4.4"
+            },
+            "provide": {
+                "psr/event-dispatcher-implementation": "1.0",
+                "symfony/event-dispatcher-implementation": "2.0"
             },
             "require-dev": {
                 "psr/log": "~1.0",
-                "symfony/config": "~2.8|~3.0|~4.0",
-                "symfony/dependency-injection": "~3.3|~4.0",
-                "symfony/expression-language": "~2.8|~3.0|~4.0",
-                "symfony/stopwatch": "~2.8|~3.0|~4.0"
+                "symfony/config": "^4.4|^5.0",
+                "symfony/dependency-injection": "^4.4|^5.0",
+                "symfony/expression-language": "^4.4|^5.0",
+                "symfony/http-foundation": "^4.4|^5.0",
+                "symfony/service-contracts": "^1.1|^2",
+                "symfony/stopwatch": "^4.4|^5.0"
             },
             "suggest": {
                 "symfony/dependency-injection": "",
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.4-dev"
+                    "dev-master": "5.0-dev"
                 }
             },
             "autoload": {
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-03-15T09:38:08+00:00"
+            "time": "2020-03-27T16:56:45+00:00"
+        },
+        {
+            "name": "symfony/event-dispatcher-contracts",
+            "version": "v2.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/event-dispatcher-contracts.git",
+                "reference": "af23c2584d4577d54661c434446fb8fbed6025dd"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/af23c2584d4577d54661c434446fb8fbed6025dd",
+                "reference": "af23c2584d4577d54661c434446fb8fbed6025dd",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2.5",
+                "psr/event-dispatcher": "^1"
+            },
+            "suggest": {
+                "symfony/event-dispatcher-implementation": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Contracts\\EventDispatcher\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Generic abstractions related to dispatching event",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "abstractions",
+                "contracts",
+                "decoupling",
+                "interfaces",
+                "interoperability",
+                "standards"
+            ],
+            "time": "2019-11-18T17:27:11+00:00"
         },
         {
             "name": "symfony/filesystem",
             ],
             "description": "Symfony Filesystem Component",
             "homepage": "https://symfony.com",
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
             "time": "2020-03-27T16:56:45+00:00"
         },
         {
             "time": "2020-02-27T09:26:54+00:00"
         },
         {
-            "name": "symfony/polyfill-mbstring",
+            "name": "symfony/polyfill-intl-idn",
             "version": "v1.15.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/symfony/polyfill-mbstring.git",
-                "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac"
+                "url": "https://github.com/symfony/polyfill-intl-idn.git",
+                "reference": "47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/81ffd3a9c6d707be22e3012b827de1c9775fc5ac",
-                "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf",
+                "reference": "47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.3"
+                "php": ">=5.3.3",
+                "symfony/polyfill-mbstring": "^1.3",
+                "symfony/polyfill-php72": "^1.10"
             },
             "suggest": {
-                "ext-mbstring": "For best performance"
+                "ext-intl": "For best performance"
             },
             "type": "library",
             "extra": {
             },
             "autoload": {
                 "psr-4": {
-                    "Symfony\\Polyfill\\Mbstring\\": ""
+                    "Symfony\\Polyfill\\Intl\\Idn\\": ""
                 },
                 "files": [
                     "bootstrap.php"
             ],
             "authors": [
                 {
-                    "name": "Nicolas Grekas",
-                    "email": "p@tchwork.com"
+                    "name": "Laurent Bassin",
+                    "email": "laurent@bassin.info"
                 },
                 {
                     "name": "Symfony Community",
                     "homepage": "https://symfony.com/contributors"
                 }
             ],
-            "description": "Symfony polyfill for the Mbstring extension",
+            "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
             "homepage": "https://symfony.com",
             "keywords": [
                 "compatibility",
-                "mbstring",
+                "idn",
+                "intl",
                 "polyfill",
                 "portable",
                 "shim"
             "time": "2020-03-09T19:04:49+00:00"
         },
         {
-            "name": "symfony/process",
-            "version": "v2.8.52",
+            "name": "symfony/polyfill-mbstring",
+            "version": "v1.15.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/symfony/process.git",
-                "reference": "c3591a09c78639822b0b290d44edb69bf9f05dc8"
+                "url": "https://github.com/symfony/polyfill-mbstring.git",
+                "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/81ffd3a9c6d707be22e3012b827de1c9775fc5ac",
+                "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "suggest": {
+                "ext-mbstring": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.15-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Mbstring\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for the Mbstring extension",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "mbstring",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-03-09T19:04:49+00:00"
+        },
+        {
+            "name": "symfony/polyfill-php72",
+            "version": "v1.15.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php72.git",
+                "reference": "37b0976c78b94856543260ce09b460a7bc852747"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/37b0976c78b94856543260ce09b460a7bc852747",
+                "reference": "37b0976c78b94856543260ce09b460a7bc852747",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.15-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php72\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-02-27T09:26:54+00:00"
+        },
+        {
+            "name": "symfony/polyfill-php73",
+            "version": "v1.15.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php73.git",
+                "reference": "0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/process/zipball/c3591a09c78639822b0b290d44edb69bf9f05dc8",
-                "reference": "c3591a09c78639822b0b290d44edb69bf9f05dc8",
+                "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7",
+                "reference": "0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.9"
+                "php": ">=5.3.3"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.8-dev"
+                    "dev-master": "1.15-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php73\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ],
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-02-27T09:26:54+00:00"
+        },
+        {
+            "name": "symfony/process",
+            "version": "v5.0.7",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/process.git",
+                "reference": "c5ca4a0fc16a0c888067d43fbcfe1f8a53d8e70e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/process/zipball/c5ca4a0fc16a0c888067d43fbcfe1f8a53d8e70e",
+                "reference": "c5ca4a0fc16a0c888067d43fbcfe1f8a53d8e70e",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2.5"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "5.0-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony Process Component",
             "homepage": "https://symfony.com",
-            "time": "2018-11-11T11:18:13+00:00"
+            "time": "2020-03-27T16:56:45+00:00"
+        },
+        {
+            "name": "symfony/service-contracts",
+            "version": "v2.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/service-contracts.git",
+                "reference": "144c5e51266b281231e947b51223ba14acf1a749"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/service-contracts/zipball/144c5e51266b281231e947b51223ba14acf1a749",
+                "reference": "144c5e51266b281231e947b51223ba14acf1a749",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2.5",
+                "psr/container": "^1.0"
+            },
+            "suggest": {
+                "symfony/service-implementation": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Contracts\\Service\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Generic abstractions related to writing services",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "abstractions",
+                "contracts",
+                "decoupling",
+                "interfaces",
+                "interoperability",
+                "standards"
+            ],
+            "time": "2019-11-18T17:27:11+00:00"
         },
         {
             "name": "symfony/translation",
-            "version": "v3.3.18",
+            "version": "v4.4.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
-                "reference": "90cb5ca3eb84b3053fef876e11e405fd819487fc"
+                "reference": "4e54d336f2eca5facad449d0b0118bb449375b76"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/translation/zipball/90cb5ca3eb84b3053fef876e11e405fd819487fc",
-                "reference": "90cb5ca3eb84b3053fef876e11e405fd819487fc",
+                "url": "https://api.github.com/repos/symfony/translation/zipball/4e54d336f2eca5facad449d0b0118bb449375b76",
+                "reference": "4e54d336f2eca5facad449d0b0118bb449375b76",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5.9|>=7.0.8",
-                "symfony/polyfill-mbstring": "~1.0"
+                "php": "^7.1.3",
+                "symfony/polyfill-mbstring": "~1.0",
+                "symfony/translation-contracts": "^1.1.6|^2"
             },
             "conflict": {
-                "symfony/config": "<2.8",
-                "symfony/yaml": "<3.3"
+                "symfony/config": "<3.4",
+                "symfony/dependency-injection": "<3.4",
+                "symfony/http-kernel": "<4.4",
+                "symfony/yaml": "<3.4"
+            },
+            "provide": {
+                "symfony/translation-implementation": "1.0"
             },
             "require-dev": {
                 "psr/log": "~1.0",
-                "symfony/config": "~2.8|~3.0",
-                "symfony/intl": "^2.8.18|^3.2.5",
-                "symfony/yaml": "~3.3"
+                "symfony/config": "^3.4|^4.0|^5.0",
+                "symfony/console": "^3.4|^4.0|^5.0",
+                "symfony/dependency-injection": "^3.4|^4.0|^5.0",
+                "symfony/finder": "~2.8|~3.0|~4.0|^5.0",
+                "symfony/http-kernel": "^4.4",
+                "symfony/intl": "^3.4|^4.0|^5.0",
+                "symfony/service-contracts": "^1.1.2|^2",
+                "symfony/yaml": "^3.4|^4.0|^5.0"
             },
             "suggest": {
-                "psr/log": "To use logging capability in translator",
+                "psr/log-implementation": "To use logging capability in translator",
                 "symfony/config": "",
                 "symfony/yaml": ""
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.4-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony Translation Component",
             "homepage": "https://symfony.com",
-            "time": "2018-01-18T14:19:00+00:00"
+            "time": "2020-03-27T16:54:36+00:00"
+        },
+        {
+            "name": "symfony/translation-contracts",
+            "version": "v2.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/translation-contracts.git",
+                "reference": "8cc682ac458d75557203b2f2f14b0b92e1c744ed"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/8cc682ac458d75557203b2f2f14b0b92e1c744ed",
+                "reference": "8cc682ac458d75557203b2f2f14b0b92e1c744ed",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2.5"
+            },
+            "suggest": {
+                "symfony/translation-implementation": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Contracts\\Translation\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Generic abstractions related to translation",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "abstractions",
+                "contracts",
+                "decoupling",
+                "interfaces",
+                "interoperability",
+                "standards"
+            ],
+            "time": "2019-11-18T17:27:11+00:00"
         },
         {
             "name": "symfony/yaml",
-            "version": "v3.3.18",
+            "version": "v4.4.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/yaml.git",
-                "reference": "af615970e265543a26ee712c958404eb9b7ac93d"
+                "reference": "ef166890d821518106da3560086bfcbeb4fadfec"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/yaml/zipball/af615970e265543a26ee712c958404eb9b7ac93d",
-                "reference": "af615970e265543a26ee712c958404eb9b7ac93d",
+                "url": "https://api.github.com/repos/symfony/yaml/zipball/ef166890d821518106da3560086bfcbeb4fadfec",
+                "reference": "ef166890d821518106da3560086bfcbeb4fadfec",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5.9|>=7.0.8"
+                "php": "^7.1.3",
+                "symfony/polyfill-ctype": "~1.8"
+            },
+            "conflict": {
+                "symfony/console": "<3.4"
             },
             "require-dev": {
-                "symfony/console": "~2.8|~3.0"
+                "symfony/console": "^3.4|^4.0|^5.0"
             },
             "suggest": {
                 "symfony/console": "For validating YAML files using the lint command"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.4-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony Yaml Component",
             "homepage": "https://symfony.com",
-            "time": "2018-01-20T15:04:53+00:00"
+            "time": "2020-03-30T11:41:10+00:00"
         },
         {
             "name": "theseer/tokenizer",
         },
         {
             "name": "webmozart/assert",
-            "version": "1.7.0",
+            "version": "1.8.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/webmozart/assert.git",
-                "reference": "aed98a490f9a8f78468232db345ab9cf606cf598"
+                "reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/webmozart/assert/zipball/aed98a490f9a8f78468232db345ab9cf606cf598",
-                "reference": "aed98a490f9a8f78468232db345ab9cf606cf598",
+                "url": "https://api.github.com/repos/webmozart/assert/zipball/ab2cb0b3b559010b75981b1bdce728da3ee90ad6",
+                "reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6",
                 "shasum": ""
             },
             "require": {
                 "symfony/polyfill-ctype": "^1.8"
             },
             "conflict": {
-                "vimeo/psalm": "<3.6.0"
+                "vimeo/psalm": "<3.9.1"
             },
             "require-dev": {
                 "phpunit/phpunit": "^4.8.36 || ^7.5.13"
                 "check",
                 "validate"
             ],
-            "time": "2020-02-14T12:15:55+00:00"
+            "time": "2020-04-18T12:12:48+00:00"
         }
     ],
     "aliases": [
index 28cb965..a77b7a0 100644 (file)
@@ -211,7 +211,7 @@ abstract class content {
      *
      * @return bool     True if content could be accessed. False otherwise.
      */
-    public function can_view(): bool {
+    public function is_view_allowed(): bool {
         // There's no capability at content level to check,
         // but plugins can overwrite this method in case they want to check something related to content properties.
         return true;
index 2e89d42..0a21353 100644 (file)
@@ -15,7 +15,7 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Content bank manager class
+ * Content bank class
  *
  * @package    core_contentbank
  * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
@@ -25,7 +25,7 @@
 namespace core_contentbank;
 
 /**
- * Content bank manager class
+ * Content bank class
  *
  * @package    core_contentbank
  * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
@@ -44,8 +44,9 @@ class contentbank {
         $enabledtypes = \core\plugininfo\contenttype::get_enabled_plugins();
         $types = [];
         foreach ($enabledtypes as $name) {
-            $classname = "\\contenttype_$name\\contenttype";
-            if (class_exists($classname)) {
+            $contenttypeclassname = "\\contenttype_$name\\contenttype";
+            $contentclassname = "\\contenttype_$name\\content";
+            if (class_exists($contenttypeclassname) && class_exists($contentclassname)) {
                 $types[] = $name;
             }
         }
@@ -65,16 +66,14 @@ class contentbank {
             $supportedextensions = [];
             foreach ($this->get_enabled_content_types() as $type) {
                 $classname = "\\contenttype_$type\\contenttype";
-                if (class_exists($classname)) {
-                    $manager = new $classname;
-                    if ($manager->is_feature_supported($manager::CAN_UPLOAD)) {
-                        $extensions = $manager->get_manageable_extensions();
-                        foreach ($extensions as $extension) {
-                            if (array_key_exists($extension, $supportedextensions)) {
-                                $supportedextensions[$extension][] = $type;
-                            } else {
-                                $supportedextensions[$extension] = [$type];
-                            }
+                $contenttype = new $classname;
+                if ($contenttype->is_feature_supported($contenttype::CAN_UPLOAD)) {
+                    $extensions = $contenttype->get_manageable_extensions();
+                    foreach ($extensions as $extension) {
+                        if (array_key_exists($extension, $supportedextensions)) {
+                            $supportedextensions[$extension][] = $type;
+                        } else {
+                            $supportedextensions[$extension] = [$type];
                         }
                     }
                 }
@@ -100,12 +99,10 @@ class contentbank {
             foreach ($supportedextensions as $extension => $types) {
                 foreach ($types as $type) {
                     $classname = "\\contenttype_$type\\contenttype";
-                    if (class_exists($classname)) {
-                        $manager = new $classname($context);
-                        if ($manager->can_upload()) {
-                            $contextextensions[$extension] = $type;
-                            break;
-                        }
+                    $contenttype = new $classname($context);
+                    if ($contenttype->can_upload()) {
+                        $contextextensions[$extension] = $type;
+                        break;
                     }
                 }
             }
@@ -155,4 +152,54 @@ class contentbank {
         }
         return null;
     }
+
+    /**
+     * Find the contents with %$search% in the contextid defined.
+     * If contextid and search are empty, all contents are returned.
+     * In all the cases, only the contents for the enabled contentbank-type plugins are returned.
+     *
+     * @param  string|null $search Optional string to search (for now it will search only into the name).
+     * @param  int $contextid Optional contextid to search.
+     * @return array The contents for the enabled contentbank-type plugins having $search as name and placed in $contextid.
+     */
+    public function search_contents(?string $search = null, ?int $contextid = 0): array {
+        global $DB;
+
+        $contents = [];
+
+        // Get only contents for enabled content-type plugins.
+        $contenttypes = array_map(function($contenttypename) {
+            return "contenttype_$contenttypename";
+        }, $this->get_enabled_content_types());
+        if (empty($contenttypes)) {
+            // Early return if there are no content-type plugins enabled.
+            return $contents;
+        }
+
+        list($sqlcontenttypes, $params) = $DB->get_in_or_equal($contenttypes, SQL_PARAMS_NAMED);
+        $sql = " contenttype $sqlcontenttypes ";
+
+        // Filter contents on this context (if defined).
+        if (!empty($contextid)) {
+            $params['contextid'] = $contextid;
+            $sql .= ' AND contextid = :contextid ';
+        }
+
+        // Search for contents having this string (if defined).
+        if (!empty($search)) {
+            $sql .= ' AND ' . $DB->sql_like('name', ':name', false, false);
+            $params['name'] = '%' . $DB->sql_like_escape($search) . '%';
+        }
+
+        $records = $DB->get_records_select('contentbank_content', $sql, $params);
+        foreach ($records as $record) {
+            $contentclass = "\\$record->contenttype\\content";
+            $content = new $contentclass($record);
+            if ($content->is_view_allowed()) {
+                $contents[] = $content;
+            }
+        }
+
+        return $contents;
+    }
 }
index 10ea721..179e80f 100644 (file)
@@ -25,5 +25,5 @@
 $string['pluginname'] = 'H5P';
 $string['pluginname_help'] = 'Content bank to upload and share H5P content';
 $string['privacy:metadata'] = 'The H5P content bank plugin does not store any personal data.';
-$string['h5p:access'] = 'Access to H5P content in the content bank';
-$string['h5p:upload'] = 'Upload new H5P content';
+$string['h5p:access'] = 'Access H5P content in the content bank';
+$string['h5p:upload'] = 'Upload new H5P content';
index 8d03118..61f28ea 100644 (file)
@@ -48,7 +48,7 @@ Feature: H5P file upload to content bank for admins
     And I click on "Save changes" "button"
     And I wait until the page is ready
     And I should see "filltheblanks.h5p"
-    And I navigate to "Plugins > Content bank > Manage content bank content types" in site administration
+    And I navigate to "Plugins > Content bank > Manage content types" in site administration
     And I click on "Disable" "icon" in the "H5P" "table_row"
     And I wait until the page is ready
     When I click on "Content bank" "link"
index 3a3e9c3..bbf9786 100644 (file)
@@ -27,6 +27,7 @@ require('../config.php');
 require_login();
 
 $contextid    = optional_param('contextid', \context_system::instance()->id, PARAM_INT);
+$search = optional_param('search', '', PARAM_CLEAN);
 $context = context::instance_by_id($contextid, MUST_EXIST);
 
 require_capability('moodle/contentbank:access', $context);
@@ -46,27 +47,13 @@ $PAGE->set_heading($title);
 $PAGE->set_pagetype('contenbank');
 
 // Get all contents managed by active plugins to render.
-$foldercontents = array();
-$contents = $DB->get_records('contentbank_content', ['contextid' => $contextid]);
-foreach ($contents as $content) {
-    $plugin = core_plugin_manager::instance()->get_plugin_info($content->contenttype);
-    if (!$plugin || !$plugin->is_enabled()) {
-        continue;
-    }
-    $contentclass = "\\$content->contenttype\\content";
-    if (class_exists($contentclass)) {
-        $contentmanager = new $contentclass($content);
-        if ($contentmanager->can_view()) {
-            $foldercontents[] = $contentmanager;
-        }
-    }
-}
+$cb = new \core_contentbank\contentbank();
+$foldercontents = $cb->search_contents($search, $contextid);
 
 // Get the toolbar ready.
 $toolbar = array ();
 if (has_capability('moodle/contentbank:upload', $context)) {
     // Don' show upload button if there's no plugin to support any file extension.
-    $cb = new \core_contentbank\contentbank();
     $accepted = $cb->get_supported_extensions_as_string($context);
     if (!empty($accepted)) {
         $importurl = new moodle_url('/contentbank/upload.php', ['contextid' => $contextid]);
index 5b1a1dd..2212b81 100644 (file)
 
 }}
 {{>core_contentbank/toolbar}}
-<div class="content-bank-container card">
+<div class="content-bank-container pb-3 border">
     <div class="content-bank">
-        <div class="cb-navbar">
+        <div class="cb-navbar bg-light p-2 border-bottom">
             {{#pix}} i/folder {{/pix}}
         </div>
-        <div class="cb-content-wrapper">
+        <div class="cb-content-wrapper d-flex flex-wrap p-2">
         {{#contents}}
-            <div class="cb-content">
-                <div class="cb-iconview">
-                    <div class="cb-file text-center position-relative">
-                        {{#link}}<a href="{{{ link }}}">{{/link}}
-                            <div style="position:relative;">
-                                <div class="cb-thumbnail text-center d-block" style="width: 110px; height: 110px;">
-                                    {{{ icon }}}
-                                </div>
-                            </div>
-                            <div class="cb-contentname-field position-absolute overflow-visible">
-                                <div class="cb-contentname text-truncate" style="width: 112px;">{{{ name }}}</div>
-                            </div>
-                        {{#link}}</a>{{/link}}
+            <div class="cb-file position-relative mb-2">
+                <div class="p-2">
+                    <div class="cb-thumbnail mb-1 text-center">
+                        {{{ icon }}}
                     </div>
+
+                    {{#link}}
+                        <a href="{{{ link }}}" class="stretched-link" title="{{{name}}}">
+                    {{/link}}
+                            <span class="cb-name word-break-all clamp-2 text-center" >
+                                {{{ name }}}
+                            </span>
+                    {{#link}}
+                        </a>
+                    {{/link}}
                 </div>
             </div>
         {{/contents}}
index 7a041a6..9ab8024 100644 (file)
@@ -37,7 +37,7 @@
         <div class="cb-toolbar float-sm-right">
         {{#tools}}
             {{#link}}<a href="{{{ link }}}" title="{{{ name }}}">{{/link}}
-                <div class="cb-tool btn btn-secondary btn-sm">
+                <div class="cb-tool icon-no-margin btn btn-secondary btn-lg">
                     {{#pix}} {{{ icon }}} {{/pix}}
                 </div>
             {{#link}}</a>{{/link}}
index 4b370b1..b49a79c 100644 (file)
@@ -9,7 +9,15 @@ Feature: Delete H5P file from the content bank
     And I follow "Manage private files..."
     And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Files" filemanager
     And I click on "Save changes" "button"
-    And I click on "Content bank" "link"
+    And I am on site homepage
+    And I turn editing mode on
+    And I add the "Navigation" block if not present
+    And I configure the "Navigation" block
+    And I set the following fields to these values:
+      | Page contexts | Display throughout the entire site |
+    And I press "Save changes"
+    And I click on "Site pages" "list_item" in the "Navigation" "block"
+    And I click on "Content bank" "link" in the "Navigation" "block"
     And I click on "Upload" "link"
     And I click on "Choose a file..." "button"
     And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
@@ -18,14 +26,12 @@ Feature: Delete H5P file from the content bank
     And I click on "Save changes" "button"
 
   Scenario: Admins can delete content from the content bank
-    Given I click on "Content bank" "link"
-    And I wait until the page is ready
-    And I should see "filltheblanks.h5p"
-    When I follow "filltheblanks.h5p"
-    And I open the action menu in "region-main-settings-menu" "region"
+    Given I should see "filltheblanks.h5p"
+    And I follow "filltheblanks.h5p"
+    When I open the action menu in "region-main-settings-menu" "region"
     Then I should see "Delete"
     And I choose "Delete" in the open action menu
-    And I should see "Are you sure you want to delete content 'filltheblanks.h5p'?"
+    And I should see "Are you sure you want to delete the content 'filltheblanks.h5p'"
     And I click on "Cancel" "button" in the "Delete content" "dialogue"
     And I should see "filltheblanks.h5p"
     And I open the action menu in "region-main-settings-menu" "region"
@@ -46,15 +52,15 @@ Feature: Delete H5P file from the content bank
       | user        | role      | contextlevel  | reference     |
       | manager     | manager       | System    |               |
     And I log out
-    When I log in as "manager"
-    And I click on "Content bank" "link"
-    And I wait until the page is ready
+    And I log in as "manager"
+    And I follow "Manage private files..."
+    And I upload "h5p/tests/fixtures/find-the-words.h5p" file to "Files" filemanager
+    And I click on "Save changes" "button"
+    When I click on "Site pages" "list_item" in the "Navigation" "block"
+    And I click on "Content bank" "link" in the "Navigation" "block"
     And I should see "filltheblanks.h5p"
     And I follow "filltheblanks.h5p"
     Then ".header-actions-container" "css_element" should not exist
-    And I click on "Private files" "link"
-    And I upload "h5p/tests/fixtures/find-the-words.h5p" file to "Files" filemanager
-    And I click on "Save changes" "button"
     And I click on "Content bank" "link"
     And I click on "Upload" "link"
     And I click on "Choose a file..." "button"
@@ -62,9 +68,8 @@ Feature: Delete H5P file from the content bank
     And I click on "find-the-words.h5p" "link"
     And I click on "Select this file" "button"
     And I click on "Save changes" "button"
-    And I wait until the page is ready
     And I should see "filltheblanks.h5p"
     And I should see "find-the-words.h5p"
-    When I follow "find-the-words.h5p"
+    And I follow "find-the-words.h5p"
     And I open the action menu in "region-main-settings-menu" "region"
-    Then I should see "Delete"
+    And I should see "Delete"
index 199b73a..824f7a5 100644 (file)
@@ -175,4 +175,143 @@ class core_contentbank_testcase extends advanced_testcase {
         $supporter = $cb->get_extension_supporter($extension, $systemcontext);
         $this->assertEquals($expected, $supporter);
     }
+
+    /**
+     * Test the behaviour of search_contents().
+     *
+     * @dataProvider search_contents_provider
+     * @param  string $search String to search.
+     * @param  int $contextid Contextid to search.
+     * @param  int $expectedresult Expected result.
+     * @param  array $contexts List of contexts where to create content.
+     */
+    public function test_search_contents(?string $search, int $contextid, int $expectedresult, array $contexts = []): void {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Create users.
+        $managerroleid = $DB->get_field('role', 'id', ['shortname' => 'manager']);
+        $manager = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->role_assign($managerroleid, $manager->id);
+
+        // Add some content to the content bank.
+        $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
+        foreach ($contexts as $context) {
+            $records = $generator->generate_contentbank_data('contenttype_h5p', 3,
+                $manager->id, $context, false);
+        }
+
+        // Search for some content.
+        $cb = new \core_contentbank\contentbank();
+        $contents = $cb->search_contents($search, $contextid);
+
+        $this->assertCount($expectedresult, $contents);
+        if (!empty($contents) && !empty($search)) {
+            foreach ($contents as $content) {
+                $this->assertContains($search, $content->get_name());
+            }
+        }
+    }
+
+    /**
+     * Data provider for test_search_contents().
+     *
+     * @return array
+     */
+    public function search_contents_provider(): array {
+        // Create a category and a course.
+        $systemcontext = \context_system::instance();
+        $coursecat = $this->getDataGenerator()->create_category();
+        $course = $this->getDataGenerator()->create_course();
+        $coursecatcontext = \context_coursecat::instance($coursecat->id);
+        $coursecontext = \context_course::instance($course->id);
+
+        return [
+            'Search all content in all contexts' => [
+                null,
+                0,
+                9,
+                [$systemcontext, $coursecatcontext, $coursecontext]
+            ],
+            'Search in all contexts for existing string in all contents' => [
+                'content',
+                0,
+                9,
+                [$systemcontext, $coursecatcontext, $coursecontext]
+            ],
+            'Search in all contexts for unexisting string in all contents' => [
+                'chocolate',
+                0,
+                0,
+                [$systemcontext, $coursecatcontext, $coursecontext]
+            ],
+            'Search in all contexts for existing string in some contents' => [
+                '1',
+                0,
+                3,
+                [$systemcontext, $coursecatcontext, $coursecontext]
+            ],
+            'Search in all contexts for existing string in some contents (create only 1 context)' => [
+                '1',
+                0,
+                1,
+                [$systemcontext]
+            ],
+            'Search in system context for existing string in all contents' => [
+                'content',
+                $systemcontext->id,
+                3,
+                [$systemcontext, $coursecatcontext, $coursecontext]
+            ],
+            'Search in category context for unexisting string in all contents' => [
+                'chocolate',
+                $coursecatcontext->id,
+                0,
+                [$systemcontext, $coursecatcontext, $coursecontext]
+            ],
+            'Search in course context for existing string in some contents' => [
+                '1',
+                $coursecontext->id,
+                1,
+                [$systemcontext, $coursecatcontext, $coursecontext]
+            ],
+            'Search in system context' => [
+                null,
+                $systemcontext->id,
+                3,
+                [$systemcontext, $coursecatcontext, $coursecontext]
+            ],
+            'Search in course context with existing content' => [
+                null,
+                $coursecontext->id,
+                3,
+                [$systemcontext, $coursecatcontext, $coursecontext]
+            ],
+            'Search in course context without existing content' => [
+                null,
+                $coursecontext->id,
+                0,
+                [$systemcontext, $coursecatcontext]
+            ],
+            'Search in an empty contentbank' => [
+                null,
+                0,
+                0,
+                []
+            ],
+            'Search in a context in an empty contentbank' => [
+                null,
+                $systemcontext->id,
+                0,
+                []
+            ],
+            'Search for a string in an empty contentbank' => [
+                'content',
+                0,
+                0,
+                []
+            ],
+        ];
+    }
 }
index 1a96fbf..d055ddc 100644 (file)
@@ -330,6 +330,26 @@ class helper {
                 'string' => new \lang_string('restorecourse', 'admin')
             );
         }
+        // Recyclebyn.
+        if (\tool_recyclebin\category_bin::is_enabled()) {
+            $categorybin = new \tool_recyclebin\category_bin($category->id);
+            if ($categorybin->can_view()) {
+                $autohide = get_config('tool_recyclebin', 'autohide');
+                if ($autohide) {
+                    $items = $categorybin->get_items();
+                } else {
+                    $items = [];
+                }
+                if (!$autohide || !empty($items)) {
+                    $pluginname = get_string('pluginname', 'tool_recyclebin');
+                    $actions['recyclebin'] = [
+                       'url' => new \moodle_url('/admin/tool/recyclebin/index.php', ['contextid' => $category->get_context()->id]),
+                       'icon' => new \pix_icon('trash', $pluginname, 'tool_recyclebin'),
+                       'string' => $pluginname
+                    ];
+                }
+            }
+        }
 
         return $actions;
     }
index 14f20e1..98ef06a 100644 (file)
@@ -18,13 +18,13 @@ Feature: Display and choose from the available activities in course
     And I am on "Course" course homepage with editing mode on
 
   Scenario: The available activities are displayed to the teacher in the activity chooser
-    Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
-    Then I should see "Add an activity or resource" in the ".modal-title" "css_element"
+    Given I click on "Add an activity" "button" in the "Topic 1" "section"
+    Then I should see "Add an activity" in the ".modal-title" "css_element"
     And I should see "Assignment" in the ".modal-body" "css_element"
 
   Scenario: The teacher can choose to add an activity from the activity items in the activity chooser
-    Given I click on "Add an activity or resource" "button" in the "Topic 3" "section"
-    When I click on "Add a new Assignment" "link" in the "Add an activity or resource" "dialogue"
+    Given I click on "Add an activity" "button" in the "Topic 3" "section"
+    When I click on "Add a new Assignment" "link" in the "Add an activity" "dialogue"
     Then I should see "Adding a new Assignment"
     And I set the following fields to these values:
       | Assignment name | Test Assignment Topic 3 |
@@ -32,19 +32,19 @@ Feature: Display and choose from the available activities in course
     Then I should see "Test Assignment Topic 3" in the "Topic 3" "section"
 
   Scenario: The teacher can choose to add an activity from the activity summary in the activity chooser
-    Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
-    When I click on "Information about the Assignment activity" "button" in the "Add an activity or resource" "dialogue"
+    Given I click on "Add an activity" "button" in the "Topic 1" "section"
+    When I click on "Information about the Assignment activity" "button" in the "Add an activity" "dialogue"
     When I click on "Add a new Assignment" "link" in the "help" "core_course > Activity chooser screen"
     Then I should see "Adding a new Assignment"
 
   Scenario: Show summary
-    Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
-    When I click on "Information about the Assignment activity" "button" in the "Add an activity or resource" "dialogue"
+    Given I click on "Add an activity" "button" in the "Topic 1" "section"
+    When I click on "Information about the Assignment activity" "button" in the "Add an activity" "dialogue"
     Then I should see "Assignment" in the "help" "core_course > Activity chooser screen"
     And I should see "The assignment activity module enables a teacher to communicate tasks, collect work and provide grades and feedback."
 
   Scenario: Hide summary
-    Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
+    Given I click on "Add an activity" "button" in the "Topic 1" "section"
     When I click on "Information about the Assignment activity" "button" in the "modules" "core_course > Activity chooser screen"
     And I should see "The assignment activity module enables a teacher to communicate tasks, collect work and provide grades and feedback." in the "help" "core_course > Activity chooser screen"
     And I should see "Back" in the "help" "core_course > Activity chooser screen"
@@ -52,7 +52,7 @@ Feature: Display and choose from the available activities in course
     Then "modules" "core_course > Activity chooser screen" should exist
     And "help" "core_course > Activity chooser screen" should not exist
     And "Back" "button" should not exist in the "modules" "core_course > Activity chooser screen"
-    And I should not see "The assignment activity module enables a teacher to communicate tasks, collect work and provide grades and feedback." in the "Add an activity or resource" "dialogue"
+    And I should not see "The assignment activity module enables a teacher to communicate tasks, collect work and provide grades and feedback." in the "Add an activity" "dialogue"
 
   Scenario: View recommended activities
     When I log out
@@ -65,85 +65,85 @@ Feature: Display and choose from the available activities in course
     And I log in as "teacher"
     And I am on "Course" course homepage with editing mode on
     And I open the activity chooser
-    Then I should see "Recommended" in the "Add an activity or resource" "dialogue"
-    And I click on "Recommended" "link" in the "Add an activity or resource" "dialogue"
+    Then I should see "Recommended" in the "Add an activity" "dialogue"
+    And I click on "Recommended" "link" in the "Add an activity" "dialogue"
     And I should see "Book" in the "recommended" "core_course > Activity chooser tab"
 
   Scenario: Favourite a module in the activity chooser
     Given I open the activity chooser
-    And I should not see "Starred" in the "Add an activity or resource" "dialogue"
-    And I click on "Star Assignment module" "button" in the "Add an activity or resource" "dialogue"
-    And I should see "Starred" in the "Add an activity or resource" "dialogue"
-    When I click on "Starred" "link" in the "Add an activity or resource" "dialogue"
+    And I should not see "Starred" in the "Add an activity" "dialogue"
+    And I click on "Star Assignment activity" "button" in the "Add an activity" "dialogue"
+    And I should see "Starred" in the "Add an activity" "dialogue"
+    When I click on "Starred" "link" in the "Add an activity" "dialogue"
     Then I should see "Assignment" in the "favourites" "core_course > Activity chooser tab"
     And I click on "Information about the Assignment activity" "button" in the "favourites" "core_course > Activity chooser tab"
     And I should see "The assignment activity module enables a teacher to communicate tasks, collect work and provide grades and feedback."
 
   Scenario: Add a favourite module and check it exists when reopening the chooser
     Given I open the activity chooser
-    And I click on "Star Assignment module" "button" in the "Add an activity or resource" "dialogue"
-    And I click on "Star Forum module" "button" in the "Add an activity or resource" "dialogue"
-    And I should see "Starred" in the "Add an activity or resource" "dialogue"
-    And I click on "Close" "button" in the "Add an activity or resource" "dialogue"
-    When I click on "Add an activity or resource" "button" in the "Topic 3" "section"
-    And I click on "Starred" "link" in the "Add an activity or resource" "dialogue"
+    And I click on "Star Assignment activity" "button" in the "Add an activity" "dialogue"
+    And I click on "Star Forum activity" "button" in the "Add an activity" "dialogue"
+    And I should see "Starred" in the "Add an activity" "dialogue"
+    And I click on "Close" "button" in the "Add an activity" "dialogue"
+    When I click on "Add an activity" "button" in the "Topic 3" "section"
+    And I click on "Starred" "link" in the "Add an activity" "dialogue"
     Then I should see "Forum" in the "favourites" "core_course > Activity chooser tab"
 
   Scenario: Add a favourite and then remove it whilst checking the tabs work as expected
     Given I open the activity chooser
-    And I click on "Star Assignment module" "button" in the "Add an activity or resource" "dialogue"
-    And I click on "Starred" "link" in the "Add an activity or resource" "dialogue"
-    And I click on "Star Assignment module" "button" in the "Add an activity or resource" "dialogue"
-    Then I should not see "Starred" in the "Add an activity or resource" "dialogue"
+    And I click on "Star Assignment activity" "button" in the "Add an activity" "dialogue"
+    And I click on "Starred" "link" in the "Add an activity" "dialogue"
+    And I click on "Star Assignment activity" "button" in the "Add an activity" "dialogue"
+    Then I should not see "Starred" in the "Add an activity" "dialogue"
 
   Scenario: The teacher can search for an activity by it's name
-    Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
+    Given I click on "Add an activity" "button" in the "Topic 1" "section"
     When I set the field "search" to "Lesson"
-    Then I should see "1 results found" in the "Add an activity or resource" "dialogue"
-    And I should see "Lesson" in the "Add an activity or resource" "dialogue"
+    Then I should see "1 results found" in the "Add an activity" "dialogue"
+    And I should see "Lesson" in the "Add an activity" "dialogue"
 
   Scenario: The teacher can search for an activity by it's description
     Given I open the activity chooser
     When I set the field "search" to "The lesson activity module enables a teacher to deliver content"
-    Then I should see "1 results found" in the "Add an activity or resource" "dialogue"
-    And I should see "Lesson" in the "Add an activity or resource" "dialogue"
+    Then I should see "1 results found" in the "Add an activity" "dialogue"
+    And I should see "Lesson" in the "Add an activity" "dialogue"
 
   Scenario: Search results are not returned if the search query does not match any activity name or description
-    Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
+    Given I click on "Add an activity" "button" in the "Topic 1" "section"
     When I set the field "search" to "Random search query"
-    Then I should see "0 results found" in the "Add an activity or resource" "dialogue"
+    Then I should see "0 results found" in the "Add an activity" "dialogue"
     And ".option" "css_element" should not exist in the ".searchresultitemscontainer" "css_element"
 
   Scenario: Teacher can return to the default activity chooser state by manually removing the search query
-    Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
+    Given I click on "Add an activity" "button" in the "Topic 1" "section"
     And I set the field "search" to "Lesson"
-    And I should see "1 results found" in the "Add an activity or resource" "dialogue"
-    And I should see "Lesson" in the "Add an activity or resource" "dialogue"
+    And I should see "1 results found" in the "Add an activity" "dialogue"
+    And I should see "Lesson" in the "Add an activity" "dialogue"
     When I set the field "search" to ""
-    And I should not see "1 results found" in the "Add an activity or resource" "dialogue"
+    And I should not see "1 results found" in the "Add an activity" "dialogue"
     Then ".searchresultscontainer" "css_element" should not exist
     And ".optionscontainer" "css_element" should exist
 
   Scenario: Teacher can not see a "clear" button if a search query is not entered in the activity chooser search bar
-    When I click on "Add an activity or resource" "button" in the "Topic 1" "section"
+    When I click on "Add an activity" "button" in the "Topic 1" "section"
     Then "Clear search input" "button" should not exist
 
   Scenario: Teacher can see a "clear" button after entering a search query in the activity chooser search bar
-    Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
+    Given I click on "Add an activity" "button" in the "Topic 1" "section"
     When I set the field "search" to "Search query"
     Then "Clear search input" "button" should not exist
 
   Scenario: Teacher can not see a "clear" button if the search query is removed in the activity chooser search bar
-    Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
+    Given I click on "Add an activity" "button" in the "Topic 1" "section"
     And I set the field "search" to "Search query"
     And "Clear search input" "button" should exist
     When I set the field "search" to ""
     Then "Clear search input" "button" should not exist
 
   Scenario: Teacher can instantly remove the search query from the activity search bar by clicking on the "clear" button
-    Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
+    Given I click on "Add an activity" "button" in the "Topic 1" "section"
     And I set the field "search" to "Search query"
-    And I should see "results found" in the "Add an activity or resource" "dialogue"
+    And I should see "results found" in the "Add an activity" "dialogue"
     When I click on "Clear search input" "button"
     Then I should not see "Search query"
     And ".searchresultscontainer" "css_element" should not exist
index 6c9aebd..319ef65 100644 (file)
@@ -74,7 +74,7 @@ $string['insertcomment'] = 'Insert frequently used comment';
 $string['maxscore'] = 'Maximum score';
 $string['name'] = 'Name';
 $string['needregrademessage'] = 'The marking guide definition was changed after this student had been graded. The student can not see this marking guide until you check the marking guide and update the grade.';
-$string['outof'] = 'Out of {$a}';
+$string['outof'] = 'Score out of {$a}';
 $string['pluginname'] = 'Marking guide';
 $string['previewmarkingguide'] = 'Preview marking guide';
 $string['privacy:metadata:criterionid'] = 'An identifier to a criterion for advanced marking.';
index 745a244..9982217 100644 (file)
@@ -138,7 +138,7 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
         if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
             $value = get_string('criterionaddlevel', 'gradingform_rubric');
             $button = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][addlevel]',
-                'id' => '{NAME}-criteria-{CRITERION-id}-levels-addlevel', 'value' => $value));
+                'id' => '{NAME}-criteria-{CRITERION-id}-levels-addlevel', 'value' => $value, 'class' => 'btn btn-secondary'));
             $criteriontemplate .= html_writer::tag('td', $button, array('class' => 'addlevel'));
         }
         $displayremark = ($options['enableremarks'] && ($mode != gradingform_rubric_controller::DISPLAY_VIEW || $options['showremarksstudent']));
index 3b9a2d2..460fbb9 100644 (file)
 
 .gradingform_rubric.editor .addcriterion input,
 .gradingform_rubric.editor .addlevel input {
-    background: transparent url([[pix:t/add]]) no-repeat top left;
+    background: #fff url([[pix:t/add]]) no-repeat 7px 8px;
     display: block;
     color: #555;
     font-weight: bold;
 }
 
 .gradingform_rubric.editor .addcriterion input {
-    background-position: 5px 8px;
     height: 30px;
     line-height: 29px;
     margin-bottom: 14px;
 }
 
 .gradingform_rubric.editor .addlevel input {
-    background-position: 5px 5px;
-    height: 25px;
-    line-height: 24px;
-    margin-bottom: 45px;
-    padding-left: 18px;
+    padding-left: 24px;
     padding-right: 8px;
 }
 
index ca5ea1a..222f128 100644 (file)
@@ -25,5 +25,5 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['pluginname'] = 'H5P framework v1.24';
-$string['pluginname_help'] = 'H5P framework. Version 1.24';
-$string['privacy:metadata'] = 'H5P framework v1.24 do not store any personal data.';
+$string['pluginname_help'] = 'H5P framework version 1.24.';
+$string['privacy:metadata'] = 'The H5P framework v1.24 does not store any personal data.';
index 99135ce..b763c30 100644 (file)
@@ -30,5 +30,5 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$string['thisdirection'] = 'ltr';
+$string['thisdirection'] = 'rtl';
 $string['thislanguage'] = 'پښتو';
index b736c9b..9370d23 100644 (file)
@@ -236,7 +236,6 @@ $string['configenablemobilewebservice'] = 'Enable mobile service for the officia
 $string['configenablerssfeeds'] = 'If enabled, RSS feeds are generated by various features across the site, such as blogs, forums, database activities and glossaries. Note that RSS feeds also need to be enabled for the particular activity modules.';
 $string['configenablerssfeedsdisabled'] = 'It is not available because RSS feeds are disabled in all the Site. To enable them, go to the Variables settings under Admin Configuration.';
 $string['configenablerssfeedsdisabled2'] = 'RSS feeds are currently disabled at site level. They may be enabled in Advanced features in the Site administration.';
-$string['configenablesafebrowserintegration'] = 'This adds the choice \'Require Safe Exam Browser\' to the \'Browser security\' field on the quiz settings form. See https://www.safeexambrowser.org/ for more information.';
 $string['configenablestats'] = 'If you choose \'yes\' here, Moodle\'s cronjob will process the logs and gather some statistics.  Depending on the amount of traffic on your site, this can take awhile. If you enable this, you will be able to see some interesting graphs and statistics about each of your courses, or on a sitewide basis.';
 $string['configenabletrusttext'] = 'By default Moodle will always thoroughly clean text that comes from users to remove any possible bad scripts, media etc that could be a security risk.  The Trusted Content system is a way of giving particular users that you trust the ability to include these advanced features in their content without interference.  To enable this system, you need to first enable this setting, and then grant the Trusted Content permission to a specific Moodle role.  Texts created or uploaded by such users will be marked as trusted and will not be cleaned before display.';
 $string['configenablewebservices'] = 'Web services enable other systems, such as the Moodle app, to log in to the site and perform operations. For extra security, the setting should be disabled if you are not using the app, or an external tool/service that requires integration via web services.';
@@ -330,7 +329,7 @@ $string['configrequestedstudentname'] = 'Word for student used in requested cour
 $string['configrequestedstudentsname'] = 'Word for students used in requested courses';
 $string['configrequestedteachername'] = 'Word for teacher used in requested courses';
 $string['configrequestedteachersname'] = 'Word for teachers used in requested courses';
-$string['configreverseproxyignore'] = 'If your server is behind multiple reverse proxies that append to the X-Forwarded-For header then you will need to specify a comma separated list of ip addresses or subnets of the reverse proxies to be ignored in order to find the users correct IP address.';
+$string['configreverseproxyignore'] = 'If your server is behind multiple reverse proxies that append to the X-Forwarded-For header, then specify a comma-separated list of IP addresses or subnets of the reverse proxies to be ignored in order to find the user\'s correct IP address.';
 $string['configsectioninterface'] = 'Interface';
 $string['configsectionmail'] = 'Mail';
 $string['configsectionmaintenance'] = 'Maintenance';
@@ -545,7 +544,6 @@ $string['enablemoodlenet'] = 'Enable integration with MoodleNet instances';
 $string['enablemoodlenet_desc'] = 'If enabled, and provided the MoodleNet plugin is installed, users can import content from MoodleNet into this site.';
 $string['enablerecordcache'] = 'Enable record cache';
 $string['enablerssfeeds'] = 'Enable RSS feeds';
-$string['enablesafebrowserintegration'] = 'Enable Safe Exam Browser integration';
 $string['enablesearchareas'] = 'Enable search areas';
 $string['enablestats'] = 'Enable statistics';
 $string['enabletrusttext'] = 'Enable trusted content';
@@ -1220,10 +1218,10 @@ $string['task_scheduled_concurrency_limit'] = 'Scheduled task concurrency limit'
 $string['task_scheduled_concurrency_limit_desc'] = 'The number of scheduled task runners allowed to run concurrently. If the limit is high then the server may experience high load which affects performance. A setting of 0 will disable processing of scheduled tasks completely.';
 $string['task_scheduled_max_runtime'] = 'Scheduled task runner lifetime';
 $string['task_scheduled_max_runtime_desc'] = 'The age of a scheduled task runner before it is freed.';
-$string['task_adhoc_concurrency_limit'] = 'Adhoc task concurrency limit';
-$string['task_adhoc_concurrency_limit_desc'] = 'The number of adhoc task runners allowed to run concurrently. If the limit is high then scheduled tasks may not run regularly when there are lots of adhoc tasks. A setting of 0 will disable processing of adhoc tasks completely.';
-$string['task_adhoc_max_runtime'] = 'Adhoc task runner lifetime';
-$string['task_adhoc_max_runtime_desc'] = 'The age of an adhoc task runner before it is freed. A low duration is recommended as there is no limit to the number of adhoc tasks queued. If this number is too high and you have a large adhoc task queue then scheduled tasks may not be run regularly.';
+$string['task_adhoc_concurrency_limit'] = 'Ad hoc task concurrency limit';
+$string['task_adhoc_concurrency_limit_desc'] = 'The number of ad hoc task runners allowed to run concurrently. If the limit is high then scheduled tasks may not run regularly when there are lots of ad hoc tasks. A setting of 0 will disable processing of ad hoc tasks completely.';
+$string['task_adhoc_max_runtime'] = 'Ad hoc task runner lifetime';
+$string['task_adhoc_max_runtime_desc'] = 'The age of an ad hoc task runner before it is freed. A low duration is recommended as there is no limit to the number of ad hoc tasks queued. If this number is too high and you have a large ad hoc task queue then scheduled tasks may not be run regularly.';
 $string['task_logmode'] = 'When to log';
 $string['task_logmode_desc'] = 'You can choose when you wish task logging to take place. By default logs are always captured. You can disable logging entirely, or change to only log tasks which fail.';
 $string['task_logmode_none'] = 'Do not log anything';
index 862ed68..eb3dc7b 100644 (file)
@@ -81,7 +81,7 @@ $string['cachedef_string'] = 'Language string cache';
 $string['cachedef_tags'] = 'Tags collections and areas';
 $string['cachedef_temp_tables'] = 'Temporary tables cache';
 $string['cachedef_userselections'] = 'Data used to persist user selections throughout Moodle';
-$string['cachedef_user_favourite_course_content_items'] = 'User\'s favourite content items (activities, resources and their subtypes)';
+$string['cachedef_user_favourite_course_content_items'] = 'User\'s starred items';
 $string['cachedef_user_group_groupings'] = 'User\'s groupings and groups per course';
 $string['cachedef_user_course_content_items'] = 'User\'s content items (activities, resources and their subtypes) per course';
 $string['cachedef_yuimodules'] = 'YUI Module definitions';
index 80b39b0..4916a4d 100644 (file)
@@ -26,14 +26,14 @@ $string['author'] = 'Author';
 $string['contentdeleted'] = 'The content has been deleted.';
 $string['contentnotdeleted'] = 'An error was encountered while trying to delete the content.';
 $string['deletecontent'] = 'Delete content';
-$string['deletecontentconfirm'] = '<p>Are you sure you want to delete content <em>\'{$a->name}\'</em>? It will remove the content and all its files.</p><p>This operation can not be undone.</p>';
+$string['deletecontentconfirm'] = 'Are you sure you want to delete the content <em>\'{$a->name}\'</em> and all associated files? This action cannot be undone.';
 $string['file'] = 'Upload content';
 $string['file_help'] = 'Files may be stored in the content bank for use in courses. Only files used by content types enabled on the site may be uploaded.';
 $string['name'] = 'Content';
-$string['nopermissiontodelete'] = 'You have no permissions to delete the content.';
+$string['nopermissiontodelete'] = 'You do not have permission to delete content.';
 $string['privacy:metadata:userid'] = 'The ID of the user creating or modifying content bank content.';
-$string['privacy:metadata:content:usercreated'] = 'The user has created the content.';
-$string['privacy:metadata:content:usermodified'] = 'Last user has modified the content.';
+$string['privacy:metadata:content:usercreated'] = 'The user who created the content.';
+$string['privacy:metadata:content:usermodified'] = 'The last user who modified the content.';
 $string['timecreated'] = 'Time created';
-$string['unsupported'] = 'This content type is not supported';
+$string['unsupported'] = 'This content type is not supported.';
 $string['upload'] = 'Upload';
index c373515..c6a8d9a 100644 (file)
@@ -27,11 +27,11 @@ $string['aria:coursecategory'] = 'Course category';
 $string['aria:courseimage'] = 'Course image';
 $string['aria:courseshortname'] = 'Course short name';
 $string['aria:coursename'] = 'Course name';
-$string['aria:defaulttab'] = 'The default modules';
+$string['aria:defaulttab'] = 'Default activities';
 $string['aria:favourite'] = 'Course is starred';
-$string['aria:favouritestab'] = 'Your starred modules';
-$string['aria:recommendedtab'] = 'The recommended modules';
-$string['aria:modulefavourite'] = 'Star {$a} module';
+$string['aria:favouritestab'] = 'Starred activities';
+$string['aria:recommendedtab'] = 'Recommended activities';
+$string['aria:modulefavourite'] = 'Star {$a} activity';
 $string['coursealreadyfinished'] = 'Course already finished';
 $string['coursenotyetstarted'] = 'The course has not yet started';
 $string['coursenotyetfinished'] = 'The course has not yet finished';
@@ -48,7 +48,7 @@ $string['errorendbeforestart'] = 'The end date ({$a}) is before the course start
 $string['favourite'] = 'Starred course';
 $string['gradetopassnotset'] = 'This course does not have a grade to pass set. It may be set in the grade item of the course (Gradebook setup).';
 $string['informationformodule'] = 'Information about the {$a} activity';
-$string['module'] = 'Module';
+$string['module'] = 'Activity';
 $string['nocourseactivity'] = 'Not enough course activity between the start and the end of the course';
 $string['nocourseendtime'] = 'The course does not have an end time';
 $string['nocoursesections'] = 'No course sections';
index 9e70441..6fcb077 100644 (file)
@@ -64,6 +64,7 @@ $string['enrolmentnewuser'] = '{$a->user} has enrolled in course "{$a->course}"'
 $string['enrolmentmethod'] = 'Enrolment method';
 $string['enrolments'] = 'Enrolments';
 $string['enrolmentoptions'] = 'Enrolment options';
+$string['enrolmentupdatedforuser'] = 'The enrolment for user "{$a->fullname}" has been updated';
 $string['enrolnotpermitted'] = 'You do not have permission or are not allowed to enrol someone in this course';
 $string['enrolperiod'] = 'Enrolment duration';
 $string['enrolusage'] = 'Instances / enrolments';
@@ -136,6 +137,7 @@ $string['totalunenrolledusers'] = '{$a} unenrolled users';
 $string['totalotherusers'] = '{$a} other users';
 $string['unassignnotpermitted'] = 'You do not have permission to unassign roles in this course';
 $string['unenrol'] = 'Unenrol';
+$string['unenrolleduser'] = 'The user "{$a->fullname}" was unenrolled from the course';
 $string['unenrolconfirm'] = 'Do you really want to unenrol "{$a->user}" (previously enrolled via "{$a->enrolinstancename}") from "{$a->course}"?';
 $string['unenrolme'] = 'Unenrol me from {$a}';
 $string['unenrolnotpermitted'] = 'You do not have permission or can not unenrol this user from this course.';
index a1f5006..cf78c4c 100644 (file)
@@ -181,15 +181,15 @@ $string['commentmisconf'] = 'Comment ID is misconfigured';
 $string['componentisuptodate'] = 'Component is up-to-date';
 $string['confirmationnotenabled'] = 'User confirmation is not enabled on this site';
 $string['confirmsesskeybad'] = 'Sorry, but your session key could not be confirmed to carry out this action.  This security feature prevents against accidental or malicious execution of important functions in your name.  Please make sure you really wanted to execute this function.';
-$string['contenttypenotfound'] = 'The \'{$a}\' content bank type doesn\'t exist or is not recognized';
+$string['contenttypenotfound'] = 'The \'{$a}\' content bank type doesn\'t exist or is not recognised.';
 $string['couldnotassignrole'] = 'A serious but unspecified error occurred while trying to assign a role to you';
 $string['couldnotupdatenoexistinguser'] = 'Cannot update the user - user doesn\'t exist';
 $string['couldnotverifyagedigitalconsent'] = 'An error occurred while trying to verify the age of digital consent.<br />Please contact administrator.';
 $string['countriesphpempty'] = 'Error: The file countries.php in language pack {$a} is empty or missing.';
 $string['coursedoesnotbelongtocategory'] = 'The course doesn\'t belong to this category';
-$string['courseformatnotfound'] = 'The course format \'{$a}\' doesn\'t exist or is not recognized';
+$string['courseformatnotfound'] = 'The course format \'{$a}\' doesn\'t exist or is not recognised.';
 $string['coursegroupunknown'] = 'Course corresponding to group {$a} not specified';
-$string['courseidnotfound'] = 'Course id doesn\'t exist';
+$string['courseidnotfound'] = 'The course ID doesn\'t exist.';
 $string['courseidnumbertaken'] = 'ID number is already used for another course ({$a})';
 $string['coursemisconf'] = 'Course is misconfigured';
 $string['courserequestdisabled'] = 'Sorry, but course requests have been disabled by the administrator.';
index b9858b5..d640425 100644 (file)
@@ -152,7 +152,7 @@ $string['noextension'] = 'The file you uploaded is not a valid HTML5 Package. (I
 $string['noh5plibhandlerdefined'] = 'There isn\'t any H5P framework handler installed, so H5P content can\'t be displayed.';
 $string['nojson'] = 'The main h5p.json file is not valid';
 $string['nopermissiontodeploy'] = 'This file can\'t be displayed because it has been uploaded by a user without the required capability to deploy H5P content.';
-$string['nopermissiontoedit'] = 'You do not have permission to edit H5P content';
+$string['nopermissiontoedit'] = 'You do not have permission to edit H5P content.';
 $string['notrustablefile'] = 'This file can\'t be displayed because it has been uploaded by a user without the capability to update H5P content types.  Please contact your administrator to ask for the content type to be installed.';
 $string['nounzip'] = 'The file you uploaded is not a valid HTML5 Package. (It is not possible to unzip it.)';
 $string['offlineDialogBody'] = 'We were unable to send information about your completion of this task. Please check your internet connection.';
index eae72de..f0a53e9 100644 (file)
@@ -70,7 +70,7 @@ $string['addnewuser'] = 'Add a new user';
 $string['addnousersrecip'] = 'Add users who haven\'t accessed this {$a} to recipient list';
 $string['addpagehere'] = 'Add text here';
 $string['addresource'] = 'Add a resource...';
-$string['addresourceoractivity'] = 'Add an activity or resource';
+$string['addresourceoractivity'] = 'Add an activity';
 $string['addresourcetosection'] = 'Add a resource to section \'{$a}\'';
 $string['address'] = 'Address';
 $string['addsections'] = 'Add sections';
@@ -176,7 +176,7 @@ $string['backtohome'] = 'Back to the site home';
 $string['backtopageyouwereon'] = 'Back to the page you were on';
 $string['backup'] = 'Backup';
 $string['backupactivehelp'] = 'Choose whether or not to do automated backups.';
-$string['backupadhocpending'] = 'Course backup adhoc task pending';
+$string['backupadhocpending'] = 'Course backup ad hoc task pending';
 $string['backupcancelled'] = 'Backup cancelled';
 $string['backupcoursefileshelp'] = 'If enabled then course files will be included in automated backups';
 $string['backupdate'] = 'Backup date';
@@ -1002,7 +1002,7 @@ $string['changepassword'] = 'Change password';
 $string['changessaved'] = 'Changes saved';
 $string['check'] = 'Check';
 $string['checks'] = 'Checks';
-$string['checksok'] = 'All \'{$a}\' checks ok';
+$string['checksok'] = 'All \'{$a}\' checks OK';
 $string['checkall'] = 'Check all';
 $string['checkingbackup'] = 'Checking backup';
 $string['checkingcourse'] = 'Checking course';
@@ -1183,7 +1183,7 @@ $string['makethismyhome'] = 'Make this my home page';
 $string['makeunavailable'] = 'Make unavailable';
 $string['manageblocks'] = 'Blocks';
 $string['managecategorythis'] = 'Manage this category';
-$string['managecontentbanktypes'] = 'Manage content bank content types';
+$string['managecontentbanktypes'] = 'Manage content types';
 $string['managecourses'] = 'Manage courses';
 $string['managedataformats'] = 'Manage data formats';
 $string['managedatabase'] = 'Database';
@@ -1297,7 +1297,7 @@ $string['moodleversion'] = 'Moodle version';
 $string['moodlerelease'] = 'Moodle release';
 $string['more'] = 'more';
 $string['morehelp'] = 'More help';
-$string['morehelpaboutmodule'] = 'More help about the {$a} module';
+$string['morehelpaboutmodule'] = 'More help about the {$a} activity';
 $string['moreinfo'] = 'More info';
 $string['moreinformation'] = 'More information about this error';
 $string['moreprofileinfoneeded'] = 'Please tell us more about yourself';
@@ -1619,7 +1619,7 @@ $string['privacy:metadata:log:module'] = 'module';
 $string['privacy:metadata:log:time'] = 'The time when the action took place';
 $string['privacy:metadata:log:url'] = 'The URL related to the event';
 $string['privacy:metadata:log:userid'] = 'The ID of the user who performed the action';
-$string['privacy:metadata:task_adhoc'] = 'The status of adhoc tasks.';
+$string['privacy:metadata:task_adhoc'] = 'The status of ad hoc tasks.';
 $string['privacy:metadata:task_adhoc:component'] = 'The component owning the task.';
 $string['privacy:metadata:task_adhoc:nextruntime'] = 'The earliest time to run this task.';
 $string['privacy:metadata:task_adhoc:userid'] = 'The user to run the task as.';
index bc6d094..7ed41e6 100644 (file)
@@ -188,8 +188,8 @@ $string['norepositoriesexternalavailable'] = 'Sorry, none of your current reposi
 $string['notyourinstances'] = 'You can not view/edit repository instances of another user';
 $string['off'] = 'Enabled but hidden';
 $string['original'] = 'Original';
-$string['originalextensionchange'] = 'The original file extension has been modified as a part of the file name change. Changing the extension from ".{$a->originalextension}" to ".{$a->newextension}" could potentially cause some side effects.';
-$string['originalextensionremove'] = 'The original file extension has been removed as a part of the file name change. Removing the extension ".{$a}" could potentially cause some side effects.';
+$string['originalextensionchange'] = 'The original file extension has been modified as a part of the file name change. Changing the extension from ".{$a->originalextension}" to ".{$a->newextension}" may result in a file which cannot be opened.';
+$string['originalextensionremove'] = 'The original file extension has been removed as a part of the file name change. Removing the extension ".{$a}" is likely to result in a file which cannot be opened.';
 $string['openpicker'] = 'Choose a file...';
 $string['operation'] = 'Operation';
 $string['on'] = 'Enabled and visible';
index a37ebed..12a250e 100644 (file)
@@ -151,9 +151,9 @@ $string['confirmunassigntitle'] = 'Confirm role change';
 $string['confirmunassignyes'] = 'Remove';
 $string['confirmunassignno'] = 'Cancel';
 $string['contentbank:access'] = 'Access the content bank';
-$string['contentbank:deleteanycontent'] = 'Delete any content from the content bank on the site';
-$string['contentbank:deleteowncontent'] = 'Delete content from the content bank created by the user';
-$string['contentbank:upload'] = 'Upload new content in the content bank';
+$string['contentbank:deleteanycontent'] = 'Delete any content from the content bank';
+$string['contentbank:deleteowncontent'] = 'Delete content from own content bank';
+$string['contentbank:upload'] = 'Upload content to the content bank';
 $string['context'] = 'Context';
 $string['course:activityvisibility'] = 'Hide/show activities';
 $string['course:bulkmessaging'] = 'Send a message to many people';
@@ -183,7 +183,7 @@ $string['course:markcomplete'] = 'Mark users as complete in course completion';
 $string['course:movesections'] = 'Move sections';
 $string['course:overridecompletion'] = 'Override activity completion status';
 $string['course:renameroles'] = 'Rename roles';
-$string['course:recommendactivity'] = 'Recommend activities to the activity chooser';
+$string['course:recommendactivity'] = 'Recommend activities in the activity chooser';
 $string['course:request'] = 'Request new courses';
 $string['course:reset'] = 'Reset course';
 $string['course:reviewotherusers'] = 'Review other users';
index f6170bb..1d3b68b 100644 (file)
Binary files a/lib/amd/build/chart_base.min.js and b/lib/amd/build/chart_base.min.js differ
index a67c58f..0307da5 100644 (file)
Binary files a/lib/amd/build/chart_base.min.js.map and b/lib/amd/build/chart_base.min.js.map differ
index 491017c..0f6373b 100644 (file)
Binary files a/lib/amd/build/chart_output_chartjs.min.js and b/lib/amd/build/chart_output_chartjs.min.js differ
index 39844cb..00452c6 100644 (file)
Binary files a/lib/amd/build/chart_output_chartjs.min.js.map and b/lib/amd/build/chart_output_chartjs.min.js.map differ
index 20e78fd..6153da1 100644 (file)
Binary files a/lib/amd/build/chart_series.min.js and b/lib/amd/build/chart_series.min.js differ
index 6ad12f0..3e8aa88 100644 (file)
Binary files a/lib/amd/build/chart_series.min.js.map and b/lib/amd/build/chart_series.min.js.map differ
index 969d782..f2ddae7 100644 (file)
Binary files a/lib/amd/build/custom_interaction_events.min.js and b/lib/amd/build/custom_interaction_events.min.js differ
index 5db8445..7e05919 100644 (file)
Binary files a/lib/amd/build/custom_interaction_events.min.js.map and b/lib/amd/build/custom_interaction_events.min.js.map differ
index 3c718e1..1cf0ff5 100644 (file)
@@ -58,6 +58,14 @@ define(['core/chart_series', 'core/chart_axis'], function(Series, Axis) {
      */
     Base.prototype._labels = null;
 
+    /**
+     * Options for chart legend display.
+     *
+     * @protected
+     * @type {Object}
+     */
+    Base.prototype._legendOptions = null;
+
     /**
      * The title of the chart.
      *
@@ -144,6 +152,9 @@ define(['core/chart_series', 'core/chart_axis'], function(Series, Axis) {
         Chart.setConfigColorSet(data.config_colorset);
         Chart.setLabels(data.labels);
         Chart.setTitle(data.title);
+        if (data.legend_options) {
+            Chart.setLegendOptions(data.legend_options);
+        }
         data.series.forEach(function(seriesData) {
             Chart.addSeries(Series.prototype.create(seriesData));
         });
@@ -203,6 +214,15 @@ define(['core/chart_series', 'core/chart_axis'], function(Series, Axis) {
         return this._labels;
     };
 
+    /**
+     * Get whether to display the chart legend.
+     *
+     * @return {Bool}
+     */
+    Base.prototype.getLegendOptions = function() {
+        return this._legendOptions;
+    };
+
     /**
      * Get the series.
      *
@@ -312,6 +332,18 @@ define(['core/chart_series', 'core/chart_axis'], function(Series, Axis) {
         this._labels = labels;
     };
 
+    /**
+     * Set options for chart legend display.
+     *
+     * @param {Object} legendOptions
+     */
+    Base.prototype.setLegendOptions = function(legendOptions) {
+        if (typeof legendOptions !== 'object') {
+            throw new Error('Setting legend with non-object value:' + legendOptions);
+        }
+        this._legendOptions = legendOptions;
+    };
+
     /**
      * Set the title of the chart.
      *
index 4f0a0b0..024cd0c 100644 (file)
@@ -203,6 +203,11 @@ define([
                 }
             }
         };
+        var legendOptions = this._chart.getLegendOptions();
+        if (legendOptions) {
+            config.options.legend = legendOptions;
+        }
+
 
         this._chart.getXAxes().forEach(function(axis, i) {
             var axisLabels = axis.getLabels();
@@ -256,7 +261,7 @@ define([
                 label: this._cleanData(series.getLabel()),
                 data: series.getValues(),
                 type: series.getType(),
-                fill: false,
+                fill: series.getFill(),
                 backgroundColor: colors,
                 // Pie charts look better without borders.
                 borderColor: this._chart.getType() == Pie.prototype.TYPE ? '#fff' : colors,
index 90cb281..34e5211 100644 (file)
@@ -71,6 +71,14 @@ define([], function() {
      */
     Series.prototype._colors = null;
 
+    /**
+     * The fill mode of the series.
+     *
+     * @type {Object}
+     * @protected
+     */
+    Series.prototype._fill = false;
+
     /**
      * The label of the series.
      *
@@ -149,6 +157,7 @@ define([], function() {
             s.setColor(obj.colors[0]);
         }
 
+        s.setFill(obj.fill);
         s.setSmooth(obj.smooth);
         return s;
     };
@@ -180,6 +189,15 @@ define([], function() {
         return this._values.length;
     };
 
+    /**
+     * Get the fill mode of the series.
+     *
+     * @return {Object}
+     */
+    Series.prototype.getFill = function() {
+      return this._fill;
+    };
+
     /**
      * Get the series label.
      *
@@ -273,6 +291,15 @@ define([], function() {
         this._colors = colors || [];
     };
 
+    /**
+     * Set the fill mode for the series.
+     *
+     * @param {Object} fill
+     */
+    Series.prototype.setFill = function(fill) {
+      this._fill = typeof fill === 'undefined' ? null : fill;
+    };
+
     /**
      * Set the labels for the values of the series.
      *
index 98725b8..674fffa 100644 (file)
@@ -43,6 +43,7 @@ define(['jquery', 'core/key_codes'], function($, keyCodes) {
         ctrlPageUp: 'cie:ctrlPageUp',
         ctrlPageDown: 'cie:ctrlPageDown',
         enter: 'cie:enter',
+        accessibleChange: 'cie:accessibleChange',
     };
     // Static cache of jQuery events that have been handled. This should
     // only be populated by JavaScript generated events (which will keep it
@@ -415,6 +416,48 @@ define(['jquery', 'core/key_codes'], function($, keyCodes) {
         addKeyboardEvent(element, events.enter, keyCodes.enter);
     };
 
+    /**
+     * Trigger the AccessibleChange event on the given element if the value of the element is changed.
+     *
+     * @method addAccessibleChangeListener
+     * @private
+     * @param {object} element jQuery object to add event listeners to
+     */
+    var addAccessibleChangeListener = function(element) {
+        var onMac = navigator.userAgent.indexOf('Macintosh') !== -1;
+        var touchEnabled = ('ontouchstart' in window) || (('msMaxTouchPoints' in navigator) && (navigator.msMaxTouchPoints > 0));
+        if (onMac || touchEnabled) {
+            element.on('change', function(e) {
+                triggerEvent(events.accessibleChange, e);
+            });
+        } else {
+            element.on('focus', function() {
+                $(this).data('initValue', this.value);
+            });
+            element.on('blur', function(e) {
+                var initValue = $(this).data('initValue');
+                $(this).removeData('initValue');
+                if (this.value !== initValue) {
+                    triggerEvent(events.accessibleChange, e);
+                }
+            });
+            element.on('keydown', function(e) {
+                if ((e.which === keyCodes.enter) && this.value !== $(this).data('initValue')) {
+                    triggerEvent(events.accessibleChange, e);
+                } else if (e.which === keyCodes.escape) {
+                    this.value = $(this).data('initValue');
+                }
+            });
+            element.on('click', function(e) {
+                var initValue = $(this).data('initValue');
+                // Some browsers trigger onclick before onblur, therefore it is possible that initValue is undefined.
+                if (typeof initValue !== 'undefined' && initValue != this.value) {
+                    triggerEvent(events.accessibleChange, e);
+                }
+            });
+        }
+    };
+
     /**
      * Get the list of events and their handlers.
      *
@@ -441,6 +484,7 @@ define(['jquery', 'core/key_codes'], function($, keyCodes) {
         handlers[events.ctrlPageUp] = addCtrlPageUpListener;
         handlers[events.ctrlPageDown] = addCtrlPageDownListener;
         handlers[events.enter] = addEnterListener;
+        handlers[events.accessibleChange] = addAccessibleChangeListener;
 
         return handlers;
     };
index c9eeb6c..31d6679 100644 (file)
@@ -51,6 +51,6 @@ $string['tcpsocketport'] = 'TCP socket port';
 $string['tcpsocketportdesc'] = 'The port to use when connecting to ClamAV';
 $string['unknownerror'] = 'There was an unknown error with ClamAV.';
 $string['tries'] = 'Scanning attempts';
-$string['tries_desc'] = 'Number of attempts clamav will try when there is an error during scanning process';
+$string['tries_desc'] = 'Number of attempts made by ClamAV if there is an error during the scanning process.';
 $string['tries_notice'] = 'Clamav scanning has tried {$a->tries} time(s).
 {$a->notice}';
index 8bd3815..095c0e2 100644 (file)
@@ -637,14 +637,17 @@ class behat_config_util {
             if (!isset($values['capabilities'])) {
                 $values['capabilities'] = [];
             }
-            if (!isset($values['capabilities']['chrome'])) {
-                $values['capabilities']['chrome'] = [];
+            if (!isset($values['capabilities']['extra_capabilities'])) {
+                $values['capabilities']['extra_capabilities'] = [];
             }
-            if (!isset($values['capabilities']['chrome']['switches'])) {
-                $values['capabilities']['chrome']['switches'] = [];
+            if (!isset($values['capabilities']['extra_capabilities']['chromeOptions'])) {
+                $values['capabilities']['extra_capabilities']['chromeOptions'] = [];
             }
-            $values['capabilities']['chrome']['switches'][] = '--unlimited-storage';
-            $values['capabilities']['chrome']['switches'][] = '--disable-web-security';
+            if (!isset($values['capabilities']['extra_capabilities']['chromeOptions']['args'])) {
+                $values['capabilities']['extra_capabilities']['chromeOptions']['args'] = [];
+            }
+            $values['capabilities']['extra_capabilities']['chromeOptions']['args'][] = '--unlimited-storage';
+            $values['capabilities']['extra_capabilities']['chromeOptions']['args'][] = '--disable-web-security';
 
             // If the mobile app is enabled, check its version and add appropriate tags.
             if ($mobiletags = $this->get_mobile_version_tags()) {
index 313cd08..03880b0 100644 (file)
@@ -517,7 +517,14 @@ function cli_execute_parallel($cmds, $cwd = null, $delay = 0) {
 
     // Create child process.
     foreach ($cmds as $name => $cmd) {
-        $process = new Symfony\Component\Process\Process($cmd);
+        if (method_exists('\\Symfony\\Component\\Process\\Process', 'fromShellCommandline')) {
+            // Process 4.2 and up.
+            $process = Symfony\Component\Process\Process::fromShellCommandline($cmd);
+        } else {
+            // Process 4.1 and older.
+            $process = new Symfony\Component\Process\Process(null);
+            $process->setCommandLine($cmd);
+        }
 
         $process->setWorkingDirectory($cwd);
         $process->setTimeout(null);
index 1cb3160..e052efd 100644 (file)
@@ -48,6 +48,8 @@ class chart_base implements JsonSerializable, renderable {
     protected $xaxes = [];
     /** @var chart_axis[] The Y axes. */
     protected $yaxes = [];
+    /** @var array Options for the chart legend. */
+    protected $legendoptions = [];
 
     /**
      * Constructor.
@@ -86,6 +88,7 @@ class chart_base implements JsonSerializable, renderable {
                 'x' => $this->xaxes,
                 'y' => $this->yaxes,
             ],
+            'legend_options' => !empty($this->legendoptions) ? $this->legendoptions : null,
             'config_colorset' => !empty($CFG->chart_colorset) ? $CFG->chart_colorset : null
         ];
     }
@@ -131,6 +134,15 @@ class chart_base implements JsonSerializable, renderable {
         return $this->labels;
     }
 
+    /**
+     * Get an array of options for the chart legend.
+     *
+     * @return array
+     */
+    public function get_legend_options() {
+        return $this->legendoptions;
+    }
+
     /**
      * Get the series.
      *
@@ -221,6 +233,20 @@ class chart_base implements JsonSerializable, renderable {
         $this->labels = $labels;
     }
 
+    /**
+     * Set options for the chart legend.
+     * See https://www.chartjs.org/docs/2.7.0/configuration/legend.html for options.
+     *
+     * Note: Setting onClick and onHover events is not directly supported through
+     * this method. These config options must be set directly within Javascript
+     * on the page.
+     *
+     * @param array $legendoptions Whether or not to display the chart's legend.
+     */
+    public function set_legend_options(array $legendoptions) {
+        $this->legendoptions = $legendoptions;
+    }
+
     /**
      * Set the title.
      *
index b3e79d8..5db4ede 100644 (file)
@@ -44,6 +44,8 @@ class chart_series implements JsonSerializable {
 
     /** @var string[] Colors of the series. */
     protected $colors = [];
+    /** @var string Fill mode for area charts. See https://www.chartjs.org/docs/latest/charts/area.html */
+    protected $fill = null;
     /** @var string Label for this series. */
     protected $label;
     /** @var string[] Labels for the values of the series. */
@@ -97,6 +99,13 @@ class chart_series implements JsonSerializable {
         return count($this->values);
     }
 
+    /**
+     * Get area fill mode for series.
+     */
+    public function get_fill() {
+        return $this->fill;
+    }
+
     /**
      * Get the label of the series.
      *
@@ -181,6 +190,7 @@ class chart_series implements JsonSerializable {
             'type' => $this->type,
             'values' => $this->values,
             'colors' => $this->colors,
+            'fill' => $this->fill,
             'axes' => [
                 'x' => $this->xaxis,
                 'y' => $this->yaxis,
@@ -208,6 +218,14 @@ class chart_series implements JsonSerializable {
         $this->colors = $colors;
     }
 
+    /**
+     * Set fill mode for the series.
+     * @param string $fill
+     */
+    public function set_fill($fill) {
+        $this->fill = $fill;
+    }
+
     /**
      * Set labels for the values of the series.
      *
index f898c36..2a6eae8 100644 (file)
@@ -86,6 +86,7 @@ class core_component {
     protected static $psr0namespaces = array(
         'Horde' => 'lib/horde/framework/Horde',
         'Mustache' => 'lib/mustache/src/Mustache',
+        'CFPropertyList' => 'lib/plist/classes/CFPropertyList',
     );
     /** @var array associative array of PRS-4 namespaces and corresponding paths. */
     protected static $psr4namespaces = array(
index 900c4e4..962bc3e 100644 (file)
@@ -1960,13 +1960,13 @@ class core_plugin_manager {
 
             'quizaccess' => array(
                 'delaybetweenattempts', 'ipaddress', 'numattempts', 'offlineattempts', 'openclosedate',
-                'password', 'safebrowser', 'securewindow', 'timelimit'
+                'password', 'seb', 'securewindow', 'timelimit'
             ),
 
             'report' => array(
                 'backups', 'competency', 'completion', 'configlog', 'courseoverview', 'eventlist',
                 'insights', 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances',
-                'security', 'stats', 'performance', 'usersessions'
+                'security', 'stats', 'status', 'performance', 'usersessions'
             ),
 
             'repository' => array(
index b7fd89c..fdf4e3e 100644 (file)
@@ -568,8 +568,7 @@ class manager {
 
         $where = '(nextruntime IS NULL OR nextruntime < :timestart1)';
         $params = array('timestart1' => $timestart);
-        $records = $DB->get_records_select('task_adhoc', $where, $params, 'nextruntime ASC, id ASC');
-
+        $records = $DB->get_records_select('task_adhoc', $where, $params, 'nextruntime ASC, id ASC', '*', 0, 2000);
         $records = self::ensure_adhoc_task_qos($records);
 
         $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
index 15b781f..eecc092 100644 (file)
@@ -38,7 +38,7 @@ class messaging_cleanup_task extends scheduled_task {
     }
 
     /**
-     * Do the job.
+     * Do the job. Each message processor also gets the chance to perform it's own cleanup.
      * Throw exceptions on errors (the job will be retried).
      */
     public function execute() {
@@ -46,9 +46,17 @@ class messaging_cleanup_task extends scheduled_task {
 
         $timenow = time();
 
+        $processors = get_message_processors(true);
+
         // Cleanup read and unread notifications.
         if (!empty($CFG->messagingdeleteallnotificationsdelay)) {
             $notificationdeletetime = $timenow - $CFG->messagingdeleteallnotificationsdelay;
+
+            /** @var \message_output $processor */
+            foreach (array_column($processors, 'object') as $processor) {
+                $processor->cleanup_all_notifications($notificationdeletetime);
+            }
+
             $params = array('notificationdeletetime' => $notificationdeletetime);
             $DB->delete_records_select('notifications', 'timecreated < :notificationdeletetime', $params);
         }
@@ -56,6 +64,12 @@ class messaging_cleanup_task extends scheduled_task {
         // Cleanup read notifications.
         if (!empty($CFG->messagingdeletereadnotificationsdelay)) {
             $notificationdeletetime = $timenow - $CFG->messagingdeletereadnotificationsdelay;
+
+            /** @var \message_output $processor */
+            foreach (array_column($processors, 'object') as $processor) {
+                $processor->cleanup_read_notifications($notificationdeletetime);
+            }
+
             $params = array('notificationdeletetime' => $notificationdeletetime);
             $DB->delete_records_select('notifications', 'timeread < :notificationdeletetime', $params);
         }
index f71d4d4..0b5f9e4 100644 (file)
@@ -2306,5 +2306,13 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2020041700.01);
     }
 
+    if ($oldversion < 2020042800.01) {
+        // Delete obsolete config value.
+        unset_config('enablesafebrowserintegration');
+        // Clean up config of the old plugin.
+        unset_all_config_for_plugin('quizaccess_safebrowser');
+
+        upgrade_main_savepoint(true, 2020042800.01);
+    }
     return true;
 }
index ce314c7..2a42f8e 100644 (file)
@@ -2169,7 +2169,7 @@ function readfile_accel($file, $mimetype, $accelerate) {
         if (is_object($file)) {
             $fs = get_file_storage();
             if ($fs->supports_xsendfile()) {
-                if ($fs->xsendfile($file->get_contenthash())) {
+                if ($fs->xsendfile_file($file)) {
                     return;
                 }
             }
index 14eb6c6..9e59954 100644 (file)
@@ -1821,6 +1821,21 @@ class file_storage {
         }
     }
 
+    /**
+     * Serve file content using X-Sendfile header.
+     * Please make sure that all headers are already sent and the all
+     * access control checks passed.
+     *
+     * This alternate method to xsendfile() allows an alternate file system
+     * to use the full file metadata and avoid extra lookups.
+     *
+     * @param stored_file $file The file to send
+     * @return bool success
+     */
+    public function xsendfile_file(stored_file $file): bool {
+        return $this->filesystem->xsendfile_file($file);
+    }
+
     /**
      * Serve file content using X-Sendfile header.
      * Please make sure that all headers are already sent
index 5015696..e8a78fb 100644 (file)
@@ -435,6 +435,21 @@ abstract class file_system {
         return $image;
     }
 
+    /**
+     * Serve file content using X-Sendfile header.
+     * Please make sure that all headers are already sent and the all
+     * access control checks passed.
+     *
+     * This alternate method to xsendfile() allows an alternate file system
+     * to use the full file metadata and avoid extra lookups.
+     *
+     * @param stored_file $file The file to send
+     * @return bool success
+     */
+    public function xsendfile_file(stored_file $file): bool {
+        return $this->xsendfile($file->get_contenthash());
+    }
+
     /**
      * Serve file content using X-Sendfile header.
      * Please make sure that all headers are already sent and the all
index 78923c7..2da0f15 100644 (file)
@@ -448,7 +448,7 @@ function ldap_stripslashes($text) {
     $text = preg_replace_callback($quoted,
                                   function ($match) use ($specialchars) {
                                       if (ctype_xdigit(ltrim($match[1], '\\'))) {
-                                          return chr(hexdec($match[1]));
+                                          return chr(hexdec(ltrim($match[1], '\\')));
                                       } else {
                                           return str_replace($specialchars[LDAP_DN_SPECIAL_CHARS_QUOTED_ALPHA],
                                                              $specialchars[LDAP_DN_SPECIAL_CHARS],
index 6d4a2be..542fd0d 100644 (file)
@@ -9546,37 +9546,83 @@ function get_performance_info() {
     }
 
     $info['html'] .= '</ul>';
+    $html = '';
     if ($stats = cache_helper::get_stats()) {
-        $html = '<ul class="cachesused list-unstyled ml-1 row">';
-        $html .= '<li class="cache-stats-heading font-weight-bold">Caches used (hits/misses/sets)</li>';
-        $html .= '</ul><ul class="cachesused list-unstyled ml-1">';
+
+        $table = new html_table();
+        $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-striped';
+        $table->head = ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S'];
+        $table->data = [];
+        $table->align = ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right'];
+
         $text = 'Caches used (hits/misses/sets): ';
         $hits = 0;
         $misses = 0;
         $sets = 0;
+        $maxstores = 0;
+
+        // We want to align static caches into their own column.
+        $hasstatic = false;
+        foreach ($stats as $definition => $details) {
+            $numstores = count($details['stores']);
+            $first = key($details['stores']);
+            if ($first !== cache_store::STATIC_ACCEL) {
+                $numstores++; // Add a blank space for the missing static store.
+            }
+            $maxstores = max($maxstores, $numstores);
+        }
+
+        $storec = 0;
+
+        while ($storec++ < ($maxstores - 2)) {
+            if ($storec == ($maxstores - 2)) {
+                $table->head[] = get_string('mappingfinal', 'cache');
+            } else {
+                $table->head[] = "Store $storec";
+            }
+            $table->align[] = 'left';
+            $table->align[] = 'right';
+            $table->align[] = 'right';
+            $table->align[] = 'right';
+            $table->head[] = 'H';
+            $table->head[] = 'M';
+            $table->head[] = 'S';
+        }
+
+        ksort($stats);
+
         foreach ($stats as $definition => $details) {
             switch ($details['mode']) {
                 case cache_store::MODE_APPLICATION:
                     $modeclass = 'application';
-                    $mode = ' <span title="application cache">[a]</span>';
+                    $mode = ' <span title="application cache">App</span>';
                     break;
                 case cache_store::MODE_SESSION:
                     $modeclass = 'session';
-                    $mode = ' <span title="session cache">[s]</span>';
+                    $mode = ' <span title="session cache">Ses</span>';
                     break;
                 case cache_store::MODE_REQUEST:
                     $modeclass = 'request';
-                    $mode = ' <span title="request cache">[r]</span>';
+                    $mode = ' <span title="request cache">Req</span>';
                     break;
             }
-            $html .= '<li class="d-inline-flex"><ul class="cache-definition-stats list-unstyled ml-1 mb-1 cache-mode-'.$modeclass.' card d-inline-block">';
-            $html .= '<li class="cache-definition-stats-heading p-t-1 card-header bg-dark bg-inverse font-weight-bold">' .
-                $definition . $mode.'</li>';
+            $row = [$mode, $definition];
+
             $text .= "$definition {";
+
+            $storec = 0;
             foreach ($details['stores'] as $store => $data) {
-                $hits += $data['hits'];
+
+                if ($storec == 0 && $store !== cache_store::STATIC_ACCEL) {
+                    $row[] = '';
+                    $row[] = '';
+                    $row[] = '';
+                    $storec++;
+                }
+
+                $hits   += $data['hits'];
                 $misses += $data['misses'];
-                $sets += $data['sets'];
+                $sets   += $data['sets'];
                 if ($data['hits'] == 0 and $data['misses'] > 0) {
                     $cachestoreclass = 'nohits text-danger';
                 } else if ($data['hits'] < $data['misses']) {
@@ -9585,18 +9631,100 @@ function get_performance_info() {
                     $cachestoreclass = 'hihits text-success';
                 }
                 $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
-                $html .= "<li class=\"cache-store-stats $cachestoreclass p-x-1\">" .
-                    "$store: $data[hits] / $data[misses] / $data[sets]</li>";
-                // This makes boxes of same sizes.
-                if (count($details['stores']) == 1) {
-                    $html .= "<li class=\"cache-store-stats $cachestoreclass p-x-1\">&nbsp;</li>";
+                $cell = new html_table_cell($store);
+                $cell->attributes = ['class' => $cachestoreclass];
+                $row[] = $cell;
+                $cell = new html_table_cell($data['hits']);
+                $cell->attributes = ['class' => $cachestoreclass];
+                $row[] = $cell;
+                $cell = new html_table_cell($data['misses']);
+                $cell->attributes = ['class' => $cachestoreclass];
+                $row[] = $cell;
+
+                if ($store !== cache_store::STATIC_ACCEL) {
+                    // The static cache is never set.
+                    $cell = new html_table_cell($data['sets']);
+                    $cell->attributes = ['class' => $cachestoreclass];
+                    $row[] = $cell;
                 }
+                $storec++;
+            }
+            while ($storec++ < $maxstores) {
+                $row[] = '';
+                $row[] = '';
+                $row[] = '';
+                $row[] = '';
             }
-            $html .= '</ul></li>';
             $text .= '} ';
+
+            $table->data[] = $row;
         }
-        $html .= '</ul> ';
-        $html .= "<div class='cache-total-stats row'>Total: $hits / $misses / $sets</div>";
+
+        $html .= html_writer::table($table);
+
+        // Now lets also show sub totals for each cache store.
+        $storetotals = [];
+        $storetotal = ['hits' => 0, 'misses' => 0, 'sets' => 0];
+        foreach ($stats as $definition => $details) {
+            foreach ($details['stores'] as $store => $data) {
+                if (!array_key_exists($store, $storetotals)) {
+                    $storetotals[$store] = ['hits' => 0, 'misses' => 0, 'sets' => 0];
+                }
+                $storetotals[$store]['class']   = $data['class'];
+                $storetotals[$store]['hits']   += $data['hits'];
+                $storetotals[$store]['misses'] += $data['misses'];
+                $storetotals[$store]['sets']   += $data['sets'];
+                $storetotal['hits']   += $data['hits'];
+                $storetotal['misses'] += $data['misses'];
+                $storetotal['sets']   += $data['sets'];
+            }
+        }
+
+        $table = new html_table();
+        $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-striped';
+        $table->head = [get_string('storename', 'cache'), get_string('type_cachestore', 'plugin'), 'H', 'M', 'S'];
+        $table->data = [];
+        $table->align = ['left', 'left', 'right', 'right', 'right'];
+
+        ksort($storetotals);
+
+        foreach ($storetotals as $store => $data) {
+            $row = [];
+            if ($data['hits'] == 0 and $data['misses'] > 0) {
+                $cachestoreclass = 'nohits text-danger';
+            } else if ($data['hits'] < $data['misses']) {
+                $cachestoreclass = 'lowhits text-warning';
+            } else {
+                $cachestoreclass = 'hihits text-success';
+            }
+            $cell = new html_table_cell($store);
+            $cell->attributes = ['class' => $cachestoreclass];
+            $row[] = $cell;
+            $cell = new html_table_cell($data['class']);
+            $cell->attributes = ['class' => $cachestoreclass];
+            $row[] = $cell;
+            $cell = new html_table_cell($data['hits']);
+            $cell->attributes = ['class' => $cachestoreclass];
+            $row[] = $cell;
+            $cell = new html_table_cell($data['misses']);
+            $cell->attributes = ['class' => $cachestoreclass];
+            $row[] = $cell;
+            $cell = new html_table_cell($data['sets']);
+            $cell->attributes = ['class' => $cachestoreclass];
+            $row[] = $cell;
+            $table->data[] = $row;
+        }
+        $row = [
+            get_string('total'),
+            '',
+            $storetotal['hits'],
+            $storetotal['misses'],
+            $storetotal['sets'],
+        ];
+        $table->data[] = $row;
+
+        $html .= html_writer::table($table);
+
         $info['cachesused'] = "$hits / $misses / $sets";
         $info['html'] .= $html;
         $info['txt'] .= $text.'. ';
index e4d617c..81e2995 100644 (file)
@@ -2683,7 +2683,7 @@ $iconprogress
 EOD;
         if ($options->env != 'url') {
             $html .= <<<EOD
-    <div id="file_info_{$client_id}" class="mdl-left filepicker-filelist" style="position: relative">
+    <div id="file_info_{$client_id}" class="mdl-left filepicker-filelist border" style="position: relative">
     <div class="filepicker-filename">
         <div class="filepicker-container">$currentfile<div class="dndupload-message">$strdndenabled <br/><div class="dndupload-arrow"></div></div></div>
         <div class="dndupload-progressbars"></div>
diff --git a/lib/plist/LICENSE b/lib/plist/LICENSE
new file mode 100644 (file)
index 0000000..5e63f61
--- /dev/null
@@ -0,0 +1,22 @@
+The MIT License
+
+Copyright (c) 2009 Christian Kruse, Rodney Rehm
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
diff --git a/lib/plist/README.md b/lib/plist/README.md
new file mode 100644 (file)
index 0000000..7eaf4f8
--- /dev/null
@@ -0,0 +1,39 @@
+# CFPropertyList
+
+The PHP implementation of Apple's PropertyList can handle XML PropertyLists as well as binary PropertyLists. It offers functionality to easily convert data between worlds, e.g. recalculating timestamps from unix epoch to apple epoch and vice versa. A feature to automagically create (guess) the plist structure from a normal PHP data structure will help you dump your data to plist in no time.
+
+Note: CFPropertylist was originally hosted on [Google Code](http://code.google.com/p/cfpropertylist/)
+
+## Choose Your Favorite Operating System
+
+CFPropertyList does not rely on any "Apple proprietary" components, like plutil. CFPropertyList runs on any Operating System with PHP and some standard extensions installed.
+
+Although you might want to deliver data to your iPhone application, you might want to run those server side services on your standard Linux (or even Windows) environment, rather than buying an expensive Apple Server. With CFPropertyList you now have the power to provide data from your favorite Operating System.
+
+## Requirements And Limitations
+
+* requires PHP5.3 (as of CFPropertyList 2.0)
+* requires either [MBString](http://php.net/mbstring) or [Iconv](http://php.net/iconv)
+* requires either [BC](http://php.net/bc) or [GMP](http://php.net/gmp) or [phpseclib](http://phpseclib.sourceforge.net/) (see BigIntegerBug for an explanation) - as of CFPropertyList 1.0.1
+
+## Authors
+
+- Rodney Rehm <rodney.rehm@medialize.de>
+- Christian Kruse <cjk@wwwtech.de>
+- PSR-0 changes by Jarvis Badgley <https://github.com/ChiperSoft/CFPropertyList>
+
+## License
+
+CFPropertyList is published under the [MIT License](http://www.opensource.org/licenses/mit-license.php).
+
+## Installation
+
+see [Composer / Packagist](http://packagist.org/packages/rodneyrehm/plist).
+
+## Related
+
+* [man(5) plist](http://developer.apple.com/documentation/Darwin/Reference/ManPages/man5/plist.5.html)
+* [CFBinaryPList.c](http://www.opensource.apple.com/source/CF/CF-476.15/CFBinaryPList.c)
+* [CFPropertyList in Ruby](http://rubyforge.org/projects/cfpropertylist/)
+* [CFPropertyList in Python](https://github.com/bencochran/CFPropertyList)
+* [plist on Wikipedia](http://en.wikipedia.org/wiki/Plist)
diff --git a/lib/plist/classes/CFPropertyList/CFBinaryPropertyList.php b/lib/plist/classes/CFPropertyList/CFBinaryPropertyList.php
new file mode 100644 (file)
index 0000000..34cb708
--- /dev/null
@@ -0,0 +1,1051 @@
+<?php
+/**
+ * CFPropertyList
+ * {@link http://developer.apple.com/documentation/Darwin/Reference/ManPages/man5/plist.5.html Property Lists}
+ * @author Rodney Rehm <rodney.rehm@medialize.de>
+ * @author Christian Kruse <cjk@wwwtech.de>
+ * @package plist
+ * @version $Id$
+ */
+namespace CFPropertyList;
+
+/**
+ * Facility for reading and writing binary PropertyLists. Ported from {@link http://www.opensource.apple.com/source/CF/CF-476.15/CFBinaryPList.c CFBinaryPList.c}.
+ * @author Rodney Rehm <rodney.rehm@medialize.de>
+ * @author Christian Kruse <cjk@wwwtech.de>
+ * @package plist
+ * @example example-read-02.php Read a Binary PropertyList
+ * @example example-read-03.php Read a PropertyList without knowing the type
+ */
+abstract class CFBinaryPropertyList {
+  /**
+   * Content of the plist (unparsed string)
+   * @var string
+   */
+  protected $content = NULL;
+
+  /**
+   * position in the (unparsed) string
+   * @var integer
+   */
+  protected $pos = 0;
+
+  /**
+   * Table containing uniqued objects
+   * @var array
+   */
+  protected $uniqueTable = Array();
+
+  /**
+   * Number of objects in file
+   * @var integer
+   */
+  protected $countObjects = 0;
+
+  /**
+   * The length of all strings in the file (byte length, not character length)
+   * @var integer
+   */
+  protected $stringSize = 0;
+
+  /**
+   * The length of all ints in file (byte length)
+   * @var integer
+   */
+  protected $intSize = 0;
+
+  /**
+   * The length of misc objects (i.e. not integer and not string) in file
+   * @var integer
+   */
+  protected $miscSize = 0;
+
+  /**
+   * Number of object references in file (needed to calculate reference byte length)
+   * @var integer
+   */
+  protected $objectRefs = 0;
+
+  /**
+   * Number of objects written during save phase; needed to calculate the size of the object table
+   * @var integer
+   */
+  protected $writtenObjectCount = 0;
+
+  /**
+   * Table containing all objects in the file
+   */
+  protected $objectTable = Array();
+
+  /**
+   * The size of object references
+   */
+  protected $objectRefSize = 0;
+
+  /**
+   * The „offsets” (i.e. the different entries) in the file
+   */
+  protected $offsets = Array();
+
+  /**
+   * Read a „null type” (filler byte, true, false, 0 byte)
+   * @param $length The byte itself
+   * @return the byte value (e.g. CFBoolean(true), CFBoolean(false), 0 or 15)
+   * @throws PListException on encountering an unknown null type
+   */
+  protected function readBinaryNullType($length) {
+    switch($length) {
+      case 0: return 0; // null type
+      case 8: return new CFBoolean(false);
+      case 9: return new CFBoolean(true);
+      case 15: return 15; // fill type
+    }
+
+    throw new PListException("unknown null type: $length");
+  }
+
+  /**
+   * Create an 64 bit integer using bcmath or gmp
+   * @param int $hi The higher word
+   * @param int $lo The lower word
+   * @return mixed The integer (as int if possible, as string if not possible)
+   * @throws PListException if neither gmp nor bc available
+   */
+  protected static function make64Int($hi,$lo) {
+    // on x64, we can just use int
+    if(PHP_INT_SIZE > 4) return (((int)$hi)<<32) | ((int)$lo);
+
+    // lower word has to be unsigned since we don't use bitwise or, we use bcadd/gmp_add
+    $lo = sprintf("%u", $lo);
+
+    // use GMP or bcmath if possible
+    if(function_exists("gmp_mul")) return gmp_strval(gmp_add(gmp_mul($hi, "4294967296"), $lo));
+
+    if(function_exists("bcmul")) return bcadd(bcmul($hi,"4294967296"), $lo);
+
+    if(class_exists('Math_BigInteger')) {
+      $bi = new \Math_BigInteger($hi);
+      return $bi->multiply(new \Math_BigInteger("4294967296"))->add(new \Math_BigInteger($lo))->toString();
+    }
+
+    throw new PListException("either gmp or bc has to be installed, or the Math_BigInteger has to be available!");
+  }
+
+  /**
+   * Read an integer value
+   * @param integer $length The length (in bytes) of the integer value, coded as „set bit $length to 1”
+   * @return CFNumber The integer value
+   * @throws PListException if integer val is invalid
+   * @throws IOException if read error occurs
+   * @uses make64Int() to overcome PHP's big integer problems
+   */
+  protected function readBinaryInt($length) {
+    if($length > 3) throw new PListException("Integer greater than 8 bytes: $length");
+
+    $nbytes = 1 << $length;
+
+    $val = null;
+    if(strlen($buff = substr($this->content, $this->pos, $nbytes)) != $nbytes) throw IOException::readError("");
+    $this->pos += $nbytes;
+
+    switch($length) {
+      case 0:
+        $val = unpack("C", $buff);
+        $val = $val[1];
+        break;
+      case 1:
+        $val = unpack("n", $buff);
+        $val = $val[1];
+        break;
+      case 2:
+        $val = unpack("N", $buff);
+        $val = $val[1];
+        break;
+      case 3:
+        $words = unpack("Nhighword/Nlowword",$buff);
+        //$val = $words['highword'] << 32 | $words['lowword'];
+        $val = self::make64Int($words['highword'],$words['lowword']);
+        break;
+    }
+
+    return new CFNumber($val);
+  }
+
+  /**
+   * Read a real value
+   * @param integer $length The length (in bytes) of the integer value, coded as „set bit $length to 1”
+   * @return CFNumber The real value
+   * @throws PListException if real val is invalid
+   * @throws IOException if read error occurs
+   */
+  protected function readBinaryReal($length) {
+    if($length > 3) throw new PListException("Real greater than 8 bytes: $length");
+
+    $nbytes = 1 << $length;
+    $val = null;
+    if(strlen($buff = substr($this->content,$this->pos, $nbytes)) != $nbytes) throw IOException::readError("");
+    $this->pos += $nbytes;
+
+    switch($length) {
+      case 0: // 1 byte float? must be an error
+      case 1: // 2 byte float? must be an error
+        $x = $length + 1;
+        throw new PListException("got {$x} byte float, must be an error!");
+      case 2:
+        $val = unpack("f", strrev($buff));
+        $val = $val[1];
+        break;
+      case 3:
+        $val = unpack("d", strrev($buff));
+        $val = $val[1];
+        break;
+    }
+
+    return new CFNumber($val);
+  }
+
+  /**
+   * Read a date value
+   * @param integer $length The length (in bytes) of the integer value, coded as „set bit $length to 1”
+   * @return CFDate The date value
+   * @throws PListException if date val is invalid
+   * @throws IOException if read error occurs
+   */
+  protected function readBinaryDate($length) {
+    if($length > 3) throw new PListException("Date greater than 8 bytes: $length");
+
+    $nbytes = 1 << $length;
+    $val = null;
+    if(strlen($buff = substr($this->content, $this->pos, $nbytes)) != $nbytes) throw IOException::readError("");
+    $this->pos += $nbytes;
+
+    switch($length) {
+      case 0: // 1 byte CFDate is an error
+      case 1: // 2 byte CFDate is an error
+        $x = $length + 1;
+        throw new PListException("{$x} byte CFdate, error");
+
+      case 2:
+        $val = unpack("f", strrev($buff));
+        $val = $val[1];
+        break;
+      case 3:
+        $val = unpack("d", strrev($buff));
+        $val = $val[1];
+        break;
+    }
+
+    return new CFDate($val,CFDate::TIMESTAMP_APPLE);
+  }
+
+  /**
+   * Read a data value
+   * @param integer $length The length (in bytes) of the integer value, coded as „set bit $length to 1”
+   * @return CFData The data value
+   * @throws IOException if read error occurs
+   */
+  protected function readBinaryData($length) {
+    if($length == 0) $buff = "";
+    else {
+      $buff = substr($this->content, $this->pos, $length);
+      if(strlen($buff) != $length) throw IOException::readError("");
+      $this->pos += $length;
+    }
+
+    return new CFData($buff,false);
+  }
+
+  /**
+   * Read a string value, usually coded as utf8
+   * @param integer $length The length (in bytes) of the string value
+   * @return CFString The string value, utf8 encoded
+   * @throws IOException if read error occurs
+   */
+  protected function readBinaryString($length) {
+    if($length == 0) $buff = "";
+    else {
+      if(strlen($buff = substr($this->content, $this->pos, $length)) != $length) throw IOException::readError("");
+      $this->pos += $length;
+    }
+
+    if(!isset($this->uniqueTable[$buff])) $this->uniqueTable[$buff] = true;
+    return new CFString($buff);
+  }
+
+  /**
+   * Convert the given string from one charset to another.
+   * Trying to use MBString, Iconv, Recode - in that particular order.
+   * @param string $string the string to convert
+   * @param string $fromCharset the charset the given string is currently encoded in
+   * @param string $toCharset the charset to convert to, defaults to UTF-8
+   * @return string the converted string
+   * @throws PListException on neither MBString, Iconv, Recode being available
+   */
+  public static function convertCharset($string, $fromCharset, $toCharset='UTF-8') {
+    if(function_exists('mb_convert_encoding')) return mb_convert_encoding($string, $toCharset, $fromCharset);
+    if(function_exists('iconv')) return iconv($fromCharset, $toCharset, $string);
+    if(function_exists('recode_string')) return recode_string($fromCharset .'..'. $toCharset, $string);
+
+    throw new PListException('neither iconv nor mbstring supported. how are we supposed to work on strings here?');
+  }
+
+  /**
+   * Count characters considering character set
+   * Trying to use MBString, Iconv - in that particular order.
+   * @param string $string the string to convert
+   * @param string $charset the charset the given string is currently encoded in
+   * @return integer The number of characters in that string
+   * @throws PListException on neither MBString, Iconv being available
+   */
+  public static function charsetStrlen($string,$charset="UTF-8") {
+    if(function_exists('mb_strlen')) return mb_strlen($string, $charset);
+    if(function_exists('iconv_strlen')) return iconv_strlen($string,$charset);
+
+    throw new PListException('neither iconv nor mbstring supported. how are we supposed to work on strings here?');
+  }
+
+  /**
+   * Read a unicode string value, coded as UTF-16BE
+   * @param integer $length The length (in bytes) of the string value
+   * @return CFString The string value, utf8 encoded
+   * @throws IOException if read error occurs
+   */
+  protected function readBinaryUnicodeString($length) {
+    /* The problem is: we get the length of the string IN CHARACTERS;
+       since a char in UTF-16 can be 16 or 32 bit long, we don't really know
+       how long the string is in bytes */
+    if(strlen($buff = substr($this->content, $this->pos, 2*$length)) != 2*$length) throw IOException::readError("");
+    $this->pos += 2 * $length;
+
+    if(!isset($this->uniqueTable[$buff])) $this->uniqueTable[$buff] = true;
+    return new CFString(self::convertCharset($buff, "UTF-16BE", "UTF-8"));
+  }
+
+  /**
+   * Read an array value, including contained objects
+   * @param integer $length The number of contained objects
+   * @return CFArray The array value, including the objects
+   * @throws IOException if read error occurs
+   */
+  protected function readBinaryArray($length) {
+    $ary = new CFArray();
+
+    // first: read object refs
+    if($length != 0) {
+      if(strlen($buff = substr($this->content, $this->pos, $length * $this->objectRefSize)) != $length * $this->objectRefSize) throw IOException::readError("");
+      $this->pos += $length * $this->objectRefSize;
+
+      $objects = self::unpackWithSize($this->objectRefSize, $buff);
+
+      // now: read objects
+      for($i=0;$i<$length;++$i) {
+        $object = $this->readBinaryObjectAt($objects[$i+1]+1);
+        $ary->add($object);
+      }
+    }
+
+    return $ary;
+  }
+
+  /**
+   * Read a dictionary value, including contained objects
+   * @param integer $length The number of contained objects
+   * @return CFDictionary The dictionary value, including the objects
+   * @throws IOException if read error occurs
+   */
+  protected function readBinaryDict($length) {
+    $dict = new CFDictionary();
+
+    // first: read keys
+    if($length != 0) {
+      if(strlen($buff = substr($this->content, $this->pos, $length * $this->objectRefSize)) != $length * $this->objectRefSize) throw IOException::readError("");
+      $this->pos += $length * $this->objectRefSize;
+      $keys = self::unpackWithSize($this->objectRefSize, $buff);
+
+      // second: read object refs
+      if(strlen($buff = substr($this->content, $this->pos, $length * $this->objectRefSize)) != $length * $this->objectRefSize) throw IOException::readError("");
+      $this->pos += $length * $this->objectRefSize;
+      $objects = self::unpackWithSize($this->objectRefSize, $buff);
+
+      // read real keys and objects
+      for($i=0;$i<$length;++$i) {
+        $key = $this->readBinaryObjectAt($keys[$i+1]+1);
+        $object = $this->readBinaryObjectAt($objects[$i+1]+1);
+        $dict->add($key->getValue(),$object);
+      }
+    }
+
+    return $dict;
+  }
+
+  /**
+   * Read an object type byte, decode it and delegate to the correct reader function
+   * @return mixed The value of the delegate reader, so any of the CFType subclasses
+   * @throws IOException if read error occurs
+   */
+  function readBinaryObject() {
+    // first: read the marker byte
+    if(strlen($buff = substr($this->content,$this->pos,1)) != 1) throw IOException::readError("");
+    $this->pos++;
+
+    $object_length = unpack("C*", $buff);
+    $object_length = $object_length[1]  & 0xF;
+    $buff = unpack("H*", $buff);
+    $buff = $buff[1];
+
+    $object_type = substr($buff, 0, 1);
+    if($object_type != "0" && $object_length == 15) {
+      $object_length = $this->readBinaryObject($this->objectRefSize);
+      $object_length = $object_length->getValue();
+    }
+
+    $retval = null;
+    switch($object_type) {
+      case '0': // null, false, true, fillbyte
+        $retval = $this->readBinaryNullType($object_length);
+        break;
+      case '1': // integer
+        $retval = $this->readBinaryInt($object_length);
+        break;
+      case '2': // real
+        $retval = $this->readBinaryReal($object_length);
+        break;
+      case '3': // date
+        $retval = $this->readBinaryDate($object_length);
+        break;
+      case '4': // data
+        $retval = $this->readBinaryData($object_length);
+        break;
+      case '5': // byte string, usually utf8 encoded
+        $retval = $this->readBinaryString($object_length);
+        break;
+      case '6': // unicode string (utf16be)
+        $retval = $this->readBinaryUnicodeString($object_length);
+        break;
+      case '8':
+        $num = $this->readBinaryInt($object_length);
+        $retval = new CFUid($num->getValue());
+        break;
+      case 'a': // array
+        $retval = $this->readBinaryArray($object_length);
+        break;
+      case 'd': // dictionary
+        $retval = $this->readBinaryDict($object_length);
+        break;
+    }
+
+    return $retval;
+  }
+
+  /**
+   * Read an object type byte at position $pos, decode it and delegate to the correct reader function
+   * @param integer $pos The table position in the offsets table
+   * @return mixed The value of the delegate reader, so any of the CFType subclasses
+   */
+  function readBinaryObjectAt($pos) {
+    $this->pos = $this->offsets[$pos];
+    return $this->readBinaryObject();
+  }
+
+  /**
+   * Parse a binary plist string
+   * @return void
+   * @throws IOException if read error occurs
+   */
+  public function parseBinaryString() {
+    $this->uniqueTable = Array();
+    $this->countObjects = 0;
+    $this->stringSize = 0;
+    $this->intSize = 0;
+    $this->miscSize = 0;
+    $this->objectRefs = 0;
+
+    $this->writtenObjectCount = 0;
+    $this->objectTable = Array();
+    $this->objectRefSize = 0;
+
+    $this->offsets = Array();
+
+    // first, we read the trailer: 32 byte from the end
+    $buff = substr($this->content,-32);
+
+    if(strlen($buff) < 32) {
+      throw new PListException('Error in PList format: content is less than at least necessary 32 bytes!');
+    }
+
+    $infos = unpack("x6/Coffset_size/Cobject_ref_size/x4/Nnumber_of_objects/x4/Ntop_object/x4/Ntable_offset",$buff);
+
+    // after that, get the offset table
+    $coded_offset_table = substr($this->content,$infos['table_offset'],$infos['number_of_objects'] * $infos['offset_size']);
+    if(strlen($coded_offset_table) != $infos['number_of_objects'] * $infos['offset_size']) throw IOException::readError("");
+    $this->countObjects = $infos['number_of_objects'];
+
+    // decode offset table
+    $formats = Array("","C*","n*",NULL,"N*");
+    if($infos['offset_size'] == 3) { # since PHP does not support parenthesis in pack/unpack expressions,
+                                     # "(H6)*" does not work and we have to work round this by repeating the
+                                     # expression as often as it fits in the string
+      $this->offsets = array(NULL);
+      while($coded_offset_table) {
+        $str = unpack("H6",$coded_offset_table);
+        $this->offsets[] = hexdec($str[1]);
+        $coded_offset_table = substr($coded_offset_table,3);
+      }
+    }
+    else $this->offsets = unpack($formats[$infos['offset_size']],$coded_offset_table);
+
+    $this->uniqueTable = Array();
+    $this->objectRefSize = $infos['object_ref_size'];
+
+    $top = $this->readBinaryObjectAt($infos['top_object']+1);
+    $this->add($top);
+  }
+
+  /**
+   * Read a binary plist stream
+   * @param resource $stream The stream to read
+   * @return void
+   * @throws IOException if read error occurs
+   */
+  function readBinaryStream($stream) {
+    if(($str = stream_get_contents($stream)) === false || empty($str)) {
+      throw new PListException("Error reading stream!");
+    }
+
+    $this->parseBinary($str);
+  }
+
+  /**
+   * parse a binary plist string
+   * @param string $content The stream to read, defaults to {@link $this->content}
+   * @return void
+   * @throws IOException if read error occurs
+   */
+  function parseBinary($content=NULL) {
+    if($content !== NULL) {
+      $this->content = $content;
+    }
+
+    if(empty($this->content)) {
+      throw new PListException("Content may not be empty!");
+    }
+
+    if(substr($this->content,0,8) != 'bplist00') {
+      throw new PListException("Invalid binary string!");
+    }
+
+    $this->pos = 0;
+
+    $this->parseBinaryString();
+  }
+
+  /**
+   * Read a binary plist file
+   * @param string $file The file to read
+   * @return void
+   * @throws IOException if read error occurs
+   */
+  function readBinary($file) {
+    if(!($fd = fopen($file,"rb"))) {
+      throw new IOException("Could not open file {$file}!");
+    }
+
+    $this->readBinaryStream($fd);
+    fclose($fd);
+  }
+
+  /**
+   * calculate the bytes needed for a size integer value
+   * @param integer $int The integer value to calculate
+   * @return integer The number of bytes needed
+   */
+  public static function bytesSizeInt($int) {
+    $nbytes = 0;
+
+    if($int > 0xE) $nbytes += 2; // 2 size-bytes
+    if($int > 0xFF) $nbytes += 1; // 3 size-bytes
+    if($int > 0xFFFF) $nbytes += 2; // 5 size-bytes
+
+    return $nbytes;
+  }
+
+  /**
+   * Calculate the byte needed for a „normal” integer value
+   * @param integer $int The integer value
+   * @return integer The number of bytes needed + 1 (because of the „marker byte”)
+   */
+  public static function bytesInt($int) {
+    $nbytes = 1;
+
+    if($int > 0xFF) $nbytes += 1; // 2 byte integer
+    if($int > 0xFFFF) $nbytes += 2; // 4 byte integer
+    if($int > 0xFFFFFFFF) $nbytes += 4; // 8 byte integer
+    if($int < 0) $nbytes += 7; // 8 byte integer (since it is signed)
+
+    return $nbytes + 1; // one „marker” byte
+  }
+
+  /**
+   * „pack” a value (i.e. write the binary representation as big endian to a string) with the specified size
+   * @param integer $nbytes The number of bytes to pack
+   * @param integer $int The integer value to pack
+   * @return string The packed value as string
+   */
+  public static function packItWithSize($nbytes,$int) {
+    $formats = Array("C", "n", "N", "N");
+    $format = $formats[$nbytes-1];
+
+    if($nbytes == 3) return substr(pack($format, $int), -3);
+    return pack($format, $int);
+  }
+
+  /**
+   * „unpack” multiple values of the specified size (i.e. get the integers from their binary representation) from a string
+   * @param integer $nbytes The number of bytes of each value to unpack
+   * @param integer $buff The string packed with integer values
+   * @return array The unpacked integers
+   */
+  public static function unpackWithSize($nbytes,$buff) {
+    $formats = Array("C*", "n*", "N*", "N*");
+    $format = $formats[$nbytes-1];
+
+    if($nbytes == 3) $buff = "\0" . implode("\0", str_split($buff, 3));
+    return unpack($format, $buff);
+  }
+
+  /**
+   * Calculate the bytes needed to save the number of objects
+   * @param integer $count_objects The number of objects
+   * @return integer The number of bytes
+   */
+  public static function bytesNeeded($count_objects) {
+    $nbytes = 0;
+
+    while($count_objects >= 1) {
+      $nbytes++;
+      $count_objects /= 256;
+    }
+
+    return $nbytes;
+  }
+
+  /**
+   * Code an integer to byte representation
+   * @param integer $int The integer value
+   * @return string The packed byte value
+   */
+  public static function intBytes($int) {
+    $intbytes = "";
+
+    if($int > 0xFFFF) $intbytes = "\x12".pack("N", $int); // 4 byte integer
+    elseif($int > 0xFF) $intbytes = "\x11".pack("n", $int); // 2 byte integer
+    else $intbytes = "\x10".pack("C", $int); // 8 byte integer
+
+    return $intbytes;
+  }
+
+  /**
+   * Code an type byte, consisting of the type marker and the length of the type
+   * @param string $type The type byte value (i.e. "d" for dictionaries)
+   * @param integer $type_len The length of the type
+   * @return string The packed type byte value
+   */
+  public static function typeBytes($type,$type_len) {
+    $optional_int = "";
+
+    if($type_len < 15) $type .= sprintf("%x", $type_len);
+    else {
+      $type .= "f";
+      $optional_int = self::intBytes($type_len);
+    }
+
+    return pack("H*", $type).$optional_int;
+  }
+
+  /**
+   * Count number of objects and create a unique table for strings
+   * @param $value The value to count and unique
+   * @return void
+   */
+  protected function uniqueAndCountValues($value) {
+    // no uniquing for other types than CFString and CFData
+    if($value instanceof CFNumber) {
+      $val = $value->getValue();
+      if(intval($val) == $val && !is_float($val) && strpos($val,'.') === false) $this->intSize += self::bytesInt($val);
+      else $this->miscSize += 9; // 9 bytes (8 + marker byte) for real
+      $this->countObjects++;
+      return;
+    }
+    elseif($value instanceof CFDate) {
+      $this->miscSize += 9; // since date in plist is real, we need 9 byte (8 + marker byte)
+      $this->countObjects++;
+      return;
+    }
+    elseif($value instanceof CFBoolean) {
+      $this->countObjects++;
+      $this->miscSize += 1;
+      return;
+    }
+    elseif($value instanceof CFArray) {
+      $cnt = 0;
+      foreach($value as $v) {
+        ++$cnt;
+        $this->uniqueAndCountValues($v);
+        $this->objectRefs++; // each array member is a ref
+      }
+
+      $this->countObjects++;
+      $this->intSize += self::bytesSizeInt($cnt);
+      $this->miscSize++; // marker byte for array
+      return;
+    }
+    elseif($value instanceof CFDictionary) {
+      $cnt = 0;
+      foreach($value as $k => $v) {
+        ++$cnt;
+        if(!isset($this->uniqueTable[$k])) {
+          $this->uniqueTable[$k] = 0;
+          $len = self::binaryStrlen($k);
+          $this->stringSize += $len + 1;
+          $this->intSize += self::bytesSizeInt(self::charsetStrlen($k,'UTF-8'));
+        }
+
+        $this->objectRefs += 2; // both, key and value, are refs
+        $this->uniqueTable[$k]++;
+        $this->uniqueAndCountValues($v);
+      }
+
+      $this->countObjects++;
+      $this->miscSize++; // marker byte for dict
+      $this->intSize += self::bytesSizeInt($cnt);
+      return;
+    }
+    elseif($value instanceOf CFData) {
+      $val = $value->getValue();
+      $len = strlen($val);
+      $this->intSize += self::bytesSizeInt($len);
+      $this->miscSize += $len + 1;
+      $this->countObjects++;
+      return;
+    }
+    else $val = $value->getValue();
+
+    if(!isset($this->uniqueTable[$val])) {
+      $this->uniqueTable[$val] = 0;
+      $len = self::binaryStrlen($val);
+      $this->stringSize += $len + 1;
+      $this->intSize += self::bytesSizeInt(self::charsetStrlen($val,'UTF-8'));
+    }
+    $this->uniqueTable[$val]++;
+  }
+
+  /**
+   * Convert CFPropertyList to binary format; since we have to count our objects we simply unique CFDictionary and CFArray
+   * @return string The binary plist content
+   */
+  public function toBinary() {
+    $this->uniqueTable = Array();
+    $this->countObjects = 0;
+    $this->stringSize = 0;
+    $this->intSize = 0;
+    $this->miscSize = 0;
+    $this->objectRefs = 0;
+
+    $this->writtenObjectCount = 0;
+    $this->objectTable = Array();
+    $this->objectRefSize = 0;
+
+    $this->offsets = Array();
+
+    $binary_str = "bplist00";
+    $value = $this->getValue(true);
+    $this->uniqueAndCountValues($value);
+
+    $this->countObjects += count($this->uniqueTable);
+    $this->objectRefSize = self::bytesNeeded($this->countObjects);
+    $file_size = $this->stringSize + $this->intSize + $this->miscSize + $this->objectRefs * $this->objectRefSize + 40;
+    $offset_size = self::bytesNeeded($file_size);
+    $table_offset = $file_size - 32;
+
+    $this->objectTable = Array();
+    $this->writtenObjectCount = 0;
+    $this->uniqueTable = Array(); // we needed it to calculate several values
+    $value->toBinary($this);
+
+    $object_offset = 8;
+    $offsets = Array();
+
+    for($i=0;$i<count($this->objectTable);++$i) {
+      $binary_str .= $this->objectTable[$i];
+      $offsets[$i] = $object_offset;
+      $object_offset += strlen($this->objectTable[$i]);
+    }
+
+    for($i=0;$i<count($offsets);++$i) {
+      $binary_str .= self::packItWithSize($offset_size, $offsets[$i]);
+    }
+
+
+    $binary_str .= pack("x6CC", $offset_size, $this->objectRefSize);
+    $binary_str .= pack("x4N", $this->countObjects);
+    $binary_str .= pack("x4N", 0);
+    $binary_str .= pack("x4N", $table_offset);
+
+    return $binary_str;
+  }
+
+  /**
+   * Counts the number of bytes the string will have when coded; utf-16be if non-ascii characters are present.
+   * @param string $val The string value
+   * @return integer The length of the coded string in bytes
+   */
+  protected static function binaryStrlen($val) {
+    for($i=0;$i<strlen($val);++$i) {
+      if(ord($val{$i}) >= 128) {
+        $val = self::convertCharset($val, 'UTF-8', 'UTF-16BE');
+        return strlen($val);
+      }
+    }
+
+    return strlen($val);
+  }
+
+  /**
+   * Uniques and transforms a string value to binary format and adds it to the object table
+   * @param string $val The string value
+   * @return integer The position in the object table
+   */
+  public function stringToBinary($val) {
+    $saved_object_count = -1;
+
+    if(!isset($this->uniqueTable[$val])) {
+      $saved_object_count = $this->writtenObjectCount++;
+      $this->uniqueTable[$val] = $saved_object_count;
+      $utf16 = false;
+
+      for($i=0;$i<strlen($val);++$i) {
+        if(ord($val{$i}) >= 128) {
+          $utf16 = true;
+          break;
+        }
+      }
+
+      if($utf16) {
+        $bdata = self::typeBytes("6", mb_strlen($val,'UTF-8')); // 6 is 0110, unicode string (utf16be)
+        $val = self::convertCharset($val, 'UTF-8', 'UTF-16BE');
+        $this->objectTable[$saved_object_count] = $bdata.$val;
+      }
+      else {
+        $bdata = self::typeBytes("5", strlen($val)); // 5 is 0101 which is an ASCII string (seems to be ASCII encoded)
+        $this->objectTable[$saved_object_count] = $bdata.$val;
+      }
+    }
+    else $saved_object_count = $this->uniqueTable[$val];
+
+    return $saved_object_count;
+  }
+
+  /**
+   * Codes an integer to binary format
+   * @param integer $value The integer value
+   * @return string the coded integer
+   */
+  protected function intToBinary($value) {
+    $nbytes = 0;
+    if($value > 0xFF) $nbytes = 1; // 1 byte integer
+    if($value > 0xFFFF) $nbytes += 1; // 4 byte integer
+    if($value > 0xFFFFFFFF) $nbytes += 1; // 8 byte integer
+    if($value < 0) $nbytes = 3; // 8 byte integer, since signed
+
+    $bdata = self::typeBytes("1", $nbytes); // 1 is 0001, type indicator for integer
+    $buff = "";
+
+    if($nbytes < 3) {
+      if($nbytes == 0) $fmt = "C";
+      elseif($nbytes == 1) $fmt = "n";
+      else $fmt = "N";
+
+      $buff = pack($fmt, $value);
+    }
+    else {
+      if(PHP_INT_SIZE > 4) {
+        // 64 bit signed integer; we need the higher and the lower 32 bit of the value
+        $high_word = $value >> 32;
+        $low_word = $value & 0xFFFFFFFF;
+      }
+      else {
+        // since PHP can only handle 32bit signed, we can only get 32bit signed values at this point - values above 0x7FFFFFFF are
+        // floats. So we ignore the existance of 64bit on non-64bit-machines
+        if($value < 0) $high_word = 0xFFFFFFFF;
+        else $high_word = 0;
+        $low_word = $value;
+      }
+      $buff = pack("N", $high_word).pack("N", $low_word);
+    }
+
+    return $bdata.$buff;
+  }
+
+  /**
+   * Codes a real value to binary format
+   * @param float $val The real value
+   * @return string The coded real
+   */
+  protected function realToBinary($val) {
+    $bdata = self::typeBytes("2", 3); // 2 is 0010, type indicator for reals
+    return $bdata.strrev(pack("d", (float)$val));
+  }
+
+  public function uidToBinary($value) {
+    $saved_object_count = $this->writtenObjectCount++;
+
+    $val = "";
+
+    $nbytes = 0;
+    if($value > 0xFF) $nbytes = 1; // 1 byte integer
+    if($value > 0xFFFF) $nbytes += 1; // 4 byte integer
+    if($value > 0xFFFFFFFF) $nbytes += 1; // 8 byte integer
+    if($value < 0) $nbytes = 3; // 8 byte integer, since signed
+
+    $bdata = self::typeBytes("1000", $nbytes); // 1 is 0001, type indicator for integer
+    $buff = "";
+
+    if($nbytes < 3) {
+      if($nbytes == 0) $fmt = "C";
+      elseif($nbytes == 1) $fmt = "n";
+      else $fmt = "N";
+
+      $buff = pack($fmt, $value);
+    }
+    else {
+      if(PHP_INT_SIZE > 4) {
+        // 64 bit signed integer; we need the higher and the lower 32 bit of the value
+        $high_word = $value >> 32;
+        $low_word = $value & 0xFFFFFFFF;
+      }
+      else {
+        // since PHP can only handle 32bit signed, we can only get 32bit signed values at this point - values above 0x7FFFFFFF are
+        // floats. So we ignore the existance of 64bit on non-64bit-machines
+        if($value < 0) $high_word = 0xFFFFFFFF;
+        else $high_word = 0;
+        $low_word = $value;
+      }
+      $buff = pack("N", $high_word).pack("N", $low_word);
+    }
+
+    $val = $bdata.$buff;
+
+    $this->objectTable[$saved_object_count] = $val;
+    return $saved_object_count;
+  }
+
+  /**
+   * Converts a numeric value to binary and adds it to the object table
+   * @param numeric $value The numeric value
+   * @return integer The position in the object table
+   */
+  public function numToBinary($value) {
+    $saved_object_count = $this->writtenObjectCount++;
+
+    $val = "";
+    if(intval($value) == $value && !is_float($value) && strpos($value,'.') === false) $val = $this->intToBinary($value);
+    else $val = $this->realToBinary($value);
+
+    $this->objectTable[$saved_object_count] = $val;
+    return $saved_object_count;
+  }
+
+  /**
+   * Convert date value (apple format) to binary and adds it to the object table
+   * @param integer $value The date value
+   * @return integer The position of the coded value in the object table
+   */
+  public function dateToBinary($val) {
+    $saved_object_count = $this->writtenObjectCount++;
+
+    $hour = gmdate("H",$val);
+    $min = gmdate("i",$val);
+    $sec = gmdate("s",$val);
+    $mday = gmdate("j",$val);
+    $mon = gmdate("n",$val);
+    $year = gmdate("Y",$val);
+
+    $val = gmmktime($hour,$min,$sec,$mon,$mday,$year) - CFDate::DATE_DIFF_APPLE_UNIX; // CFDate is a real, number of seconds since 01/01/2001 00:00:00 GMT
+
+    $bdata = self::typeBytes("3", 3); // 3 is 0011, type indicator for date
+    $this->objectTable[$saved_object_count] = $bdata.strrev(pack("d", $val));
+
+    return $saved_object_count;
+  }
+
+  /**
+   * Convert a bool value to binary and add it to the object table
+   * @param bool $val The boolean value
+   * @return integer The position in the object table
+   */
+  public function boolToBinary($val) {
+    $saved_object_count = $this->writtenObjectCount++;
+    $this->objectTable[$saved_object_count] = $val ? "\x9" : "\x8"; // 0x9 is 1001, type indicator for true; 0x8 is 1000, type indicator for false
+    return $saved_object_count;
+  }
+
+  /**
+   * Convert data value to binary format and add it to the object table
+   * @param string $val The data value
+   * @return integer The position in the object table
+   */
+  public function dataToBinary($val) {
+    $saved_object_count = $this->writtenObjectCount++;
+
+    $bdata = self::typeBytes("4", strlen($val)); // a is 1000, type indicator for data
+    $this->objectTable[$saved_object_count] = $bdata.$val;
+
+    return $saved_object_count;
+  }
+
+  /**
+   * Convert array to binary format and add it to the object table
+   * @param CFArray $val The array to convert
+   * @return integer The position in the object table
+   */
+  public function arrayToBinary($val) {
+    $saved_object_count = $this->writtenObjectCount++;
+
+    $bdata = self::typeBytes("a", count($val->getValue())); // a is 1010, type indicator for arrays
+
+    foreach($val as $v) {
+      $bval = $v->toBinary($this);
+      $bdata .= self::packItWithSize($this->objectRefSize, $bval);
+    }
+
+    $this->objectTable[$saved_object_count] = $bdata;
+    return $saved_object_count;
+  }
+
+  /**
+   * Convert dictionary to binary format and add it to the object table
+   * @param CFDictionary $val The dict to convert
+   * @return integer The position in the object table
+   */
+  public function dictToBinary($val) {
+    $saved_object_count = $this->writtenObjectCount++;
+    $bdata = self::typeBytes("d", count($val->getValue())); // d=1101, type indicator for dictionary
+
+    foreach($val as $k => $v) {
+      $str = new CFString($k);
+      $key = $str->toBinary($this);
+      $bdata .= self::packItWithSize($this->objectRefSize, $key);
+    }
+
+    foreach($val as $k => $v) {
+      $bval = $v->toBinary($this);
+      $bdata .= self::packItWithSize($this->objectRefSize, $bval);
+    }
+
+    $this->objectTable[$saved_object_count] = $bdata;
+    return $saved_object_count;
+  }
+
+}
+
+# eof
diff --git a/lib/plist/classes/CFPropertyList/CFPropertyList.php b/lib/plist/classes/CFPropertyList/CFPropertyList.php
new file mode 100644 (file)
index 0000000..0e07b7c
--- /dev/null
@@ -0,0 +1,608 @@
+<?php
+/**
+ * CFPropertyList
+ * {@link http://developer.apple.com/documentation/Darwin/Reference/ManPages/man5/plist.5.html Property Lists}
+ * @author Rodney Rehm <rodney.rehm@medialize.de>
+ * @author Christian Kruse <cjk@wwwtech.de>
+ * @package plist
+ * @version $Id$
+ * @example example-read-01.php Read an XML PropertyList
+ * @example example-read-02.php Read a Binary PropertyList
+ * @example example-read-03.php Read a PropertyList without knowing the type
+ * @example example-create-01.php Using the CFPropertyList API
+ * @example example-create-02.php Using {@link CFTypeDetector}
+ * @example example-create-03.php Using {@link CFTypeDetector} with {@link CFDate} and {@link CFData}
+ * @example example-modify-01.php Read, modify and save a PropertyList
+ */
+
+namespace CFPropertyList;
+use \Iterator, \DOMDocument, \DOMException, DOMImplementation, DOMNode;
+
+/**
+ * Require IOException, PListException, CFType and CFBinaryPropertyList
+ */
+require_once(__DIR__.'/IOException.php');
+require_once(__DIR__.'/PListException.php');
+require_once(__DIR__.'/CFType.php');
+require_once(__DIR__.'/CFBinaryPropertyList.php');
+require_once(__DIR__.'/CFTypeDetector.php');
+
+/**
+ * Property List
+ * Interface for handling reading, editing and saving Property Lists as defined by Apple.
+ * @author Rodney Rehm <rodney.rehm@medialize.de>
+ * @author Christian Kruse <cjk@wwwtech.de>
+ * @package plist
+ * @example example-read-01.php Read an XML PropertyList
+ * @example example-read-02.php Read a Binary PropertyList
+ * @example example-read-03.php Read a PropertyList without knowing the type
+ * @example example-create-01.php Using the CFPropertyList API
+ * @example example-create-02.php Using {@link CFTypeDetector}
+ * @example example-create-03.php Using {@link CFTypeDetector} with {@link CFDate} and {@link CFData}
+ * @example example-create-04.php Using and extended {@link CFTypeDetector}
+ */
+class CFPropertyList extends CFBinaryPropertyList implements Iterator {
+  /**
+   * Format constant for binary format
+   * @var integer
+   */
+  const FORMAT_BINARY = 1;
+
+  /**
+   * Format constant for xml format
+   * @var integer
+   */
+  const FORMAT_XML = 2;
+
+  /**
+   * Format constant for automatic format recognizing
+   * @var integer
+   */
+  const FORMAT_AUTO = 0;
+
+  /**
+   * Path of PropertyList
+   * @var string
+   */
+  protected $file = null;
+  
+  /**
+   * Detected format of PropertyList
+   * @var integer
+   */
+  protected $detectedFormat = null;
+
+  /**
+   * Path of PropertyList
+   * @var integer
+   */
+  protected $format = null;
+
+  /**
+   * CFType nodes
+   * @var array
+   */
+  protected $value = array();
+
+  /**
+   * Position of iterator {@link http://php.net/manual/en/class.iterator.php}
+   * @var integer
+   */
+  protected $iteratorPosition = 0;
+
+  /**
+   * List of Keys for numerical iterator access {@link http://php.net/manual/en/class.iterator.php}
+   * @var array
+   */
+  protected $iteratorKeys = null;
+
+  /**
+   * List of NodeNames to ClassNames for resolving plist-files
+   * @var array
+   */
+  protected static $types = array(
+    'string'  => 'CFString',
+    'real'    => 'CFNumber',
+    'integer' => 'CFNumber',
+    'date'    => 'CFDate',
+    'true'    => 'CFBoolean',
+    'false'   => 'CFBoolean',
+    'data'    => 'CFData',
+    'array'   => 'CFArray',
+    'dict'    => 'CFDictionary'
+ );
+
+
+  /**
+   * Create new CFPropertyList.
+   * If a path to a PropertyList is specified, it is loaded automatically.
+   * @param string $file Path of PropertyList
+   * @param integer $format he format of the property list, see {@link FORMAT_XML}, {@link FORMAT_BINARY} and {@link FORMAT_AUTO}, defaults to {@link FORMAT_AUTO}
+   * @throws IOException if file could not be read by {@link load()}
+   * @uses $file for storing the current file, if specified
+   * @uses load() for loading the plist-file
+   */
+  public function __construct($file=null,$format=self::FORMAT_AUTO) {
+    $this->file = $file;
+    $this->format = $format;
+    $this->detectedFormat = $format;
+    if($this->file) $this->load();
+  }
+
+  /**
+   * Load an XML PropertyList.
+   * @param string $file Path of PropertyList, defaults to {@link $file}
+   * @return void
+   * @throws IOException if file could not be read
+   * @throws DOMException if XML-file could not be read properly
+   * @uses load() to actually load the file
+   */
+  public function loadXML($file=null) {
+    $this->load($file,CFPropertyList::FORMAT_XML);
+  }
+
+  /**
+   * Load an XML PropertyList.
+   * @param resource $stream A stream containing the xml document.
+   * @return void
+   * @throws IOException if stream could not be read
+   * @throws DOMException if XML-stream could not be read properly
+   */
+  public function loadXMLStream($stream) {
+    if(($contents = stream_get_contents($stream)) === FALSE) throw IOException::notReadable('<stream>');
+    $this->parse($contents,CFPropertyList::FORMAT_XML);
+  }
+
+  /**
+   * Load an binary PropertyList.
+   * @param string $file Path of PropertyList, defaults to {@link $file}
+   * @return void
+   * @throws IOException if file could not be read
+   * @throws PListException if binary plist-file could not be read properly
+   * @uses load() to actually load the file
+   */
+  public function loadBinary($file=null) {
+    $this->load($file,CFPropertyList::FORMAT_BINARY);
+  }
+
+  /**
+   * Load an binary PropertyList.
+   * @param stream $stream Stream containing the PropertyList
+   * @return void
+   * @throws IOException if file could not be read
+   * @throws PListException if binary plist-file could not be read properly
+   * @uses parse() to actually load the file
+   */
+  public function loadBinaryStream($stream) {
+    if(($contents = stream_get_contents($stream)) === FALSE) throw IOException::notReadable('<stream>');
+    $this->parse($contents,CFPropertyList::FORMAT_BINARY);
+  }
+
+  /**
+   * Load a plist file.
+   * Load and import a plist file.
+   * @param string $file Path of PropertyList, defaults to {@link $file}
+   * @param integer $format The format of the property list, see {@link FORMAT_XML}, {@link FORMAT_BINARY} and {@link FORMAT_AUTO}, defaults to {@link $format}
+   * @return void
+   * @throws PListException if file format version is not 00
+   * @throws IOException if file could not be read
+   * @throws DOMException if plist file could not be parsed properly
+   * @uses $file if argument $file was not specified
+   * @uses $value reset to empty array
+   * @uses import() for importing the values
+   */
+  public function load($file=null,$format=null) {
+    $file = $file ? $file : $this->file;
+    $format = $format !== null ? $format : $this->format;
+    $this->value = array();
+
+    if(!is_readable($file)) throw IOException::notReadable($file);
+
+    switch($format) {
+      case CFPropertyList::FORMAT_BINARY:
+        $this->readBinary($file);
+        break;
+      case CFPropertyList::FORMAT_AUTO: // what we now do is ugly, but neccessary to recognize the file format
+        $fd = fopen($file,"rb");
+        if(($magic_number = fread($fd,8)) === false) throw IOException::notReadable($file);
+        fclose($fd);
+
+        $filetype = substr($magic_number,0,6);
+        $version  = substr($magic_number,-2);
+
+        if($filetype == "bplist") {
+          if($version != "00") throw new PListException("Wrong file format version! Expected 00, got $version!");
+          $this->detectedFormat = CFPropertyList::FORMAT_BINARY;
+          $this->readBinary($file);
+          break;
+        }
+        $this->detectedFormat = CFPropertyList::FORMAT_XML;
+        // else: xml format, break not neccessary
+      case CFPropertyList::FORMAT_XML:
+        $doc = new DOMDocument();
+        if(!$doc->load($file)) throw new DOMException();
+        $this->import($doc->documentElement, $this);
+        break;
+    }
+  }
+
+  /**
+   * Parse a plist string.
+   * Parse and import a plist string.
+   * @param string $str String containing the PropertyList, defaults to {@link $content}
+   * @param integer $format The format of the property list, see {@link FORMAT_XML}, {@link FORMAT_BINARY} and {@link FORMAT_AUTO}, defaults to {@link $format}
+   * @return void
+   * @throws PListException if file format version is not 00
+   * @throws IOException if file could not be read
+   * @throws DOMException if plist file could not be parsed properly
+   * @uses $content if argument $str was not specified
+   * @uses $value reset to empty array
+   * @uses import() for importing the values
+   */
+  public function parse($str=NULL,$format=NULL) {
+    $format = $format !== null ? $format : $this->format;
+    $str = $str !== null ? $str : $this->content;
+    $this->value = array();
+
+    switch($format) {
+      case CFPropertyList::FORMAT_BINARY:
+        $this->parseBinary($str);
+        break;
+      case CFPropertyList::FORMAT_AUTO: // what we now do is ugly, but neccessary to recognize the file format
+        if(($magic_number = substr($str,0,8)) === false) throw IOException::notReadable("<string>");
+
+        $filetype = substr($magic_number,0,6);
+        $version  = substr($magic_number,-2);
+
+        if($filetype == "bplist") {
+          if($version != "00") throw new PListException("Wrong file format version! Expected 00, got $version!");
+          $this->detectedFormat = CFPropertyList::FORMAT_BINARY;
+          $this->parseBinary($str);
+          break;
+        }
+        $this->detectedFormat = CFPropertyList::FORMAT_XML;
+        // else: xml format, break not neccessary
+      case CFPropertyList::FORMAT_XML:
+        $doc = new DOMDocument();
+        if(!$doc->loadXML($str)) throw new DOMException();
+        $this->import($doc->documentElement, $this);
+        break;
+    }
+  }
+
+  /**
+   * Convert a DOMNode into a CFType.
+   * @param DOMNode $node Node to import children of
+   * @param CFDictionary|CFArray|CFPropertyList $parent
+   * @return void
+   */
+  protected function import(DOMNode $node, $parent) {
+    // abort if there are no children
+    if(!$node->childNodes->length) return;
+
+    foreach($node->childNodes as $n) {
+      // skip if we can't handle the element
+      if(!isset(self::$types[$n->nodeName])) continue;
+
+      $class = 'CFPropertyList\\'.self::$types[$n->nodeName];
+      $key = null;
+
+      // find previous <key> if possible
+      $ps = $n->previousSibling;
+      while($ps && $ps->nodeName == '#text' && $ps->previousSibling) $ps = $ps->previousSibling;
+
+      // read <key> if possible
+      if($ps && $ps->nodeName == 'key') $key = $ps->firstChild->nodeValue;
+
+      switch($n->nodeName) {
+        case 'date':
+          $value = new $class(CFDate::dateValue($n->nodeValue));
+          break;