Merge branch 'MDL-68125-regression' of https://github.com/brendanheywood/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Thu, 12 Mar 2020 00:19:23 +0000 (01:19 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Thu, 12 Mar 2020 00:19:23 +0000 (01:19 +0100)
126 files changed:
.eslintignore
.stylelintignore
admin/settings/h5p.php
backup/moodle2/tests/moodle2_test.php
backup/util/dbops/restore_controller_dbops.class.php
calendar/amd/build/calendar_mini.min.js.map
calendar/amd/src/calendar_mini.js
calendar/classes/external/day_exporter.php
calendar/renderer.php
calendar/templates/minicalendar_day_link.mustache
calendar/templates/month_detailed.mustache
calendar/templates/month_mini.mustache
calendar/templates/threemonth_month.mustache
course/classes/local/service/content_item_service.php
course/lib.php
course/tests/courselib_test.php
enrol/lti/classes/task/sync_grades.php
enrol/manual/amd/build/quickenrolment.min.js
enrol/manual/amd/build/quickenrolment.min.js.map
enrol/manual/amd/src/quickenrolment.js
favourites/classes/local/repository/favourite_repository.php
favourites/classes/local/service/user_favourite_service.php
favourites/tests/repository_test.php
favourites/tests/user_favourite_service_test.php
filter/displayh5p/filter.php
filter/displayh5p/tests/behat/h5p_filter.feature
filter/displayh5p/tests/filter_test.php
grade/report/history/db/upgrade.php [new file with mode: 0644]
grade/report/history/settings.php
grade/report/history/version.php
h5p/classes/autoloader.php [deleted file]
h5p/classes/core.php
h5p/classes/factory.php
h5p/classes/local/library/autoloader.php [new file with mode: 0644]
h5p/classes/local/library/handler.php [new file with mode: 0644]
h5p/classes/player.php
h5p/h5plib/v124/classes/local/library/handler.php [new file with mode: 0644]
h5p/h5plib/v124/classes/privacy/provider.php [new file with mode: 0644]
h5p/h5plib/v124/joubel/core/LICENSE.txt [moved from lib/h5p/LICENSE.txt with 100% similarity]
h5p/h5plib/v124/joubel/core/README.txt [moved from lib/h5p/README.txt with 100% similarity]
h5p/h5plib/v124/joubel/core/doc/spec_en.html [moved from lib/h5p/doc/spec_en.html with 100% similarity]
h5p/h5plib/v124/joubel/core/embed.php [moved from lib/h5p/embed.php with 100% similarity]
h5p/h5plib/v124/joubel/core/fonts/h5p-core-23.eot [moved from lib/h5p/fonts/h5p-core-23.eot with 100% similarity]
h5p/h5plib/v124/joubel/core/fonts/h5p-core-23.svg [moved from lib/h5p/fonts/h5p-core-23.svg with 100% similarity]
h5p/h5plib/v124/joubel/core/fonts/h5p-core-23.ttf [moved from lib/h5p/fonts/h5p-core-23.ttf with 100% similarity]
h5p/h5plib/v124/joubel/core/fonts/h5p-core-23.woff [moved from lib/h5p/fonts/h5p-core-23.woff with 100% similarity]
h5p/h5plib/v124/joubel/core/h5p-default-storage.class.php [moved from lib/h5p/h5p-default-storage.class.php with 100% similarity]
h5p/h5plib/v124/joubel/core/h5p-development.class.php [moved from lib/h5p/h5p-development.class.php with 100% similarity]
h5p/h5plib/v124/joubel/core/h5p-event-base.class.php [moved from lib/h5p/h5p-event-base.class.php with 100% similarity]
h5p/h5plib/v124/joubel/core/h5p-file-storage.interface.php [moved from lib/h5p/h5p-file-storage.interface.php with 100% similarity]
h5p/h5plib/v124/joubel/core/h5p-metadata.class.php [moved from lib/h5p/h5p-metadata.class.php with 100% similarity]
h5p/h5plib/v124/joubel/core/h5p.classes.php [moved from lib/h5p/h5p.classes.php with 100% similarity]
h5p/h5plib/v124/joubel/core/images/h5p.svg [moved from lib/h5p/images/h5p.svg with 100% similarity]
h5p/h5plib/v124/joubel/core/images/throbber.gif [moved from lib/h5p/images/throbber.gif with 100% similarity]
h5p/h5plib/v124/joubel/core/js/h5p-action-bar.js [moved from lib/h5p/js/h5p-action-bar.js with 100% similarity]
h5p/h5plib/v124/joubel/core/js/h5p-confirmation-dialog.js [moved from lib/h5p/js/h5p-confirmation-dialog.js with 100% similarity]
h5p/h5plib/v124/joubel/core/js/h5p-content-type.js [moved from lib/h5p/js/h5p-content-type.js with 100% similarity]
h5p/h5plib/v124/joubel/core/js/h5p-content-upgrade-process.js [moved from lib/h5p/js/h5p-content-upgrade-process.js with 100% similarity]
h5p/h5plib/v124/joubel/core/js/h5p-content-upgrade-worker.js [moved from lib/h5p/js/h5p-content-upgrade-worker.js with 100% similarity]
h5p/h5plib/v124/joubel/core/js/h5p-content-upgrade.js [moved from lib/h5p/js/h5p-content-upgrade.js with 100% similarity]
h5p/h5plib/v124/joubel/core/js/h5p-data-view.js [moved from lib/h5p/js/h5p-data-view.js with 100% similarity]
h5p/h5plib/v124/joubel/core/js/h5p-display-options.js [moved from lib/h5p/js/h5p-display-options.js with 100% similarity]
h5p/h5plib/v124/joubel/core/js/h5p-embed.js [moved from lib/h5p/js/h5p-embed.js with 100% similarity]
h5p/h5plib/v124/joubel/core/js/h5p-event-dispatcher.js [moved from lib/h5p/js/h5p-event-dispatcher.js with 100% similarity]
h5p/h5plib/v124/joubel/core/js/h5p-library-details.js [moved from lib/h5p/js/h5p-library-details.js with 100% similarity]
h5p/h5plib/v124/joubel/core/js/h5p-library-list.js [moved from lib/h5p/js/h5p-library-list.js with 100% similarity]
h5p/h5plib/v124/joubel/core/js/h5p-resizer.js [moved from lib/h5p/js/h5p-resizer.js with 100% similarity]
h5p/h5plib/v124/joubel/core/js/h5p-utils.js [moved from lib/h5p/js/h5p-utils.js with 100% similarity]
h5p/h5plib/v124/joubel/core/js/h5p-version.js [moved from lib/h5p/js/h5p-version.js with 100% similarity]
h5p/h5plib/v124/joubel/core/js/h5p-x-api-event.js [moved from lib/h5p/js/h5p-x-api-event.js with 100% similarity]
h5p/h5plib/v124/joubel/core/js/h5p-x-api.js [moved from lib/h5p/js/h5p-x-api.js with 100% similarity]
h5p/h5plib/v124/joubel/core/js/h5p.js [moved from lib/h5p/js/h5p.js with 100% similarity]
h5p/h5plib/v124/joubel/core/js/jquery.js [moved from lib/h5p/js/jquery.js with 100% similarity]
h5p/h5plib/v124/joubel/core/js/request-queue.js [moved from lib/h5p/js/request-queue.js with 100% similarity]
h5p/h5plib/v124/joubel/core/js/settings/h5p-disable-hub.js [moved from lib/h5p/js/settings/h5p-disable-hub.js with 100% similarity]
h5p/h5plib/v124/joubel/core/readme_moodle.txt [moved from lib/h5p/readme_moodle.txt with 100% similarity]
h5p/h5plib/v124/joubel/core/styles/h5p-admin.css [moved from lib/h5p/styles/h5p-admin.css with 100% similarity]
h5p/h5plib/v124/joubel/core/styles/h5p-confirmation-dialog.css [moved from lib/h5p/styles/h5p-confirmation-dialog.css with 100% similarity]
h5p/h5plib/v124/joubel/core/styles/h5p-core-button.css [moved from lib/h5p/styles/h5p-core-button.css with 100% similarity]
h5p/h5plib/v124/joubel/core/styles/h5p.css [moved from lib/h5p/styles/h5p.css with 100% similarity]
h5p/h5plib/v124/lang/en/h5plib_v124.php [new file with mode: 0644]
h5p/h5plib/v124/thirdpartylibs.xml [new file with mode: 0644]
h5p/h5plib/v124/version.php [new file with mode: 0644]
h5p/lib.php
h5p/tests/event_h5p_deleted_test.php
h5p/tests/event_h5p_viewed_test.php
h5p/tests/external_test.php
h5p/tests/generator/lib.php
h5p/tests/generator_test.php
h5p/tests/h5p_core_test.php
h5p/tests/h5p_file_storage_test.php
h5p/upgrade.txt [new file with mode: 0644]
lang/en/h5p.php
lang/en/hub.php
lang/en/plugin.php
lib/adminlib.php
lib/amd/build/modal.min.js
lib/amd/build/modal.min.js.map
lib/amd/build/modal_factory.min.js
lib/amd/build/modal_factory.min.js.map
lib/amd/src/modal.js
lib/amd/src/modal_factory.js
lib/behat/classes/util.php
lib/classes/hub/registration.php
lib/classes/plugin_manager.php
lib/classes/plugininfo/h5plib.php [new file with mode: 0644]
lib/components.json
lib/editor/atto/plugins/h5p/lib.php
lib/externallib.php
lib/form/templates/element-checkbox.mustache
lib/form/templates/element-date_selector.mustache
lib/form/templates/element-date_time_selector.mustache
lib/form/templates/element-group.mustache
lib/form/templates/element-template-inline.mustache
lib/form/templates/element-template.mustache
lib/moodlelib.php
lib/outputrequirementslib.php
lib/tests/externallib_test.php
lib/tests/fixtures/test_external_function_throwable.php [new file with mode: 0644]
lib/tests/h5p_get_content_types_task_test.php
lib/tests/moodlelib_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
theme/boost/scss/moodle/question.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css

index 19f5a98..c111d11 100644 (file)
@@ -9,6 +9,7 @@ cache/stores/mongodb/MongoDB/
 enrol/lti/ims-blti/
 filter/algebra/AlgParser.pm
 filter/tex/mimetex.*
+h5p/h5plib/v124/joubel/core/
 lib/editor/atto/plugins/html/yui/src/codemirror/
 lib/editor/atto/plugins/html/yui/src/beautify/
 lib/editor/atto/yui/src/rangy/js/*.*
@@ -63,7 +64,6 @@ lib/php-jwt/
 lib/babel-polyfill/
 lib/mdn-polyfills/
 lib/emoji-data/
-lib/h5p/
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
 media/player/videojs/videojs/
index 4d4c82f..a828212 100644 (file)
@@ -10,6 +10,7 @@ cache/stores/mongodb/MongoDB/
 enrol/lti/ims-blti/
 filter/algebra/AlgParser.pm
 filter/tex/mimetex.*
+h5p/h5plib/v124/joubel/core/
 lib/editor/atto/plugins/html/yui/src/codemirror/
 lib/editor/atto/plugins/html/yui/src/beautify/
 lib/editor/atto/yui/src/rangy/js/*.*
@@ -64,7 +65,6 @@ lib/php-jwt/
 lib/babel-polyfill/
 lib/mdn-polyfills/
 lib/emoji-data/
-lib/h5p/
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
 media/player/videojs/videojs/
index 881b34c..3b9302d 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-// Settings page.
+// H5P overview.
 $ADMIN->add('h5p', new admin_externalpage('h5poverview', get_string('h5poverview', 'core_h5p'),
     new moodle_url('/h5p/overview.php'), ['moodle/site:config']));
-$ADMIN->add('h5p', new admin_externalpage('h5psettings', get_string('h5pmanage', 'core_h5p'),
+
+// Manage H5P libraries page.
+$ADMIN->add('h5p', new admin_externalpage('h5pmanagelibraries', get_string('h5pmanage', 'core_h5p'),
     new moodle_url('/h5p/libraries.php'), ['moodle/site:config', 'moodle/h5p:updatelibraries']));
+
+// H5P settings.
+$defaulth5plib = \core_h5p\local\library\autoloader::get_default_handler();
+if (!empty($defaulth5plib)) {
+    // As for now this page only has this setting, it will be hidden if there isn't any H5P libraries handler defined.
+    $settings = new admin_settingpage('h5psettings', new lang_string('h5psettings', 'core_h5p'));
+    $ADMIN->add('h5p', $settings);
+
+    $settings->add(new admin_settings_h5plib_handler_select('h5plibraryhandler', new lang_string('h5plibraryhandler', 'core_h5p'),
+        new lang_string('h5plibraryhandler_help', 'core_h5p'), $defaulth5plib));
+}
index c4f9e2c..6649934 100644 (file)
@@ -955,6 +955,52 @@ class core_backup_moodle2_testcase extends advanced_testcase {
         $this->assertEquals('', $requests[2]->searcharea);
     }
 
+    /**
+     * Test restoring courses based on the backup plan. Primarily used with
+     * the import functionality
+     */
+    public function test_restore_course_using_plan_defaults() {
+        global $DB, $CFG, $USER;
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $CFG->enableglobalsearch = true;
+
+        // Set admin config setting so that activities are not restored by default.
+        set_config('restore_general_activities', 0, 'restore');
+
+        // Create a course.
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $course2 = $generator->create_course();
+        $course3 = $generator->create_course();
+
+        // Add a forum.
+        $forum = $generator->create_module('forum', ['course' => $course->id]);
+
+        // Backup course...
+        $CFG->backup_file_logger_level = backup::LOG_NONE;
+        $bc = new backup_controller(backup::TYPE_1COURSE, $course->id,
+            backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
+            $USER->id);
+        $backupid = $bc->get_backupid();
+        $bc->execute_plan();
+        $bc->destroy();
+
+        // Restore it on top of course2 (should duplicate the forum).
+        $rc = new restore_controller($backupid, $course2->id,
+            backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id,
+            backup::TARGET_EXISTING_ADDING, null, backup::RELEASESESSION_NO);
+        $this->assertTrue($rc->execute_precheck());
+        $rc->execute_plan();
+        $rc->destroy();
+
+        // Get the forums now on the old course.
+        $modinfo = get_fast_modinfo($course2->id);
+        $forums = $modinfo->get_instances_of('forum');
+        $this->assertCount(0, $forums);
+    }
+
     /**
      * The Question category hierarchical structure was changed in Moodle 3.5.
      * From 3.5, all question categories in each context are a child of a single top level question category for that context.
index 1894eeb..6649777 100644 (file)
@@ -282,7 +282,14 @@ abstract class restore_controller_dbops extends restore_dbops {
             if ($plan->setting_exists($settingname)) {
                 $setting = $plan->get_setting($settingname);
                 $value = self::get_setting_default($config, $setting);
-                $locked = (get_config('restore', $config . '_locked') == true);
+                $locked = (get_config('restore',$config . '_locked') == true);
+
+                // Use the original value when this is an import and the setting is unlocked.
+                if ($controller->get_mode() == backup::MODE_IMPORT && $controller->get_interactive()) {
+                    if (!$uselocks || !$locked) {
+                        $value = $setting->get_value();
+                    }
+                }
 
                 // We can only update the setting if it isn't already locked by config or permission.
                 if ($setting->get_status() != base_setting::LOCKED_BY_CONFIG
index 3137c7d..8fd4293 100644 (file)
Binary files a/calendar/amd/build/calendar_mini.min.js.map and b/calendar/amd/build/calendar_mini.min.js.map differ
index 30f0385..e419a25 100644 (file)
@@ -20,7 +20,7 @@
  * components by listening for and responding to different events
  * triggered within the calendar UI.
  *
- * @module     core_calendar/calendar
+ * @module     core_calendar/calendar_mini
  * @package    core_calendar
  * @copyright  2017 Andrew Nicols <andrew@nicols.co.uk>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
index 7e888b8..377d2cd 100644 (file)
@@ -126,6 +126,10 @@ class day_exporter extends exporter {
                 'type' => PARAM_URL,
                 'optional' => true,
             ],
+            'viewdaylinktitle' => [
+                'type' => PARAM_RAW,
+                'optional' => true,
+            ],
             'events' => [
                 'type' => calendar_event_exporter::read_properties_definition(),
                 'multiple' => true,
@@ -183,6 +187,10 @@ class day_exporter extends exporter {
             'viewdaylink' => $this->url->out(false),
         ];
 
+        if ($viewdaylinktitle = $this->get_view_link_title()) {
+            $return['viewdaylinktitle'] = $viewdaylinktitle;
+        }
+
 
         $cache = $this->related['cache'];
         $eventexporters = array_map(function($event) use ($cache, $output) {
@@ -267,4 +275,22 @@ class day_exporter extends exporter {
             'time' => $this->calendar->time,
         ]);
     }
+
+    /**
+     * Get the title for view link.
+     *
+     * @return string
+     */
+    protected function get_view_link_title() {
+        $title = null;
+
+        $userdate = userdate($this->data[0], get_string('strftimedayshort'));
+        if ($this->data['istoday']) {
+            $title = get_string('todayplustitle', 'calendar', $userdate);
+        } else if (count($this->related['events'])) {
+            $title = get_string('eventsfor', 'calendar', $userdate);
+        }
+
+        return $title;
+    }
 }
index a078c02..0679979 100644 (file)
@@ -292,13 +292,15 @@ class core_calendar_renderer extends plugin_renderer_base {
         $courseurl = new moodle_url($returnurl);
         $courseurl->remove_params('course');
 
-        if ($label === null) {
+        $labelattributes = [];
+        if (empty($label)) {
             $label = get_string('listofcourses');
+            $labelattributes['class'] = 'sr-only';
         }
 
-        $select = html_writer::label($label, 'course', false, ['class' => 'mr-1']);
+        $select = html_writer::label($label, 'course', false, $labelattributes);
         $select .= html_writer::select($courseoptions, 'course', $selected, false,
-                ['class' => 'cal_courses_flt mr-auto']);
+                ['class' => 'cal_courses_flt ml-1 mr-auto', 'id' => 'course']);
 
         return $select;
     }
index 5438bd5..8772356 100644 (file)
@@ -34,7 +34,7 @@
     }} data-toggle="popover"{{!
     }} data-html="true"{{!
     }} data-region="mini-day-link"{{!
-    }} data-trigger="hover"{{!
+    }} data-trigger="hover focus"{{!
     }} data-placement="top"{{!
     }} data-year="{{year}}"{{!
     }} data-month="{{date.mon}}"{{!
@@ -42,6 +42,7 @@
     }} data-categoryid="{{categoryid}}"{{!
     }} data-title="{{$title}}{{title}}{{/title}}"{{!
     }} data-alternate="{{$nocontent}}{{/nocontent}}"{{!
+    }} aria-label="{{viewdaylinktitle}}"{{!
 }}>{{$day}}{{day}}{{/day}}</a>
 <div class="hidden">
     {{$content}}{{/content}}
index d5dbfa4..e1273ec 100644 (file)
@@ -46,7 +46,7 @@
         <thead>
             <tr>
                 {{# daynames }}
-                <th class="header text-xs-center">
+                <th class="header text-xs-center" aria-label="{{fullname}}">
                     {{shortname}}
                 </th>
                 {{/ daynames }}
@@ -72,7 +72,7 @@
                         data-new-event-timestamp="{{neweventtimestamp}}">
                         <div class="d-none d-md-block hidden-phone text-xs-center">
                             {{#hasevents}}
-                                <a data-action="view-day-link" href="#" class="day" title="{{viewdaylinktitle}}"
+                                <a data-action="view-day-link" href="#" class="day" aria-label="{{viewdaylinktitle}}"
                                     data-year="{{date.year}}" data-month="{{date.mon}}" data-day="{{mday}}"
                                     data-courseid="{{courseid}}" data-categoryid="{{categoryid}}"
                                     data-timestamp="{{timestamp}}">{{mday}}</a>
                         </div>
                         <div class="d-md-none hidden-desktop hidden-tablet">
                             {{#hasevents}}
-                                <a data-action="view-day-link" href="#" class="day" title="{{viewdaylinktitle}}"
+                                <a data-action="view-day-link" href="#" class="day" aria-label="{{viewdaylinktitle}}"
                                     data-year="{{date.year}}" data-month="{{date.mon}}" data-day="{{mday}}"
                                     data-courseid="{{courseid}}" data-categoryid="{{categoryid}}"
                                     data-timestamp="{{timestamp}}">{{mday}}</a>
                             {{/hasevents}}
                             {{^hasevents}}
-                                <div data-region="day-content">
                                     {{mday}}
-                                </div>
                             {{/hasevents}}
                         </div>
                     </td>
index d5cef72..ff10a74 100644 (file)
@@ -78,8 +78,8 @@
         <thead>
           <tr>
                 {{# daynames }}
-                <th class="header text-xs-center" scope="col">
-                    <abbr title="{{fullname}}">{{shortname}}</abbr>
+                <th class="header text-xs-center" scope="col" aria-label="{{fullname}}">
+                    {{shortname}}
                 </th>
                 {{/ daynames }}
             </tr>
index 8d17536..7dddb2f 100644 (file)
@@ -15,7 +15,7 @@
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template calendar/calendar_threemonth
+    @template calendar/threemonth_month
 
     Calendar view to show three months as a block.
 
index 3c2a605..76aadb9 100644 (file)
@@ -141,10 +141,17 @@ class content_item_service {
 
         $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
         $favourites = [];
-        foreach ($itemtypes as $itemtype) {
-            $favs = $ufservice->find_favourites_by_type(self::COMPONENT, $itemtype);
-            $favobj = (object) ['itemtype' => $itemtype, 'ids' => array_column($favs, 'itemid')];
-            $favourites[] = $favobj;
+        $favs = $ufservice->find_all_favourites(self::COMPONENT, $itemtypes);
+        $favsreduced = array_reduce($favs, function($carry, $item) {
+            $carry[$item->itemtype][$item->itemid] = 0;
+            return $carry;
+        }, []);
+
+        foreach ($itemtypes as $type) {
+            $favourites[] = (object) [
+                'itemtype' => $type,
+                'ids' => isset($favsreduced[$type]) ? array_keys($favsreduced[$type]) : []
+            ];
         }
         return $favourites;
     }
index 18bcfe9..7268824 100644 (file)
@@ -2612,31 +2612,68 @@ function update_course($data, $editoroptions = NULL) {
 }
 
 /**
- * Average number of participants
- * @return integer
+ * Calculate the average number of enrolled participants per course.
+ *
+ * This is intended for statistics purposes during the site registration. Only visible courses are taken into account.
+ * Front page enrolments are excluded.
+ *
+ * @param bool $onlyactive Consider only active enrolments in enabled plugins and obey the enrolment time restrictions.
+ * @param int $lastloginsince If specified, count only users who logged in after this timestamp.
+ * @return float
  */
-function average_number_of_participants() {
-    global $DB, $SITE;
+function average_number_of_participants(bool $onlyactive = false, int $lastloginsince = null): float {
+    global $DB;
 
-    //count total of enrolments for visible course (except front page)
-    $sql = 'SELECT COUNT(*) FROM (
-        SELECT DISTINCT ue.userid, e.courseid
-        FROM {user_enrolments} ue, {enrol} e, {course} c
-        WHERE ue.enrolid = e.id
-            AND e.courseid <> :siteid
-            AND c.id = e.courseid
-            AND c.visible = 1) total';
-    $params = array('siteid' => $SITE->id);
-    $enrolmenttotal = $DB->count_records_sql($sql, $params);
+    $params = [
+        'siteid' => SITEID,
+    ];
 
+    $sql = "SELECT DISTINCT ue.userid, e.courseid
+              FROM {user_enrolments} ue
+              JOIN {enrol} e ON e.id = ue.enrolid
+              JOIN {course} c ON c.id = e.courseid ";
 
-    //count total of visible courses (minus front page)
-    $coursetotal = $DB->count_records('course', array('visible' => 1));
-    $coursetotal = $coursetotal - 1 ;
+    if ($onlyactive || $lastloginsince) {
+        $sql .= "JOIN {user} u ON u.id = ue.userid ";
+    }
+
+    $sql .= "WHERE e.courseid <> :siteid
+               AND c.visible = 1 ";
+
+    if ($onlyactive) {
+        $sql .= "AND ue.status = :active
+                 AND e.status = :enabled
+                 AND ue.timestart < :now1
+                 AND (ue.timeend = 0 OR ue.timeend > :now2) ";
+
+        // Same as in the enrollib - the rounding should help caching in the database.
+        $now = round(time(), -2);
+
+        $params += [
+            'active' => ENROL_USER_ACTIVE,
+            'enabled' => ENROL_INSTANCE_ENABLED,
+            'now1' => $now,
+            'now2' => $now,
+        ];
+    }
+
+    if ($lastloginsince) {
+        $sql .= "AND u.lastlogin > :lastlogin ";
+        $params['lastlogin'] = $lastloginsince;
+    }
+
+    $sql = "SELECT COUNT(*)
+              FROM ($sql) total";
+
+    $enrolmenttotal = $DB->count_records_sql($sql, $params);
+
+    // Get the number of visible courses (exclude the front page).
+    $coursetotal = $DB->count_records('course', ['visible' => 1]);
+    $coursetotal = $coursetotal - 1;
 
-    //average of enrolment
     if (empty($coursetotal)) {
         $participantaverage = 0;
+
     } else {
         $participantaverage = $enrolmenttotal / $coursetotal;
     }
index 2fcc9be..fef3933 100644 (file)
@@ -6945,4 +6945,78 @@ class core_course_courselib_testcase extends advanced_testcase {
         // Manager has permissions.
         $this->assertTrue(course_allowed_module($course, 'assign', $manager));
     }
+
+    /**
+     * Test the {@link average_number_of_participants()} function.
+     */
+    public function test_average_number_of_participants() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $generator = $this->getDataGenerator();
+        $now = time();
+
+        // If there are no courses, expect zero number of participants per course.
+        $this->assertEquals(0, average_number_of_participants());
+
+        $c1 = $generator->create_course();
+        $c2 = $generator->create_course();
+
+        // If there are no users, expect zero number of participants per course.
+        $this->assertEquals(0, average_number_of_participants());
+
+        $t1 = $generator->create_user(['lastlogin' => $now]);
+        $s1 = $generator->create_user(['lastlogin' => $now]);
+        $s2 = $generator->create_user(['lastlogin' => $now - WEEKSECS]);
+        $s3 = $generator->create_user(['lastlogin' => $now - WEEKSECS]);
+        $s4 = $generator->create_user(['lastlogin' => $now - YEARSECS]);
+
+        // We have courses, we have users, but no enrolments yet.
+        $this->assertEquals(0, average_number_of_participants());
+
+        // Front page enrolments are ignored.
+        $generator->enrol_user($t1->id, SITEID, 'teacher');
+        $this->assertEquals(0, average_number_of_participants());
+
+        // The teacher enrolled into one of the two courses.
+        $generator->enrol_user($t1->id, $c1->id, 'editingteacher');
+        $this->assertEquals(0.5, average_number_of_participants());
+
+        // The teacher enrolled into both courses.
+        $generator->enrol_user($t1->id, $c2->id, 'editingteacher');
+        $this->assertEquals(1, average_number_of_participants());
+
+        // Student 1 enrolled in the Course 1 only.
+        $generator->enrol_user($s1->id, $c1->id, 'student');
+        $this->assertEquals(1.5, average_number_of_participants());
+
+        // Student 2 enrolled in both courses, but the enrolment in the Course 2 not active yet (enrolment starts in the future).
+        $generator->enrol_user($s2->id, $c1->id, 'student');
+        $generator->enrol_user($s2->id, $c2->id, 'student', 'manual', $now + WEEKSECS);
+        $this->assertEquals(2.5, average_number_of_participants());
+        $this->assertEquals(2, average_number_of_participants(true));
+
+        // Student 3 enrolled in the Course 1, but the enrolment already expired.
+        $generator->enrol_user($s3->id, $c1->id, 'student', 'manual', 0, $now - YEARSECS);
+        $this->assertEquals(3, average_number_of_participants());
+        $this->assertEquals(2, average_number_of_participants(true));
+
+        // Student 4 enrolled in both courses, but the enrolment has been suspended.
+        $generator->enrol_user($s4->id, $c1->id, 'student', 'manual', 0, 0, ENROL_USER_SUSPENDED);
+        $generator->enrol_user($s4->id, $c2->id, 'student', 'manual', $now - DAYSECS, $now + YEARSECS, ENROL_USER_SUSPENDED);
+        $this->assertEquals(4, average_number_of_participants());
+        $this->assertEquals(2, average_number_of_participants(true));
+
+        // Consider only t1 and s1 who logged in recently.
+        $this->assertEquals(1.5, average_number_of_participants(false, $now - DAYSECS));
+
+        // Consider only t1, s1, s2 and s3 who logged in in recent weeks.
+        $this->assertEquals(3, average_number_of_participants(false, $now - 4 * WEEKSECS));
+
+        // Hidden courses are excluded from stats.
+        $DB->set_field('course', 'visible', 0, ['id' => $c1->id]);
+        $this->assertEquals(3, average_number_of_participants());
+        $this->assertEquals(1, average_number_of_participants(true));
+    }
+
 }
index f20802e..e8fa2b9 100644 (file)
@@ -100,7 +100,7 @@ class sync_grades extends \core\task\scheduled_task {
                         }
 
                         // Need a valid context to continue.
-                        if (!$context = \context::instance_by_id($tool->contextid)) {
+                        if (!$context = \context::instance_by_id($tool->contextid, IGNORE_MISSING)) {
                             mtrace("Failed - Invalid contextid '$tool->contextid' for the tool '$tool->id'.");
                             continue;
                         }
index 1ae2fd7..a08ba79 100644 (file)
Binary files a/enrol/manual/amd/build/quickenrolment.min.js and b/enrol/manual/amd/build/quickenrolment.min.js differ
index b408f4b..d8e65c3 100644 (file)
Binary files a/enrol/manual/amd/build/quickenrolment.min.js.map and b/enrol/manual/amd/build/quickenrolment.min.js.map differ
index 980581a..74b9e21 100644 (file)
@@ -28,8 +28,9 @@ define(['core/templates',
          'core/modal_factory',
          'core/modal_events',
          'core/fragment',
+         'core/pending',
        ],
-       function(Template, $, Str, Config, Notification, ModalFactory, ModalEvents, Fragment) {
+       function(Template, $, Str, Config, Notification, ModalFactory, ModalEvents, Fragment, Pending) {
 
     /** @type {Object} The list of selectors for the quick enrolment modal. */
     var SELECTORS = {
@@ -91,6 +92,7 @@ define(['core/templates',
             });
 
             modal.getRoot().on(ModalEvents.shown, function() {
+                var pendingPromise = new Pending('enrol_manual/quickenrolment:initModal:shown');
                 var bodyPromise = this.getBody();
                 bodyPromise.then(function(html) {
                     var stringIndex = $(html).find(SELECTORS.COHORTSELECT).length ? 0 : 1;
@@ -98,7 +100,8 @@ define(['core/templates',
 
                     return;
                 })
-                .fail(Notification.exception);
+                .then(pendingPromise.resolve)
+                .catch(Notification.exception);
 
                 modal.setBody(bodyPromise);
             }.bind(this));
index d0236b7..c9403be 100644 (file)
@@ -184,7 +184,7 @@ class favourite_repository implements favourite_repository_interface {
     /**
      * Return all items matching the supplied criteria (a [key => value,..] list).
      *
-     * @param array $criteria the list of key/value criteria pairs.
+     * @param array $criteria the list of key/value(s) criteria pairs.
      * @param int $limitfrom optional pagination control for returning a subset of records, starting at this point.
      * @param int $limitnum optional pagination control for returning a subset comprising this many records.
      * @return array the list of favourites matching the criteria.
@@ -192,7 +192,22 @@ class favourite_repository implements favourite_repository_interface {
      */
     public function find_by(array $criteria, int $limitfrom = 0, int $limitnum = 0) : array {
         global $DB;
-        $records = $DB->get_records($this->favouritetable, $criteria, '', '*', $limitfrom, $limitnum);
+        $conditions = [];
+        $params = [];
+        foreach ($criteria as $field => $value) {
+            if (is_array($value) && count($value)) {
+                list($insql, $inparams) = $DB->get_in_or_equal($value, SQL_PARAMS_NAMED);
+                $conditions[] = "$field $insql";
+                $params = array_merge($params, $inparams);
+            } else {
+                $conditions[] = "$field = :$field";
+                $params = array_merge($params, [$field => $value]);
+            }
+        }
+
+        $records = $DB->get_records_select($this->favouritetable, implode(' AND ', $conditions), $params,
+            '', '*', $limitfrom, $limitnum);
+
         return $this->get_list_of_favourites_from_records($records);
     }
 
index e569523..0246373 100644 (file)
@@ -110,6 +110,38 @@ class user_favourite_service {
         );
     }
 
+    /**
+     * Find a list of favourites, by multiple types within a component.
+     *
+     * E.g. "Find all favourites in the activity chooser" might result in:
+     * $favcourses = find_all_favourites('core_course', ['contentitem_mod_assign','contentitem_mod_assignment');
+     *
+     * @param string $component the frankenstyle component name.
+     * @param array $itemtypes optional the type of the favourited item.
+     * @param int $limitfrom optional pagination control for returning a subset of records, starting at this point.
+     * @param int $limitnum optional pagination control for returning a subset comprising this many records.
+     * @return array the list of favourites found.
+     * @throws \moodle_exception if the component name is invalid, or if the repository encounters any errors.
+     */
+    public function find_all_favourites(string $component, array $itemtypes = [], int $limitfrom = 0, int $limitnum = 0) : array {
+        if (!in_array($component, \core_component::get_component_names())) {
+            throw new \moodle_exception("Invalid component name '$component'");
+        }
+        $params = [
+            'userid' => $this->userid,
+            'component' => $component,
+        ];
+        if ($itemtypes) {
+            $params['itemtype'] = $itemtypes;
+        }
+
+        return $this->repo->find_by(
+            $params,
+            $limitfrom,
+            $limitnum
+        );
+    }
+
     /**
      * Returns the SQL required to include favourite information for a given component/itemtype combination.
      *
index 7a4fa27..b95451a 100644 (file)
@@ -302,6 +302,16 @@ class favourite_repository_testcase extends advanced_testcase {
         );
         $favouritesrepo->add($favourite);
 
+        // Add another favourite.
+        $favourite = new favourite(
+            'core_course',
+            'course_item',
+            $course1context->instanceid,
+            $course1context->id,
+            $user1context->instanceid
+        );
+        $favouritesrepo->add($favourite);
+
         // From the repo, get the list of favourites for the 'core_course/course' area.
         $userfavourites = $favouritesrepo->find_by(['component' => 'core_course', 'itemtype' => 'course']);
         $this->assertIsArray($userfavourites);
@@ -311,6 +321,16 @@ class favourite_repository_testcase extends advanced_testcase {
         $userfavourites = $favouritesrepo->find_by(['component' => 'core_cannibalism', 'itemtype' => 'course']);
         $this->assertIsArray($userfavourites);
         $this->assertCount(0, $userfavourites);
+
+        // From the repo, get the list of favourites for the 'core_course/course' area when passed as an array.
+        $userfavourites = $favouritesrepo->find_by(['component' => 'core_course', 'itemtype' => ['course']]);
+        $this->assertIsArray($userfavourites);
+        $this->assertCount(1, $userfavourites);
+
+        // From the repo, get the list of favourites for the 'core_course' area given multiple item_types.
+        $userfavourites = $favouritesrepo->find_by(['component' => 'core_course', 'itemtype' => ['course', 'course_item']]);
+        $this->assertIsArray($userfavourites);
+        $this->assertCount(2, $userfavourites);
     }
 
     /**
index bc172af..be372f1 100644 (file)
@@ -85,11 +85,29 @@ class user_favourite_service_testcase extends advanced_testcase {
         $mockrepo->expects($this->any())
             ->method('find_by')
             ->will($this->returnCallback(function(array $criteria, int $limitfrom = 0, int $limitnum = 0) use (&$mockstore) {
+                // Check for single value key pair vs multiple.
+                $multipleconditions = [];
+                foreach ($criteria as $key => $value) {
+                    if (is_array($value)) {
+                        $multipleconditions[$key] = $value;
+                        unset($criteria[$key]);
+                    }
+                }
+
                 // Check the mockstore for all objects with properties matching the key => val pairs in $criteria.
                 foreach ($mockstore as $index => $mockrow) {
                     $mockrowarr = (array)$mockrow;
                     if (array_diff_assoc($criteria, $mockrowarr) == []) {
-                        $returns[$index] = $mockrow;
+                        $found = true;
+                        foreach ($multipleconditions as $key => $value) {
+                            if (!in_array($mockrowarr[$key], $value)) {
+                                $found = false;
+                                break;
+                            }
+                        }
+                        if ($found) {
+                            $returns[$index] = $mockrow;
+                        }
                     }
                 }
                 // Return a subset of the records, according to the paging options, if set.
@@ -235,6 +253,42 @@ class user_favourite_service_testcase extends advanced_testcase {
         $this->assertEquals($fav2->id, $favourites[$fav2->id]->id);
     }
 
+    /**
+     * Test fetching favourites for single user, by area.
+     */
+    public function test_find_all_favourites() {
+        list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
+
+        // Get a user_favourite_service for the user.
+        $repo = $this->get_mock_repository([]); // Mock repository, using the array as a mock DB.
+        $service = new \core_favourites\local\service\user_favourite_service($user1context, $repo);
+
+        // Favourite 2 courses, in separate areas.
+        $fav1 = $service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context);
+        $fav2 = $service->create_favourite('core_course', 'anothertype', $course2context->instanceid, $course2context);
+        $fav3 = $service->create_favourite('core_course', 'yetanothertype', $course2context->instanceid, $course2context);
+
+        // Verify we can get favourites by area.
+        $favourites = $service->find_all_favourites('core_course', ['course']);
+        $this->assertIsArray($favourites);
+        $this->assertCount(1, $favourites); // We only get favourites for the 'core_course/course' area.
+        $this->assertEquals($fav1->id, $favourites[$fav1->id]->id);
+
+        $favourites = $service->find_all_favourites('core_course', ['course', 'anothertype']);
+        $this->assertIsArray($favourites);
+        // We only get favourites for the 'core_course/course' and 'core_course/anothertype area.
+        $this->assertCount(2, $favourites);
+        $this->assertEquals($fav1->id, $favourites[$fav1->id]->id);
+        $this->assertEquals($fav2->id, $favourites[$fav2->id]->id);
+
+        $favourites = $service->find_all_favourites('core_course');
+        $this->assertIsArray($favourites);
+        $this->assertCount(3, $favourites); // We only get favourites for the 'core_cours' area.
+        $this->assertEquals($fav2->id, $favourites[$fav2->id]->id);
+        $this->assertEquals($fav1->id, $favourites[$fav1->id]->id);
+        $this->assertEquals($fav3->id, $favourites[$fav3->id]->id);
+    }
+
     /**
      * Make sure the find_favourites_by_type() method only returns favourites for the scoped user.
      */
index 9e2bae7..8d85c1c 100644 (file)
@@ -23,6 +23,8 @@
 
 defined('MOODLE_INTERNAL') || die;
 
+use core_h5p\local\library\autoloader;
+
 /**
  * Display H5P filter
  *
@@ -74,6 +76,7 @@ class filter_displayh5p extends moodle_text_filter {
         $specialchars = ['?', '&'];
         $escapedspecialchars = ['\?', '&amp;'];
         $h5pcontents = array();
+        $h5plinks = array();
 
         // Check all allowed sources.
         foreach ($allowedsources as $source) {
@@ -82,7 +85,8 @@ class filter_displayh5p extends moodle_text_filter {
 
             if (($source == $localsource)) {
                 $params['tagbegin'] = '<iframe src="'.$CFG->wwwroot.'/h5p/embed.php?url=';
-                $ultimatepattern = '#'.$source.'#';
+                $escapechars = $source;
+                $ultimatepattern = $source;
             } else {
                 if (!stripos($source, 'embed')) {
                     $params['urlmodifier'] = '/embed';
@@ -90,7 +94,7 @@ class filter_displayh5p extends moodle_text_filter {
                 // Convert special chars.
                 $sourceid = str_replace('[id]', '[0-9]+', $source);
                 $escapechars = str_replace($specialchars, $escapedspecialchars, $sourceid);
-                $ultimatepattern = '#(' . $escapechars . ')#';
+                $ultimatepattern = '(' . $escapechars . ')';
             }
 
             // Improve performance creating filterobjects only when needed.
@@ -101,15 +105,39 @@ class filter_displayh5p extends moodle_text_filter {
             $h5pcontenturl = new filterobject($source, null, null, false,
                 false, null, [$this, 'filterobject_prepare_replacement_callback'], $params);
 
-            $h5pcontenturl->workregexp = $ultimatepattern;
+            $h5pcontenturl->workregexp = '#'.$ultimatepattern.'#';
             $h5pcontents[] = $h5pcontenturl;
+
+            // Regex to find h5p extensions in an <a> tag.
+            $linkregexp = '~<a [^>]*href=["\']('.$escapechars.'[^"\']*)["\'][^>]*>([^<]*)</a>~is';
+
+            $h5plinkurl = new filterobject($linkregexp, null, null, false,
+                false, null, [$this, 'filterobject_prepare_replacement_callback'], $params);
+            $h5plinkurl->workregexp = $linkregexp;
+            $h5plinks[] = $h5plinkurl;
         }
 
-        if (empty($h5pcontents)) {
+        if (empty($h5pcontents) && empty($h5links)) {
             // No matches to deal with.
             return $text;
         }
 
+        // Apply filter inside <a> tag href attribute.
+        // We can not use filter_phrase function because it removes all tags and can not be applied in tag attributes.
+        foreach ($h5plinks as $h5plink) {
+            $text = preg_replace_callback($h5plink->workregexp,
+                function ($matches) use ($h5plink) {
+                    if ($matches[1] == $matches[2]) {
+                        filter_prepare_phrase_for_replacement($h5plink);
+
+                        return str_replace('$1', $matches[1], $h5plink->workreplacementphrase);
+                    } else {
+                        return $matches[0];
+                    }
+                }, $text);
+
+        }
+
         $result = filter_phrases($text, $h5pcontents, null, null, false, true);
 
         // Encoding H5P file URLs.
@@ -151,7 +179,7 @@ class filter_displayh5p extends moodle_text_filter {
 
         // We want to request the resizing script only once.
         if (self::$loadresizerjs) {
-            $resizerurl = new moodle_url('/lib/h5p/js/h5p-resizer.js');
+            $resizerurl = autoloader::get_h5p_core_library_url('js/h5p-resizer.js');
             $tagend .= '<script src="' . $resizerurl->out() . '"></script>';
             self::$loadresizerjs = false;
         }
index ff62186..4bad090 100644 (file)
@@ -35,13 +35,25 @@ Feature: Render H5P content using filters
     Then I should see "Lorum ipsum"
 
   @javascript
-  Scenario: Add an external H5P content URL in a link. Shouldn't be rendered.
+  Scenario: Add an external H5P content URL in a link with the URL. Should be rendered.
     Given I log in as "teacher1"
     And I am on "Course 1" course homepage
     And I follow "PageName1"
     And I navigate to "Edit settings" in current page administration
 #   This content won't be displayed, so this scenario shouldn't be labeled as external.
-    And I set the field "Page content" to "<a href='https://moodle.h5p.com/content/1290772960722742119/embed'>Go to https://moodle.h5p.com/content/1290772960722742119/embed</a>"
+    And I set the field "Page content" to "<a href='https://moodle.h5p.com/content/1290772960722742119/embed'>https://moodle.h5p.com/content/1290772960722742119/embed</a>"
+    When I click on "Save and display" "button"
+    And I wait until the page is ready
+    And I switch to "h5p-iframe" class iframe
+    Then I should see "Lorum ipsum"
+
+  Scenario: Add an external H5P content URL in a link with text. Shouldn't be rendered.
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I follow "PageName1"
+    And I navigate to "Edit settings" in current page administration
+#   This content won't be displayed, so this scenario shouldn't be labeled as external.
+    And I set the field "Page content" to "<a href='https://moodle.h5p.com/content/1290772960722742119/embed'>Here you are the content</a>"
     When I click on "Save and display" "button"
     And I wait until the page is ready
     Then ".h5p-iframe" "css_element" should not exist
index 8ace093..467118c 100644 (file)
@@ -42,7 +42,7 @@ class filter_displayh5p_testcase extends advanced_testcase {
         $this->resetAfterTest(true);
 
         set_config('allowedsources',
-            "https://h5p.org/h5p/embed/[id]\nhttps://moodle.h5p.com/content/[id]/embed\nhttps://moodle.h5p.com/content/[id]
+            "https://moodle.h5p.com/content/[id]/embed\nhttps://moodle.h5p.com/content/[id]
                 \nhttps://generic.wordpress.soton.ac.uk/altc/wp-admin/admin-ajax.php?action=h5p_embed&id=[id]",
             'filter_displayh5p');
     }
@@ -74,16 +74,20 @@ class filter_displayh5p_testcase extends advanced_testcase {
         return [
             ["http:://example.com", "#http:://example.com#"],
             ["http://google.es/h5p/embed/3425234", "#http://google.es/h5p/embed/3425234#"],
-            ["https://h5p.org/h5p/embed/547225", "#<iframe src=\"https://h5p.org/h5p/embed/547225\"[^>]+?>#"],
             ["https://moodle.h5p.com/content/1290729733828858779/embed", "#<iframe src=\"https://moodle.h5p.com/content/1290729733828858779/embed\"[^>]+?>#"],
             ["https://moodle.h5p.com/content/1290729733828858779", "#<iframe src=\"https://moodle.h5p.com/content/1290729733828858779/embed\"[^>]+?>#"],
-            ["<a href=\"https://h5p.org/h5p/embed/547225\">link</a>",  "#^((?!iframe).)*$#"],
-            ["this is a text with an h5p url https://h5p.org/h5p/embed/547225 inside",
-                    "#this is a text with an h5p url <iframe src=\"https://h5p.org/h5p/embed/547225\"(.|\n)*> inside#"],
+            ["<a href=\"https://moodle.h5p.com/content/1290848995208939539/embed\">https://moodle.h5p.com/content/1290848995208939539/embed</a>",
+                "#<iframe src=\"https://moodle.h5p.com/content/1290848995208939539/embed\"[^>]+?>#"],
+            ["<a href=\"https://moodle.org\">https://moodle.h5p.com/content/1290848995208939539/embed</a>",
+                "#^((?!iframe).)*$#"],
+            ["<a href=\"https://moodle.h5p.com/content/1290848995208939539/embed\">link</a>",  "#^((?!iframe).)*$#"],
+            ["this is a text with an h5p url https://moodle.h5p.com/content/1290848995208939539/embed inside",
+                    "#this is a text with an h5p url <iframe src=\"https://moodle.h5p.com/content/1290848995208939539/embed\"(.|\n)*> inside#"],
             ["https://generic.wordpress.soton.ac.uk/altc/wp-admin/admin-ajax.php?action=h5p_embed&amp;id=13",
                     "#<iframe src=\"https://generic.wordpress.soton.ac.uk/altc/wp-admin/admin-ajax.php\?action=h5p_embed\&amp\;id=13\"[^>]+?>#"],
-            ["https://h5p.org/h5p/embed/547225 another content in the same page https://moodle.h5p.com/content/1290729733828858779/embed",
-                    "#<iframe src=\"https://h5p.org/h5p/embed/547225\"[^>]+?>((?!<iframe).)*<iframe src=\"https://moodle.h5p.com/content/1290729733828858779/embed\"[^>]+?>#"],
+            ["https://moodle.h5p.com/content/1290848995208939539/embed another content in the same page https://moodle.h5p.com/content/1290729733828858779/embed",
+                    "#<iframe src=\"https://moodle.h5p.com/content/1290848995208939539/embed\"[^>]+?>((?!<iframe).)*".
+                    "<iframe src=\"https://moodle.h5p.com/content/1290729733828858779/embed\"[^>]+?>#"],
             [$CFG->wwwroot."/pluginfile.php/5/user/private/interactive-video.h5p?export=1&embed=1",
                     "#<iframe src=\"{$CFG->wwwroot}/h5p/embed.php\?url=".rawurlencode("{$CFG->wwwroot}/pluginfile.php/5/user/private/interactive-video.h5p").
                     "&export=1&embed=1\"[^>]*?></iframe>#"],
diff --git a/grade/report/history/db/upgrade.php b/grade/report/history/db/upgrade.php
new file mode 100644 (file)
index 0000000..d2c6135
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Grade overview report upgrade steps.
+ *
+ * @package    gradereport_history
+ * @copyright  2020 Michael Hawkins <michaelh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Function to upgrade grade history report.
+ *
+ * @param int $oldversion the version we are upgrading from
+ * @return bool result
+ */
+function xmldb_gradereport_history_upgrade($oldversion) {
+
+    if ($oldversion < 2019111801) {
+        $perpageconfig = get_config('moodle', 'grade_report_historyperpage');
+
+        // For existing installations with a non-integer 'per page' config, update the value to the default.
+        if (!empty($perpageconfig) && filter_var($perpageconfig, FILTER_VALIDATE_INT) === false) {
+            set_config('grade_report_historyperpage', 50);
+        }
+
+        upgrade_plugin_savepoint(true, 2019111801, 'gradereport', 'history');
+    }
+
+    return true;
+}
index c841c80..3653f07 100644 (file)
@@ -30,7 +30,8 @@ if ($ADMIN->fulltree) {
     $settings->add(new admin_setting_configtext('grade_report_historyperpage',
         new lang_string('historyperpage', 'gradereport_history'),
         new lang_string('historyperpage_help', 'gradereport_history'),
-        50
+        50,
+        PARAM_INT
     ));
 
 }
index b19225b..885b84a 100644 (file)
@@ -25,6 +25,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2019111800;
+$plugin->version   = 2019111801;
 $plugin->requires  = 2019111200;
 $plugin->component = 'gradereport_history';
diff --git a/h5p/classes/autoloader.php b/h5p/classes/autoloader.php
deleted file mode 100644 (file)
index 3e8daea..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * H5P Autoloader.
- *
- * @package    core_h5p
- * @copyright  2019 Mihail Geshoski <mihail@moodle.com>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace core_h5p;
-
-defined('MOODLE_INTERNAL') || die();
-
-/**
- * H5P Autoloader.
- *
- * @package    core_h5p
- * @copyright  2019 Mihail Geshoski <mihail@moodle.com>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class autoloader {
-    public static function register(): void {
-        spl_autoload_register([self::class, 'autoload']);
-    }
-
-    public static function autoload($classname): void {
-        global $CFG;
-
-        $classes = [
-            'H5PCore' => '/lib/h5p/h5p.classes.php',
-            'H5PHubEndpoints' => '/lib/h5p/h5p.classes.php',
-            'H5PFrameworkInterface' => '/lib/h5p/h5p.classes.php',
-            'H5PContentValidator' => 'lib/h5p/h5p.classes.php',
-            'H5PValidator' => '/lib/h5p/h5p.classes.php',
-            'H5PStorage' => '/lib/h5p/h5p.classes.php',
-            'H5PDevelopment' => '/lib/h5p/h5p-development.class.php',
-            'H5PFileStorage' => '/lib/h5p/h5p-file-storage.interface.php',
-            'H5PMetadata' => '/lib/h5p/h5p-metadata.class.php',
-        ];
-
-        if (isset($classes[$classname])) {
-            require_once("{$CFG->dirroot}{$classes[$classname]}");
-        }
-    }
-}
index 1458372..3c15d85 100644 (file)
@@ -32,6 +32,7 @@ use H5PCore;
 use H5PFrameworkInterface;
 use stdClass;
 use moodle_url;
+use core_h5p\local\library\autoloader;
 
 /**
  * H5P core class, containing functions and storage shared by the other H5P classes.
@@ -138,20 +139,24 @@ class core extends \H5PCore {
     }
 
     /**
-     * Get core JavaScript files.
+     * Get the list of JS scripts to include on the page.
      *
      * @return array The array containg urls of the core JavaScript files
      */
     public static function get_scripts(): array {
-        global $CFG;
-        $cachebuster = '?ver='.$CFG->jsrev;
-        $liburl = $CFG->wwwroot . '/lib/h5p/';
-        $urls = [];
+        global $PAGE;
 
+        $factory = new factory();
+        $jsrev = $PAGE->requires->get_jsrev();
+        $urls = [];
         foreach (self::$scripts as $script) {
-            $urls[] = new moodle_url($liburl . $script . $cachebuster);
+            $urls[] = autoloader::get_h5p_core_library_url($script, [
+                'ver' => $jsrev,
+            ]);
         }
-        $urls[] = new moodle_url("/h5p/js/h5p_overrides.js");
+        $urls[] = new moodle_url("/h5p/js/h5p_overrides.js", [
+            'ver' => $jsrev,
+        ]);
 
         return $urls;
     }
index 52c8cc0..f115179 100644 (file)
@@ -27,11 +27,12 @@ namespace core_h5p;
 
 defined('MOODLE_INTERNAL') || die();
 
-use \core_h5p\framework as framework;
-use \core_h5p\core as core;
-use \H5PStorage as storage;
-use \H5PValidator as validator;
-use \H5PContentValidator as content_validator;
+use core_h5p\local\library\autoloader;
+use core_h5p\framework;
+use core_h5p\core;
+use H5PStorage as storage;
+use H5PValidator as validator;
+use H5PContentValidator as content_validator;
 
 /**
  * H5P factory class.
@@ -43,6 +44,9 @@ use \H5PContentValidator as content_validator;
  */
 class factory {
 
+    /** @var \core_h5p\local\library\autoloader The autoloader */
+    protected $autoloader;
+
     /** @var \core_h5p\core The Moodle H5PCore implementation */
     protected $core;
 
@@ -63,9 +67,19 @@ class factory {
      */
     public function __construct() {
         // Loading classes we need from H5P third party library.
+        $this->autoloader = new autoloader();
         autoloader::register();
     }
 
+    /**
+     * Returns an instance of the \core_h5p\local\library\autoloader class.
+     *
+     * @return \core_h5p\local\library\autoloader
+     */
+    public function get_autoloader(): autoloader {
+        return $this->autoloader;
+    }
+
     /**
      * Returns an instance of the \core_h5p\framework class.
      *
diff --git a/h5p/classes/local/library/autoloader.php b/h5p/classes/local/library/autoloader.php
new file mode 100644 (file)
index 0000000..76ef5c9
--- /dev/null
@@ -0,0 +1,124 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * H5P autoloader management class.
+ *
+ * @package    core_h5p
+ * @copyright  2019 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_h5p\local\library;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * H5P autoloader management class.
+ *
+ * @package    core_h5p
+ * @copyright  2019 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class autoloader {
+
+    /**
+     * Returns the list of plugins that can work as H5P library handlers (have class PLUGINNAME\local\library\handler)
+     * @return array with the format: pluginname => class
+     */
+    public static function get_all_handlers(): array {
+        $handlers = [];
+        foreach (\core_component::get_plugin_types() as $ptype => $unused) {
+            $plugins = \core_component::get_plugin_list_with_class($ptype, 'local\library\handler') +
+                \core_component::get_plugin_list_with_class($ptype, 'local_library_handler');
+            // Allow plugins to have the class either with namespace or without (useful for unittest).
+            foreach ($plugins as $pname => $class) {
+                $handlers[$pname] = $class;
+            }
+        }
+
+        return $handlers;
+    }
+
+    /**
+     * Returns the default H5P library handler.
+     * @return string|null H5P library handler class
+     */
+    public static function get_default_handler(): ?string {
+        $default = null;
+        $handlers = self::get_all_handlers();
+        if (!empty($handlers)) {
+            // The default handler will be the first in the list.
+            $keys = array_keys($handlers);
+            $default = array_shift($keys);
+        }
+
+        return $default;
+    }
+
+    /**
+     * Returns the current H5P library handler class.
+     *
+     * @return string H5P library handler class
+     * @throws \moodle_exception
+     */
+    public static function get_handler_classname(): string {
+        global $CFG;
+
+        $handlers = self::get_all_handlers();
+        if (!empty($CFG->h5plibraryhandler)) {
+            if (isset($handlers[$CFG->h5plibraryhandler])) {
+                return $handlers[$CFG->h5plibraryhandler];
+            }
+        }
+
+        // If no handler has been defined or it doesn't exist, return the default one.
+        $defaulthandler = self::get_default_handler();
+        if (empty($defaulthandler)) {
+            // If there is no default handler, throw an exception.
+            throw new \moodle_exception('noh5plibhandlerdefined', 'core_h5p');
+        }
+
+        return $defaulthandler;
+    }
+
+    /**
+     * Get the current version of the H5P core library.
+     *
+     * @return string
+     */
+    public static function get_h5p_version(): string {
+        return component_class_callback(self::get_handler_classname(), 'get_h5p_version', []);
+    }
+
+    /**
+     * Get a URL for the current H5P Core Library.
+     *
+     * @param string $filepath The path within the h5p root
+     * @param array $params these params override current params or add new
+     * @return null|moodle_url
+     */
+    public static function get_h5p_core_library_url(?string $filepath = null, ?array $params = null): ?\moodle_url {
+        return component_class_callback(self::get_handler_classname(), 'get_h5p_core_library_url', [$filepath, $params]);
+    }
+
+    /**
+     * Register the H5P autoloader.
+     */
+    public static function register(): void {
+        component_class_callback(self::get_handler_classname(), 'register', []);
+    }
+}
diff --git a/h5p/classes/local/library/handler.php b/h5p/classes/local/library/handler.php
new file mode 100644 (file)
index 0000000..9bb4864
--- /dev/null
@@ -0,0 +1,118 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Base class for library handlers.
+ *
+ * @package    core_h5p
+ * @copyright  2019 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_h5p\local\library;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Base class for library handlers.
+ *
+ * If a new H5P libraries handler plugin has to be created, it has to define class
+ * PLUGINNAME\local\library\handler that extends \core_h5p\local\library\handler.
+ *
+ * @package    core_h5p
+ * @copyright  2019 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class handler {
+
+    /**
+     * Get the current version of the H5P core library.
+     *
+     * @return string
+     */
+    abstract public static function get_h5p_version(): string;
+
+    /**
+     * Get the base path for the H5P Libraries.
+     *
+     * @return null|string
+     */
+    public static function get_h5p_library_base(): ?string {
+        $h5pversion = static::get_h5p_version();
+        return "/h5p/h5plib/v{$h5pversion}/joubel";
+    }
+
+    /**
+     * Get the base path for the current H5P Core Library.
+     *
+     * @param string $filepath The path within the H5P root
+     * @return null|string
+     */
+    public static function get_h5p_core_library_base(?string $filepath = null): ?string {
+        return static::get_h5p_library_base() . "/core/{$filepath}";
+    }
+
+    /**
+     * Register the H5P autoloader.
+     */
+    public static function register(): void {
+        spl_autoload_register([static::class, 'autoload']);
+    }
+
+    /**
+     * SPL Autoloading function for H5P.
+     *
+     * @param string $classname The name of the class to load
+     */
+    public static function autoload($classname): void {
+        global $CFG;
+
+        $classes = static::get_class_list();
+
+        if (isset($classes[$classname])) {
+            require_once($CFG->dirroot . static::get_h5p_core_library_base($classes[$classname]));
+        }
+    }
+
+    /**
+     * Get a URL for the current H5P Core Library.
+     *
+     * @param string $filepath The path within the h5p root
+     * @param array $params these params override current params or add new
+     * @return null|moodle_url
+     */
+    public static function get_h5p_core_library_url(?string $filepath = null, ?array $params = null): ?\moodle_url {
+        return new \moodle_url(static::get_h5p_core_library_base($filepath), $params);
+    }
+
+    /**
+     * Return the list of classes with their location within the joubel directory.
+     *
+     * @return array
+     */
+    protected static function get_class_list(): array {
+        return [
+            'H5PCore' => 'h5p.classes.php',
+            'H5PFrameworkInterface' => 'h5p.classes.php',
+            'H5PContentValidator' => 'h5p.classes.php',
+            'H5PValidator' => 'h5p.classes.php',
+            'H5PStorage' => 'h5p.classes.php',
+            'H5PDevelopment' => 'h5p-development.class.php',
+            'H5PFileStorage' => 'h5p-file-storage.interface.php',
+            'H5PMetadata' => 'h5p-metadata.class.php',
+        ];
+    }
+}
index f9d32ab..b6c355e 100644 (file)
@@ -26,6 +26,8 @@ namespace core_h5p;
 
 defined('MOODLE_INTERNAL') || die();
 
+use core_h5p\local\library\autoloader;
+
 /**
  * H5P player class, for displaying any local H5P content.
  *
@@ -598,13 +600,15 @@ class player {
         $cachebuster = $this->get_cache_buster();
 
         // Use relative URL to support both http and https.
-        $liburl = $CFG->wwwroot . '/lib/h5p/';
+        $liburl = autoloader::get_h5p_core_library_url()->out();
         $relpath = '/' . preg_replace('/^[^:]+:\/\/[^\/]+\//', '', $liburl);
 
         // Add core stylesheets.
         foreach (core::$styles as $style) {
             $settings['core']['styles'][] = $relpath . $style . $cachebuster;
-            $this->cssrequires[] = new \moodle_url($liburl . $style . $cachebuster);
+            $this->cssrequires[] = autoloader::get_h5p_core_library_url($style, [
+                'ver' => $cachebuster,
+            ]);
         }
         // Add core JavaScript.
         foreach (core::get_scripts() as $script) {
@@ -687,7 +691,7 @@ class player {
             'crossorigin' => null,
             'libraryConfig' => $this->core->h5pF->getLibraryConfig(),
             'pluginCacheBuster' => $this->get_cache_buster(),
-            'libraryUrl' => $basepath . 'lib/h5p/js',
+            'libraryUrl' => autoloader::get_h5p_core_library_url('js'),
             'moodleLibraryPaths' => $this->core->get_dependency_roots($this->h5pid),
         );
 
@@ -715,7 +719,7 @@ class player {
         global $OUTPUT;
 
         $template = new \stdClass();
-        $template->resizeurl = new \moodle_url('/lib/h5p/js/h5p-resizer.js');
+        $template->resizeurl = autoloader::get_h5p_core_library_url('js/h5p-resizer.js');
 
         return $OUTPUT->render_from_template('core_h5p/h5presize', $template);
     }
diff --git a/h5p/h5plib/v124/classes/local/library/handler.php b/h5p/h5plib/v124/classes/local/library/handler.php
new file mode 100644 (file)
index 0000000..f9a7851
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Handler for the version 1.24 of the H5P library.
+ *
+ * @package    h5plib_v124
+ * @copyright  2019 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace h5plib_v124\local\library;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Handler for the version 1.24 of the H5P library.
+ *
+ * @package    h5plib_v124
+ * @copyright  2019 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class handler extends \core_h5p\local\library\handler {
+
+    /**
+     * Get the current version of the H5P core library.
+     *
+     * @return string
+     */
+    public static function get_h5p_version(): string {
+        return '124';
+    }
+}
diff --git a/h5p/h5plib/v124/classes/privacy/provider.php b/h5p/h5plib/v124/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..b6c8242
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Privacy provider implementation for the version 1.24 of the H5P library.
+ *
+ * @package    h5plib_v124
+ * @copyright  2020 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace h5plib_v124\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy provider implementation for the version 1.24 of the H5P library.
+ *
+ * @copyright  2020 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
diff --git a/h5p/h5plib/v124/lang/en/h5plib_v124.php b/h5p/h5plib/v124/lang/en/h5plib_v124.php
new file mode 100644 (file)
index 0000000..ca5ea1a
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Strings for component 'h5p_v124'
+ *
+ * @package    h5plib_v124
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+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.';
diff --git a/h5p/h5plib/v124/thirdpartylibs.xml b/h5p/h5plib/v124/thirdpartylibs.xml
new file mode 100644 (file)
index 0000000..000c977
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<libraries>
+  <library>
+    <location>joubel/core</location>
+    <name>h5p-php-library</name>
+    <license>GPL-3.0</license>
+    <version>1.24</version>
+  </library>
+</libraries>
diff --git a/h5p/h5plib/v124/version.php b/h5p/h5plib/v124/version.php
new file mode 100644 (file)
index 0000000..1e450cc
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Version information.
+ *
+ * @package   h5plib_v124
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version   = 2019121300;       // The current module version (Date: YYYYMMDDXX)
+$plugin->requires  = 2019051100;       // Requires this Moodle version
+$plugin->component = 'h5plib_v124';    // Full name of the plugin (used for diagnostics).
index deccba4..b640925 100644 (file)
@@ -23,6 +23,8 @@
  */
 defined('MOODLE_INTERNAL') || die();
 
+use core_h5p\local\library\autoloader;
+
 /**
  * Serve the files from the core_h5p file areas.
  *
@@ -44,7 +46,7 @@ function core_h5p_pluginfile($course, $cm, $context, string $filearea, array $ar
     global $DB;
 
     // Require classes from H5P third party library
-    \core_h5p\autoloader::register();
+    autoloader::register();
 
     $filesettingsset = false;
 
index 5159b7e..c2c65f5 100644 (file)
@@ -26,7 +26,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-use core_h5p\autoloader;
+use core_h5p\local\library\autoloader;
 
 /**
  * Tests for h5p deleted event.
index 451ba4e..75a7671 100644 (file)
@@ -26,7 +26,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-use core_h5p\autoloader;
+use core_h5p\local\library\autoloader;
 
 /**
  * Tests for h5p viewed event.
index b9ae3ca..737c7cc 100644 (file)
@@ -33,7 +33,7 @@ require_once($CFG->dirroot . '/webservice/tests/helpers.php');
 
 use core_h5p\external;
 use core_h5p\file_storage;
-use core_h5p\autoloader;
+use core_h5p\local\library\autoloader;
 
 /**
  * Core h5p external functions tests
index 5f4654e..5d7ee9f 100644 (file)
@@ -23,7 +23,7 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-use core_h5p\autoloader;
+use core_h5p\local\library\autoloader;
 use core_h5p\core;
 
 defined('MOODLE_INTERNAL') || die();
index 118484c..60fd74f 100644 (file)
@@ -25,7 +25,7 @@
 
 namespace core_h5p;
 
-use core_h5p\autoloader;
+use core_h5p\local\library\autoloader;
 
 defined('MOODLE_INTERNAL') || die();
 
index 648b64e..a814c44 100644 (file)
@@ -25,6 +25,8 @@
 
 namespace core_h5p;
 
+use core_h5p\local\library\autoloader;
+
 defined('MOODLE_INTERNAL') || die();
 
 /**
index d6199e4..fa83984 100644 (file)
@@ -26,7 +26,7 @@
 namespace core_h5p\local\tests;
 
 use core_h5p\file_storage;
-use core_h5p\autoloader;
+use core_h5p\local\library\autoloader;
 use core_h5p\helper;
 use file_archive;
 use zip_archive;
@@ -617,4 +617,4 @@ class h5p_file_storage_testcase extends \advanced_testcase {
             ],
         ];
     }
-}
\ No newline at end of file
+}
diff --git a/h5p/upgrade.txt b/h5p/upgrade.txt
new file mode 100644 (file)
index 0000000..e5060d4
--- /dev/null
@@ -0,0 +1,8 @@
+This files describes API changes in core libraries and APIs,
+information provided here is intended especially for developers.
+
+=== 3.9 ===
+* A new plugintype has been created, h5plib, for having installed more
+than one H5P library version.
+* H5P third-party libraries have been moved from /lib/h5p to h5p/h5plib/v124,
+as an h5plib plugintype.
index 355f523..c1a4494 100644 (file)
@@ -91,11 +91,14 @@ $string['h5p'] = 'H5P';
 $string['h5ptitle'] = 'Visit h5p.org to check out more content.';
 $string['h5pfilenotfound'] = 'H5P file not found';
 $string['h5pinvalidurl'] = 'Invalid H5P content URL.';
+$string['h5plibraryhandler'] = 'H5P framework handler';
+$string['h5plibraryhandler_help'] = 'The H5P framework used to display any H5P content.';
 $string['h5pprivatefile'] = 'This H5P content can\'t be displayed because you don\'t have access to the .h5p file.';
 $string['h5pmanage'] = 'Manage H5P content types';
 $string['h5poverview'] = 'H5P overview';
 $string['h5ppackage'] = 'H5P content type';
 $string['h5ppackage_help'] = 'An H5P content type is a file with an H5P or ZIP extension containing all libraries required to display the content.';
+$string['h5psettings'] = 'H5P settings';
 $string['hideadvanced'] = 'Hide advanced';
 $string['installedcontentlibraries'] = 'Installed H5P libraries';
 $string['installedcontenttypes'] = 'Installed H5P content types';
@@ -145,8 +148,9 @@ $string['missingmbstring'] = 'The mbstring PHP extension is not loaded. It is re
 $string['missinguploadpermissions'] = 'Note that the libraries may exist in the file you uploaded, but you\'re not allowed to upload new libraries. Please contact your administrator.';
 $string['nocopyright'] = 'No copyright information available for this content.';
 $string['noextension'] = 'The file you uploaded is not a valid HTML5 Package. (It doesn\'t have the .h5p file extension.)';
-$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['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['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 3df03ba..f931fd9 100644 (file)
@@ -22,6 +22,8 @@
  * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
+$string['activeparticipantnumberaverage'] = 'Average number of recently active participants ({$a})';
+$string['activeusersnumber'] = 'Number of recently active users ({$a})';
 $string['analyticsactions'] = 'Number of actions taken on generated predictions ({$a})';
 $string['analyticsactionsnotuseful'] = 'Number of actions marking a prediction as not useful ({$a})';
 $string['analyticsenabledmodels'] = 'Number of enabled prediction models ({$a})';
index 94c5616..caaf671 100644 (file)
@@ -149,6 +149,8 @@ $string['type_gradereport'] = 'Gradebook report';
 $string['type_gradereport_plural'] = 'Gradebook reports';
 $string['type_gradingform'] = 'Advanced grading method';
 $string['type_gradingform_plural'] = 'Advanced grading methods';
+$string['type_h5plib'] = 'H5P framework';
+$string['type_h5plib_plural'] = 'H5P frameworks';
 $string['type_mlbackend'] = 'Machine learning backend';
 $string['type_mlbackend_plural'] = 'Machine learning backends';
 $string['type_local'] = 'Local plugin';
index 854486a..3f0dc85 100644 (file)
@@ -11208,3 +11208,44 @@ class admin_setting_configthemepreset extends admin_setting_configselect {
         return true;
     }
 }
+
+/**
+ * Selection of plugins that can work as H5P libraries handlers
+ *
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2020 Sara Arjona <sara@moodle.com>
+ */
+class admin_settings_h5plib_handler_select extends admin_setting_configselect {
+
+    /**
+     * Constructor
+     * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting'
+     *        for ones in config_plugins.
+     * @param string $visiblename localised
+     * @param string $description long localised info
+     * @param string $defaultsetting
+     */
+    public function __construct($name, $visiblename, $description, $defaultsetting = '') {
+        parent::__construct($name, $visiblename, $description, $defaultsetting, null);
+    }
+
+    /**
+     * Lazy-load the available choices for the select box
+     */
+    public function load_choices() {
+        if (during_initial_install()) {
+            return false;
+        }
+        if (is_array($this->choices)) {
+            return true;
+        }
+
+        $this->choices = \core_h5p\local\library\autoloader::get_all_handlers();
+        foreach ($this->choices as $name => $class) {
+            $this->choices[$name] = new lang_string('sitepolicyhandlerplugin', 'core_admin',
+                ['name' => new lang_string('pluginname', $name), 'component' => $name]);
+        }
+
+        return true;
+    }
+}
index e54631a..3269e6e 100644 (file)
Binary files a/lib/amd/build/modal.min.js and b/lib/amd/build/modal.min.js differ
index 0a501e5..5b24706 100644 (file)
Binary files a/lib/amd/build/modal.min.js.map and b/lib/amd/build/modal.min.js.map differ
index f38a752..2fa4d54 100644 (file)
Binary files a/lib/amd/build/modal_factory.min.js and b/lib/amd/build/modal_factory.min.js differ
index 32561b5..ff93873 100644 (file)
Binary files a/lib/amd/build/modal_factory.min.js.map and b/lib/amd/build/modal_factory.min.js.map differ
index 16469ae..aeefaed 100644 (file)
@@ -32,7 +32,8 @@ define([
     'core/event',
     'core/modal_events',
     'core/local/aria/focuslock',
-], function($, Templates, Notification, KeyCodes, CustomEvents, ModalBackdrop, Event, ModalEvents, FocusLock) {
+    'core/pending',
+], function($, Templates, Notification, KeyCodes, CustomEvents, ModalBackdrop, Event, ModalEvents, FocusLock, Pending) {
 
     var SELECTORS = {
         CONTAINER: '[data-region="modal-container"]',
@@ -615,6 +616,8 @@ define([
             return;
         }
 
+        var pendingPromise = new Pending('core/modal:show');
+
         if (this.hasFooterContent()) {
             this.showFooter();
         } else {
@@ -625,7 +628,8 @@ define([
             this.attachToDOM();
         }
 
-        this.getBackdrop().done(function(backdrop) {
+        this.getBackdrop()
+        .then(function(backdrop) {
             var currentIndex = this.calculateZIndex();
             var newIndex = currentIndex + 2;
             var newBackdropIndex = newIndex - 1;
@@ -638,7 +642,10 @@ define([
             this.getModal().focus();
             $('body').addClass('modal-open');
             this.root.trigger(ModalEvents.shown, this);
-        }.bind(this));
+
+            return;
+        }.bind(this))
+        .then(pendingPromise.resolve);
     };
 
     /**
index 4e9381e..2b304bc 100644 (file)
  */
 define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal',
         'core/modal_save_cancel', 'core/modal_cancel',
-        'core/templates', 'core/notification', 'core/custom_interaction_events'],
+        'core/templates', 'core/notification', 'core/custom_interaction_events',
+        'core/pending'],
     function($, ModalEvents, ModalRegistry, Modal, ModalSaveCancel,
-        ModalCancel, Templates, Notification, CustomEvents) {
+        ModalCancel, Templates, Notification, CustomEvents, Pending) {
 
     // The templates for each type of modal.
     var TEMPLATES = {
@@ -64,6 +65,7 @@ define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal',
         var hasPreShowCallback = (typeof modalConfig.preShowCallback == 'function');
         // Function to handle the trigger element being activated.
         var triggeredCallback = function(e, data) {
+            var pendingPromise = new Pending('core/modal_factory:setUpTrigger:triggeredCallback');
             actualTriggerElement = $(e.currentTarget);
             modalPromise.then(function(modal) {
                 if (hasPreShowCallback) {
@@ -75,7 +77,8 @@ define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal',
                 modal.show();
 
                 return modal;
-            });
+            })
+            .then(pendingPromise.resolve);
             data.originalEvent.preventDefault();
         };
 
index 5338be1..267406b 100644 (file)
@@ -34,6 +34,7 @@ require_once(__DIR__ . '/../../filelib.php');
 require_once(__DIR__ . '/../../clilib.php');
 
 use Behat\Mink\Session;
+use Behat\Mink\Exception\ExpectationException;
 
 /**
  * Init/reset utilities for Behat database and dataroot
index cd716ef..1fb94e3 100644 (file)
@@ -58,6 +58,8 @@ class registration {
         ],
         // Analytics stats added in Moodle 3.7.
         2019022200 => ['analyticsenabledmodels', 'analyticspredictions', 'analyticsactions', 'analyticsactionsnotuseful'],
+        // Active users stats added in Moodle 3.9.
+        2020022600 => ['activeusers', 'activeparticipantnumberaverage'],
     ];
 
     /** @var Site privacy: not displayed */
@@ -168,6 +170,7 @@ class registration {
         // Statistical data.
         $siteinfo['courses'] = $DB->count_records('course') - 1;
         $siteinfo['users'] = $DB->count_records('user', array('deleted' => 0));
+        $siteinfo['activeusers'] = $DB->count_records_select('user', 'deleted = ? AND lastlogin > ?', [0, time() - DAYSECS * 30]);
         $siteinfo['enrolments'] = $DB->count_records('role_assignments');
         $siteinfo['posts'] = $DB->count_records('forum_posts');
         $siteinfo['questions'] = $DB->count_records('question');
@@ -175,6 +178,7 @@ class registration {
         $siteinfo['badges'] = $DB->count_records_select('badge', 'status <> ' . BADGE_STATUS_ARCHIVED);
         $siteinfo['issuedbadges'] = $DB->count_records('badge_issued');
         $siteinfo['participantnumberaverage'] = average_number_of_participants();
+        $siteinfo['activeparticipantnumberaverage'] = average_number_of_participants(true, time() - DAYSECS * 30);
         $siteinfo['modulenumberaverage'] = average_number_of_courses_modules();
 
         // Version and url.
@@ -229,6 +233,7 @@ class registration {
             'moodlerelease' => get_string('sitereleasenum', 'hub', $moodlerelease),
             'courses' => get_string('coursesnumber', 'hub', $siteinfo['courses']),
             'users' => get_string('usersnumber', 'hub', $siteinfo['users']),
+            'activeusers' => get_string('activeusersnumber', 'hub', $siteinfo['activeusers']),
             'enrolments' => get_string('roleassignmentsnumber', 'hub', $siteinfo['enrolments']),
             'posts' => get_string('postsnumber', 'hub', $siteinfo['posts']),
             'questions' => get_string('questionsnumber', 'hub', $siteinfo['questions']),
@@ -237,6 +242,8 @@ class registration {
             'issuedbadges' => get_string('issuedbadgesnumber', 'hub', $siteinfo['issuedbadges']),
             'participantnumberaverage' => get_string('participantnumberaverage', 'hub',
                 format_float($siteinfo['participantnumberaverage'], 2)),
+            'activeparticipantnumberaverage' => get_string('activeparticipantnumberaverage', 'hub',
+                format_float($siteinfo['activeparticipantnumberaverage'], 2)),
             'modulenumberaverage' => get_string('modulenumberaverage', 'hub',
                 format_float($siteinfo['modulenumberaverage'], 2)),
             'mobileservicesenabled' => get_string('mobileservicesenabled', 'hub', $mobileservicesenabled),
index 7ac652d..86b2751 100644 (file)
@@ -1881,6 +1881,10 @@ class core_plugin_manager {
                 'rubric', 'guide'
             ),
 
+            'h5plib' => array(
+                'v124'
+            ),
+
             'local' => array(
             ),
 
diff --git a/lib/classes/plugininfo/h5plib.php b/lib/classes/plugininfo/h5plib.php
new file mode 100644 (file)
index 0000000..02765ee
--- /dev/null
@@ -0,0 +1,88 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Defines classes used for plugin info.
+ *
+ * @package    core
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core\plugininfo;
+
+use moodle_url;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class for H5P libraries.
+ */
+class h5plib extends base {
+
+    /**
+     * Defines if there should be a way to uninstall the plugin via the administration UI.
+     *
+     * @return bool
+     */
+    public function is_uninstall_allowed(): bool {
+        return true;
+    }
+
+    /**
+     * H5P versions cannot be disabled.
+     *
+     * @return boolean
+     */
+    public function is_enabled(): bool {
+        return true;
+    }
+
+    /**
+     * Return URL used for management of plugins of this type.
+     * @return moodle_url
+     */
+    public static function get_manage_url(): \moodle_url {
+        return new moodle_url('/admin/settings.php', ['section' => 'h5psettings']);
+    }
+
+    /**
+     * Loads plugin settings to the settings tree
+     *
+     * This function usually includes settings.php file in plugins folder.
+     * Alternatively it can create a link to some settings page (instance of admin_externalpage)
+     *
+     * @param \part_of_admin_tree $adminroot
+     * @param string $parentnodename
+     * @param bool $hassiteconfig whether the current user has moodle/site:config capability
+     */
+    public function load_settings(\part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
+        global $CFG, $USER, $DB, $OUTPUT, $PAGE; // In case settings.php wants to refer to them.
+        $ADMIN = $adminroot; // May be used in settings.php.
+        $plugininfo = $this; // Also can be used inside settings.php.
+
+        if (!$this->is_installed_and_upgraded()) {
+            return;
+        }
+
+        if (!$hassiteconfig) {
+            return;
+        }
+
+        if (file_exists($this->full_path('settings.php'))) {
+            include($this->full_path('settings.php'));
+        }
+    }
+}
\ No newline at end of file
index 680f7c0..5796992 100644 (file)
@@ -36,7 +36,8 @@
         "cachelock": "cache\/locks",
         "fileconverter": "files\/converter",
         "theme": "theme",
-        "local": "local"
+        "local": "local",
+        "h5plib": "h5p\/h5plib"
     },
     "subsystems": {
         "access": null,
index 60b34e7..404f2e1 100644 (file)
@@ -24,6 +24,8 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+use core_h5p\local\library\autoloader;
+
 /**
  * Set params for this button.
  *
@@ -81,7 +83,7 @@ function atto_h5p_strings_for_js() {
     );
 
     $PAGE->requires->strings_for_js($strings, 'atto_h5p');
-    $PAGE->requires->js(new moodle_url('/lib/h5p/js/h5p-resizer.js'));
+    $PAGE->requires->js(autoloader::get_h5p_core_library_url('js/h5p-resizer.js'));
 }
 
 
index e6d5e99..24002e0 100644 (file)
@@ -182,7 +182,7 @@ class external_api {
 
         require_once($CFG->libdir . "/pagelib.php");
 
-        $externalfunctioninfo = self::external_function_info($function);
+        $externalfunctioninfo = static::external_function_info($function);
 
         $currentpage = $PAGE;
         $currentcourse = $COURSE;
@@ -252,7 +252,7 @@ class external_api {
 
             $response['error'] = false;
             $response['data'] = $result;
-        } catch (Exception $e) {
+        } catch (Throwable $e) {
             $exception = get_exception_info($e);
             unset($exception->a);
             $exception->backtrace = format_backtrace($exception->backtrace, true);
index e83e494..0e5e186 100644 (file)
@@ -2,8 +2,6 @@
     <div class="col-md-3">
         <span class="float-sm-right text-nowrap">
             {{#required}}<abbr class="initialism text-danger" title="{{#str}}required{{/str}}">{{#pix}}req, core, {{#str}}required{{/str}}{{/pix}}</abbr>{{/required}}
-            {{#advanced}}<abbr class="initialism text-info" title="{{#str}}advanced{{/str}}">!</abbr>{{/advanced}}
-            {{{helpbutton}}}
         </span>
         {{#text}}
             <label for="{{element.id}}">
index 250c491..1f3897d 100644 (file)
@@ -1,10 +1,13 @@
-{{< core_form/element-template }}
+{{< core_form/element-group }}
     {{$element}}
-        <span class="fdate_selector d-flex">
-        {{#element.elements}}
-            {{{separator}}}
-            {{{html}}}
-        {{/element.elements}}
-        </span>
+        <fieldset class="m-0 p-0 border-0" id="{{element.id}}">
+            <legend class="sr-only">{{label}}</legend>
+            <span class="fdate_selector d-flex">
+            {{#element.elements}}
+                {{{separator}}}
+                {{{html}}}
+            {{/element.elements}}
+            </span>
+        </fieldset>
     {{/element}}
-{{/ core_form/element-template }}
+{{/ core_form/element-group }}
index 225c4ee..a78ad2e 100644 (file)
@@ -1,10 +1,13 @@
-{{< core_form/element-template }}
+{{< core_form/element-group }}
     {{$element}}
-        <div class="fdate_time_selector d-flex flex-wrap align-items-center">
-        {{#element.elements}}
-            {{{separator}}}
-            {{{html}}}
-        {{/element.elements}}
-        </div>
+        <fieldset class="m-0 p-0 border-0" id="{{element.id}}">
+            <legend class="sr-only">{{label}}</legend>
+            <div class="fdate_time_selector d-flex flex-wrap align-items-center">
+            {{#element.elements}}
+                {{{separator}}}
+                {{{html}}}
+            {{/element.elements}}
+            </div>
+        </fieldset>
     {{/element}}
-{{/ core_form/element-template }}
+{{/ core_form/element-group }}
index 9af767f..7a53d37 100644 (file)
@@ -1,8 +1,28 @@
 {{< core_form/element-template }}
+    {{$label}}
+        {{^element.hiddenlabel}}
+            <p id="{{element.id}}_label" class="col-form-label d-inline" aria-hidden="true">
+                {{{label}}}
+            </p>
+        {{/element.hiddenlabel}}
+    {{/label}}
     {{$element}}
-        {{#element.elements}}
-            {{{separator}}}
-            {{{html}}}
-        {{/element.elements}}
+        <fieldset class="m-0 p-0 border-0">
+            <legend class="sr-only">{{label}}</legend>
+            <div class="d-flex flex-wrap">
+            {{#element.elements}}
+                {{{separator}}}
+                {{{html}}}
+            {{/element.elements}}
+            </div>
+        </fieldset>
     {{/element}}
 {{/ core_form/element-template }}
+{{#js}}
+require(['jquery'], function($) {
+    $('#{{element.id}}_label').css('cursor', 'default');
+    $('#{{element.id}}_label').click(function() {
+        $('#{{element.id}}').find('button, a, input, select, textarea, [tabindex]:not([tabindex="-1"])').filter(':enabled').first().focus();
+    });
+});
+{{/js}}
index d445e48..b8f3c48 100644 (file)
@@ -1,9 +1,14 @@
 <div class="form-group {{#error}}has-danger{{/error}} fitem {{#advanced}}advanced{{/advanced}} {{{element.extraclasses}}}" {{#element.groupname}}data-groupname="{{.}}"{{/element.groupname}}>
-    <label class="col-form-label {{#element.hiddenlabel}}sr-only{{/element.hiddenlabel}}" for="{{element.id}}">
-        {{{label}}} {{{helpbutton}}}
+    {{#label}}
+        <label class="col-form-label {{#element.hiddenlabel}}sr-only{{/element.hiddenlabel}}" for="{{element.id}}">
+            {{{label}}}
+        </label>
+    {{/label}}
+    <span>
+        {{{helpbutton}}}
         {{#required}}<abbr class="initialism text-danger" title="{{#str}}required{{/str}}">{{#pix}}req, core, {{#str}}required{{/str}}{{/pix}}</abbr>{{/required}}
         {{#advanced}}<abbr class="initialism text-info" title="{{#str}}advanced{{/str}}">!</abbr>{{/advanced}}
-    </label>
+    </span>
     <span data-fieldtype="{{element.type}}">
         {{$ element }}
             <!-- Element goes here -->
index 6ad5489..bbf538d 100644 (file)
             {{#advanced}}<abbr class="initialism text-info" title="{{#str}}advanced{{/str}}">!</abbr>{{/advanced}}
             {{{helpbutton}}}
         </span>
-        {{^element.staticlabel}}
-        <label class="col-form-label d-inline {{#element.hiddenlabel}}sr-only{{/element.hiddenlabel}}" for="{{element.id}}">
-            {{{label}}}
-        </label>
-        {{/element.staticlabel}}
-        {{#element.staticlabel}}
-        <span class="col-form-label d-inline-block {{#element.hiddenlabel}}sr-only{{/element.hiddenlabel}}">
-            {{{label}}}
-        </span>
-        {{/element.staticlabel}}
+        {{$ label }}
+            {{^element.staticlabel}}
+                <label class="col-form-label d-inline {{#element.hiddenlabel}}sr-only{{/element.hiddenlabel}}" for="{{element.id}}">
+                    {{{label}}}
+                </label>
+            {{/element.staticlabel}}
+            {{#element.staticlabel}}
+                <span class="col-form-label d-inline-block {{#element.hiddenlabel}}sr-only{{/element.hiddenlabel}}">
+                    {{{label}}}
+                </span>
+            {{/element.staticlabel}}
+        {{/ label }}
     </div>
     <div class="col-md-9 form-inline felement" data-fieldtype="{{element.type}}">
         {{$ element }}
index dd23118..e879546 100644 (file)
@@ -947,7 +947,7 @@ function clean_param($param, $type) {
         case PARAM_COMPONENT:
             // We do not want any guessing here, either the name is correct or not
             // please note only normalised component names are accepted.
-            if (!preg_match('/^[a-z]+(_[a-z][a-z0-9_]*)?[a-z0-9]+$/', $param)) {
+            if (!preg_match('/^[a-z][a-z0-9]*(_[a-z][a-z0-9_]*)?[a-z0-9]+$/', $param)) {
                 return '';
             }
             if (strpos($param, '__') !== false) {
index 3eb24e2..34cd2fe 100644 (file)
@@ -400,7 +400,7 @@ class page_requirements_manager {
      *
      * @return int the jsrev to use.
      */
-    protected function get_jsrev() {
+    public function get_jsrev() {
         global $CFG;
 
         if (empty($CFG->cachejs)) {
index 619ae28..aa8101f 100644 (file)
@@ -535,7 +535,7 @@ class core_externallib_testcase extends advanced_testcase {
 
 
     public function test_call_external_function() {
-        global $PAGE, $COURSE;
+        global $PAGE, $COURSE, $CFG;
 
         $this->resetAfterTest(true);
 
@@ -570,6 +570,16 @@ class core_externallib_testcase extends advanced_testcase {
 
         $this->assertSame($beforepage, $PAGE);
         $this->assertSame($beforecourse, $COURSE);
+
+        // Test a function that triggers a PHP exception.
+        require_once($CFG->dirroot . '/lib/tests/fixtures/test_external_function_throwable.php');
+
+        // Call our test function.
+        $result = test_external_function_throwable::call_external_function('core_throw_exception', array(), false);
+
+        $this->assertTrue($result['error']);
+        $this->assertArrayHasKey('exception', $result);
+        $this->assertEquals($result['exception']->message, 'Exception - Modulo by zero');
     }
 
     /**
diff --git a/lib/tests/fixtures/test_external_function_throwable.php b/lib/tests/fixtures/test_external_function_throwable.php
new file mode 100644 (file)
index 0000000..b1e975d
--- /dev/null
@@ -0,0 +1,86 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * An external function that throws an exception, for tests.
+ *
+ * @package    core
+ * @category   phpunit
+ * @copyright  2020 Dani Palou
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once("$CFG->libdir/externallib.php");
+
+/**
+ * Create an external function that throws an exception, for tests.
+ *
+ * @copyright  2020 Dani Palou
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class test_external_function_throwable extends external_api {
+
+    /**
+     * Returns description of throw_exception() parameters.
+     *
+     * @return external_function_parameters
+     */
+    public static function throw_exception_parameters() {
+        return new external_function_parameters(array());
+    }
+
+    /**
+     * Throws a PHP error.
+     *
+     * @return array empty array.
+     */
+    public static function throw_exception() {
+        $a = 1 % 0;
+
+        return array();
+    }
+
+    /**
+     * Returns description of throw_exception() result value.
+     *
+     * @return external_description
+     */
+    public static function throw_exception_returns() {
+        return new external_single_structure(array());
+    }
+
+    /**
+     * Override external_function_info to accept our fake WebService.
+     */
+    public static function external_function_info($function, $strictness=MUST_EXIST) {
+        if ($function == 'core_throw_exception') {
+            // Convert it to an object.
+            $function = new stdClass();
+            $function->name = $function;
+            $function->classname = 'test_external_function_throwable';
+            $function->methodname = 'throw_exception';
+            $function->classpath = ''; // No need to define class path because current file is already loaded.
+            $function->component = 'fake';
+            $function->capabilities = '';
+            $function->services = 'moodle_mobile_app';
+            $function->loginrequired = false;
+        }
+
+        return parent::external_function_info($function, $strictness);
+    }
+}
index 21dd185..3502a1b 100644 (file)
@@ -22,7 +22,7 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-use core_h5p\autoloader;
+use core_h5p\local\library\autoloader;
 use core_h5p\h5p_test_factory;
 
 defined('MOODLE_INTERNAL') || die();
index e98653e..4140074 100644 (file)
@@ -523,7 +523,7 @@ class core_moodlelib_testcase extends advanced_testcase {
         $this->assertSame('', clean_param('mod__something', PARAM_COMPONENT));
         $this->assertSame('', clean_param('auth_xx-yy', PARAM_COMPONENT));
         $this->assertSame('', clean_param('_auth_xx', PARAM_COMPONENT));
-        $this->assertSame('', clean_param('a2uth_xx', PARAM_COMPONENT));
+        $this->assertSame('a2uth_xx', clean_param('a2uth_xx', PARAM_COMPONENT));
         $this->assertSame('', clean_param('auth_xx_', PARAM_COMPONENT));
         $this->assertSame('', clean_param('auth_xx.old', PARAM_COMPONENT));
         $this->assertSame('', clean_param('_user', PARAM_COMPONENT));
index fea8015..e2c5f4a 100644 (file)
     <license>MIT</license>
     <version>4.1.0</version>
   </library>
-  <library>
-    <location>h5p</location>
-    <name>h5p-php-library</name>
-    <license>GPL-3.0</license>
-    <version>1.24</version>
-  </library>
 </libraries>
index 3ae720c..b647892 100644 (file)
@@ -34,6 +34,8 @@ information provided here is intended especially for developers.
 * Added function cleanup_after_drop to the database_manager class to take care of all the cleanups that need to be done after a table is dropped.
 * The 'xxxx_check_password_policy' callback now only fires if $CFG->passwordpolicy is true
 * grade_item::update_final_grade() can now take an optional parameter to set the grade->timemodified. If not present the current time will carry on being used.
+* lib/outputrequirementslib::get_jsrev now is public, it can be called from other classes.
+* H5P libraries have been moved from /lib/h5p to h5p/h5plib as an h5plib plugintype.
 
 === 3.8 ===
 * Add CLI option to notify all cron tasks to stop: admin/cli/cron.php --stop
index 4831fe5..5e6a8bb 100644 (file)
 
 body.path-question-type {
     /* Hacks to display the labels within a form group. */
-    .fitem_fgroup .accesshide {
-        font: inherit;
-        position: static;
-        padding-right: .3em;
-    }
-    .form-group .sr-only {
+    .form-group .sr-only:not(legend) {
         position: static;
         width: auto;
         height: auto;
index b097199..6a5a1c5 100644 (file)
@@ -14917,11 +14917,7 @@ a.ygtvspacer:hover {
 
 body.path-question-type {
   /* Hacks to display the labels within a form group. */ }
-  body.path-question-type .fitem_fgroup .accesshide {
-    font: inherit;
-    position: static;
-    padding-right: .3em; }
-  body.path-question-type .form-group .sr-only {
+  body.path-question-type .form-group .sr-only:not(legend) {
     position: static;
     width: auto;
     height: auto;
index 6782110..415f3ef 100644 (file)
@@ -15135,11 +15135,7 @@ a.ygtvspacer:hover {
 
 body.path-question-type {
   /* Hacks to display the labels within a form group. */ }
-  body.path-question-type .fitem_fgroup .accesshide {
-    font: inherit;
-    position: static;
-    padding-right: .3em; }
-  body.path-question-type .form-group .sr-only {
+  body.path-question-type .form-group .sr-only:not(legend) {
     position: static;
     width: auto;
     height: auto;