Merge branch 'MDL-66272-master' of git://github.com/vmdef/moodle
authorJun Pataleta <jun@moodle.com>
Thu, 8 Aug 2019 04:43:51 +0000 (12:43 +0800)
committerJun Pataleta <jun@moodle.com>
Thu, 8 Aug 2019 04:43:51 +0000 (12:43 +0800)
420 files changed:
.eslintignore
.eslintrc
.stylelintignore
.travis.yml
Gruntfile.js
admin/cli/install.php
admin/cli/reset_password.php
admin/roles/allow.php
admin/roles/classes/allow_assign_page.php
admin/roles/classes/allow_override_page.php
admin/roles/classes/allow_role_page.php
admin/roles/classes/allow_switch_page.php
admin/roles/classes/allow_view_page.php
admin/roles/classes/define_role_table_advanced.php
admin/roles/define.php
admin/roles/override.php
admin/settings/courses.php
admin/settings/server.php
admin/tool/analytics/classes/output/models_list.php
admin/tool/analytics/classes/task/predict_models.php
admin/tool/analytics/classes/task/train_models.php
admin/tool/analytics/cli/evaluate_model.php
admin/tool/dataprivacy/classes/form/purpose.php
admin/tool/mobile/tests/externallib_test.php
admin/tool/recyclebin/classes/category_bin.php
admin/tool/recyclebin/classes/course_bin.php
admin/tool/templatelibrary/amd/build/display.min.js
admin/tool/templatelibrary/amd/build/display.min.js.map
admin/tool/templatelibrary/amd/src/display.js
admin/tool/templatelibrary/classes/api.php
admin/tool/templatelibrary/classes/external.php
admin/tool/templatelibrary/version.php
admin/tool/uploaduser/index.php
admin/tool/usertours/classes/helper.php
admin/tool/usertours/classes/local/table/tour_list.php
admin/tool/usertours/classes/manager.php
admin/tool/usertours/classes/tour.php
admin/tool/usertours/lang/en/tool_usertours.php
admin/tool/usertours/tests/behat/duplicate_tour.feature [new file with mode: 0644]
analytics/classes/analysis.php
analytics/classes/local/analyser/base.php
analytics/tests/behat/manage_models.feature [new file with mode: 0644]
auth/classes/output/login.php
auth/oauth2/classes/auth.php
babel-plugin-add-module-to-define.js
backup/backup.php
backup/controller/backup_controller.class.php
backup/moodle2/backup_root_task.class.php
backup/util/dbops/backup_plan_dbops.class.php
backup/util/dbops/restore_dbops.class.php
backup/util/helper/backup_cron_helper.class.php
backup/util/helper/backup_helper.class.php
backup/util/ui/backup_ui_stage.class.php
backup/util/ui/renderer.php
blocks/myoverview/amd/build/view.min.js
blocks/myoverview/amd/build/view.min.js.map
blocks/myoverview/amd/src/view.js
blocks/myoverview/classes/output/main.php
blocks/myoverview/lib.php
blocks/myoverview/templates/courses-view.mustache
blocks/myoverview/tests/behat/block_myoverview_pagelimit_persistence.feature
blocks/myoverview/version.php
blocks/search_forums/templates/search_form.mustache
blocks/settings/templates/search_form.mustache
config-dist.php
course/lib.php
course/templates/course_search_form.mustache
course/tests/courselib_test.php
enrol/editenrolment.php
enrol/editenrolment_form.php
enrol/manual/ajax.php
enrol/manual/classes/enrol_users_form.php
enrol/manual/manage.php
enrol/manual/tests/behat/quickenrolment.feature
enrol/tests/enrollib_test.php
grade/lib.php
grade/report/history/templates/user_button.mustache
grade/report/singleview/templates/bulk_insert.mustache
grade/report/singleview/templates/button.mustache
grade/report/singleview/templates/dropdown_attribute.mustache
grade/report/singleview/templates/text_attribute.mustache
grade/templates/edit_tree.mustache
grade/templates/weight_field.mustache
grade/templates/weight_override_field.mustache
install.php
install/lang/pl/admin.php
lang/en/admin.php
lang/en/backup.php
lang/en/badges.php
lang/en/completion.php
lang/en/deprecated.txt
lang/en/enrol.php
lang/en/message.php
lang/en/moodle.php
lang/en/role.php
lib/accesslib.php
lib/adminlib.php
lib/amd/build/adapter.min.js
lib/amd/build/adapter.min.js.map
lib/amd/build/form-autocomplete.min.js
lib/amd/build/form-autocomplete.min.js.map
lib/amd/build/paged_content_factory.min.js
lib/amd/build/paged_content_factory.min.js.map
lib/amd/build/templates.min.js
lib/amd/build/templates.min.js.map
lib/amd/src/adapter.js
lib/amd/src/form-autocomplete.js
lib/amd/src/paged_content_factory.js
lib/amd/src/templates.js
lib/authlib.php
lib/behat/classes/behat_config_util.php
lib/classes/event/capability_assigned.php [new file with mode: 0644]
lib/classes/event/capability_unassigned.php [new file with mode: 0644]
lib/classes/event/role_allow_assign_updated.php
lib/classes/event/role_allow_override_updated.php
lib/classes/event/role_allow_switch_updated.php
lib/classes/event/role_allow_view_updated.php
lib/classes/event/role_capabilities_updated.php
lib/classes/event/role_updated.php [new file with mode: 0644]
lib/classes/output/external.php
lib/classes/output/mustache_template_finder.php
lib/classes/output/mustache_template_source_loader.php
lib/classes/plugin_manager.php
lib/classes/requirejs.php
lib/classes/task/task_log_cleanup_task.php
lib/classes/user.php
lib/db/messages.php
lib/db/upgrade.php
lib/deprecatedlib.php
lib/enrollib.php
lib/filestorage/file_storage.php
lib/filestorage/file_system.php
lib/filestorage/file_system_filedir.php
lib/filestorage/tests/file_storage_test.php
lib/filestorage/tests/file_system_filedir_test.php
lib/filestorage/tests/file_system_test.php
lib/form/templates/element-radio.mustache
lib/installlib.php
lib/jssourcemap.php
lib/moodlelib.php
lib/navigationlib.php
lib/outputrenderers.php
lib/pagelib.php
lib/phpmailer/COMMITMENT [new file with mode: 0644]
lib/phpmailer/LICENSE
lib/phpmailer/README.md
lib/phpmailer/README_MOODLE.txt
lib/phpmailer/VERSION
lib/phpmailer/changelog.md [deleted file]
lib/phpmailer/language/phpmailer.lang-ar.php
lib/phpmailer/language/phpmailer.lang-eo.php
lib/phpmailer/language/phpmailer.lang-es.php
lib/phpmailer/language/phpmailer.lang-hi.php [new file with mode: 0644]
lib/phpmailer/language/phpmailer.lang-hu.php
lib/phpmailer/language/phpmailer.lang-id.php
lib/phpmailer/language/phpmailer.lang-it.php
lib/phpmailer/language/phpmailer.lang-mg.php [new file with mode: 0644]
lib/phpmailer/language/phpmailer.lang-ms.php
lib/phpmailer/language/phpmailer.lang-nb.php
lib/phpmailer/language/phpmailer.lang-nl.php
lib/phpmailer/language/phpmailer.lang-sk.php
lib/phpmailer/language/phpmailer.lang-sl.php
lib/phpmailer/language/phpmailer.lang-sr.php [moved from lib/phpmailer/language/phpmailer.lang-rs.php with 79% similarity]
lib/phpmailer/language/phpmailer.lang-tl.php [new file with mode: 0644]
lib/phpmailer/language/phpmailer.lang-uk.php
lib/phpmailer/src/PHPMailer.php
lib/phpmailer/src/SMTP.php
lib/phpunit/tests/basic_test.php
lib/requirejs.php
lib/setup.php
lib/templates/custom_menu_item.mustache
lib/templates/filemanager_confirmdialog.mustache
lib/templates/filemanager_default_searchform.mustache
lib/templates/filemanager_fileselect.mustache
lib/templates/filemanager_modal_generallayout.mustache
lib/templates/filemanager_page_generallayout.mustache
lib/templates/filemanager_processexistingfile.mustache
lib/templates/filemanager_processexistingfilemultiple.mustache
lib/templates/filemanager_selectlayout.mustache
lib/templates/filemanager_uploadform.mustache
lib/templates/form_autocomplete_input.mustache
lib/templates/form_autocomplete_suggestions.mustache
lib/templates/full_header.mustache
lib/templates/loginform.mustache
lib/templates/paged_content_paging_bar.mustache
lib/templates/preferences_groups.mustache
lib/tests/accesslib_test.php
lib/tests/behat/app_behat_runtime.js
lib/tests/behat/behat_app.php
lib/tests/behat/behat_data_generators.php
lib/tests/behat/behat_forms.php
lib/tests/behat/behat_general.php
lib/tests/moodlelib_test.php
lib/tests/mustache_template_finder_test.php
lib/tests/outputcomponents_test.php
lib/tests/weblib_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/weblib.php
login/change_password_form.php
login/set_password_form.php
login/tests/lib_test.php
message/amd/build/message_drawer.min.js
message/amd/build/message_drawer.min.js.map
message/amd/build/message_drawer_view_contacts.min.js
message/amd/build/message_drawer_view_contacts.min.js.map
message/amd/build/message_drawer_view_conversation.min.js
message/amd/build/message_drawer_view_conversation.min.js.map
message/amd/build/message_drawer_view_conversation_constants.min.js
message/amd/build/message_drawer_view_conversation_constants.min.js.map
message/amd/build/message_drawer_view_conversation_renderer.min.js
message/amd/build/message_drawer_view_conversation_renderer.min.js.map
message/amd/build/message_drawer_view_conversation_state_manager.min.js
message/amd/build/message_drawer_view_conversation_state_manager.min.js.map
message/amd/src/message_drawer.js
message/amd/src/message_drawer_view_contacts.js
message/amd/src/message_drawer_view_conversation.js
message/amd/src/message_drawer_view_conversation_constants.js
message/amd/src/message_drawer_view_conversation_renderer.js
message/amd/src/message_drawer_view_conversation_state_manager.js
message/classes/api.php
message/classes/helper.php
message/externallib.php
message/index.php
message/pendingcontactrequests.php [new file with mode: 0644]
message/templates/message_drawer_view_conversation_body_confirm_dialogue.mustache
message/templates/message_index.mustache
message/tests/api_test.php
message/tests/behat/block_user.feature [new file with mode: 0644]
message/tests/behat/message_admin_settings.feature
message/tests/externallib_test.php
message/upgrade.txt
mod/assign/feedback/editpdf/styles.css
mod/data/lib.php
mod/data/module.js [deleted file]
mod/data/view.php
mod/forum/classes/local/factories/renderer.php
mod/forum/templates/discussion_list.mustache
mod/forum/templates/quick_search_form.mustache
mod/forum/templates/social_discussion_list.mustache
mod/forum/view.php
pix/s/angry.svg
pix/s/approve.svg
pix/s/biggrin.svg
pix/s/blackeye.svg
pix/s/blush.svg
pix/s/clown.svg
pix/s/cool.svg
pix/s/dead.svg
pix/s/egg.svg
pix/s/evil.svg
pix/s/heart.svg
pix/s/kiss.svg
pix/s/martin.svg
pix/s/mixed.svg
pix/s/no.svg
pix/s/sad.svg
pix/s/shy.svg
pix/s/sleepy.svg
pix/s/smiley.svg
pix/s/surprise.svg
pix/s/thoughtful.svg
pix/s/tongueout.svg
pix/s/wideeyes.svg
pix/s/wink.svg
pix/s/yes.svg
question/behaviour/interactive/behaviour.php
question/behaviour/interactive/renderer.php
question/behaviour/interactive/tests/walkthrough_test.php
question/format.php
repository/googledocs/lang/en/repository_googledocs.php
search/tests/behat/search_by_user.feature
theme/boost/amd/build/alert.min.js
theme/boost/amd/build/alert.min.js.map
theme/boost/amd/build/button.min.js
theme/boost/amd/build/button.min.js.map
theme/boost/amd/build/carousel.min.js
theme/boost/amd/build/carousel.min.js.map
theme/boost/amd/build/collapse.min.js
theme/boost/amd/build/collapse.min.js.map
theme/boost/amd/build/dropdown.min.js
theme/boost/amd/build/dropdown.min.js.map
theme/boost/amd/build/index.min.js [new file with mode: 0644]
theme/boost/amd/build/index.min.js.map [new file with mode: 0644]
theme/boost/amd/build/loader.min.js
theme/boost/amd/build/loader.min.js.map
theme/boost/amd/build/modal.min.js
theme/boost/amd/build/modal.min.js.map
theme/boost/amd/build/pending.min.js
theme/boost/amd/build/pending.min.js.map
theme/boost/amd/build/popover.min.js
theme/boost/amd/build/popover.min.js.map
theme/boost/amd/build/sanitizer.min.js [new file with mode: 0644]
theme/boost/amd/build/sanitizer.min.js.map [new file with mode: 0644]
theme/boost/amd/build/scrollspy.min.js
theme/boost/amd/build/scrollspy.min.js.map
theme/boost/amd/build/tab.min.js
theme/boost/amd/build/tab.min.js.map
theme/boost/amd/build/toast.min.js [new file with mode: 0644]
theme/boost/amd/build/toast.min.js.map [new file with mode: 0644]
theme/boost/amd/build/tooltip.min.js
theme/boost/amd/build/tooltip.min.js.map
theme/boost/amd/build/util.min.js
theme/boost/amd/build/util.min.js.map
theme/boost/amd/src/alert.js
theme/boost/amd/src/button.js
theme/boost/amd/src/carousel.js
theme/boost/amd/src/collapse.js
theme/boost/amd/src/dropdown.js
theme/boost/amd/src/index.js [new file with mode: 0644]
theme/boost/amd/src/loader.js
theme/boost/amd/src/modal.js
theme/boost/amd/src/pending.js
theme/boost/amd/src/popover.js
theme/boost/amd/src/sanitizer.js [new file with mode: 0644]
theme/boost/amd/src/scrollspy.js
theme/boost/amd/src/tab.js
theme/boost/amd/src/toast.js [new file with mode: 0644]
theme/boost/amd/src/tooltip.js
theme/boost/amd/src/util.js
theme/boost/readme_moodle.txt
theme/boost/scss/bootstrap/LICENSE [deleted file]
theme/boost/scss/bootstrap/_alert.scss
theme/boost/scss/bootstrap/_badge.scss
theme/boost/scss/bootstrap/_breadcrumb.scss
theme/boost/scss/bootstrap/_button-group.scss
theme/boost/scss/bootstrap/_buttons.scss
theme/boost/scss/bootstrap/_card.scss
theme/boost/scss/bootstrap/_carousel.scss
theme/boost/scss/bootstrap/_close.scss
theme/boost/scss/bootstrap/_code.scss
theme/boost/scss/bootstrap/_custom-forms.scss
theme/boost/scss/bootstrap/_dropdown.scss
theme/boost/scss/bootstrap/_forms.scss
theme/boost/scss/bootstrap/_functions.scss
theme/boost/scss/bootstrap/_images.scss
theme/boost/scss/bootstrap/_input-group.scss
theme/boost/scss/bootstrap/_jumbotron.scss
theme/boost/scss/bootstrap/_list-group.scss
theme/boost/scss/bootstrap/_mixins.scss
theme/boost/scss/bootstrap/_modal.scss
theme/boost/scss/bootstrap/_nav.scss
theme/boost/scss/bootstrap/_navbar.scss
theme/boost/scss/bootstrap/_pagination.scss
theme/boost/scss/bootstrap/_popover.scss
theme/boost/scss/bootstrap/_print.scss
theme/boost/scss/bootstrap/_progress.scss
theme/boost/scss/bootstrap/_reboot.scss
theme/boost/scss/bootstrap/_spinners.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/_tables.scss
theme/boost/scss/bootstrap/_toasts.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/_tooltip.scss
theme/boost/scss/bootstrap/_transitions.scss
theme/boost/scss/bootstrap/_type.scss
theme/boost/scss/bootstrap/_utilities.scss
theme/boost/scss/bootstrap/_variables.scss
theme/boost/scss/bootstrap/bootstrap-grid.scss
theme/boost/scss/bootstrap/bootstrap-reboot.scss
theme/boost/scss/bootstrap/bootstrap.scss
theme/boost/scss/bootstrap/mixins/_badge.scss
theme/boost/scss/bootstrap/mixins/_border-radius.scss
theme/boost/scss/bootstrap/mixins/_box-shadow.scss
theme/boost/scss/bootstrap/mixins/_breakpoints.scss
theme/boost/scss/bootstrap/mixins/_buttons.scss
theme/boost/scss/bootstrap/mixins/_caret.scss
theme/boost/scss/bootstrap/mixins/_deprecate.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/mixins/_float.scss
theme/boost/scss/bootstrap/mixins/_forms.scss
theme/boost/scss/bootstrap/mixins/_gradients.scss
theme/boost/scss/bootstrap/mixins/_grid-framework.scss
theme/boost/scss/bootstrap/mixins/_grid.scss
theme/boost/scss/bootstrap/mixins/_hover.scss
theme/boost/scss/bootstrap/mixins/_image.scss
theme/boost/scss/bootstrap/mixins/_list-group.scss
theme/boost/scss/bootstrap/mixins/_nav-divider.scss
theme/boost/scss/bootstrap/mixins/_navbar-align.scss [deleted file]
theme/boost/scss/bootstrap/mixins/_pagination.scss
theme/boost/scss/bootstrap/mixins/_reset-text.scss
theme/boost/scss/bootstrap/mixins/_screen-reader.scss
theme/boost/scss/bootstrap/mixins/_size.scss
theme/boost/scss/bootstrap/mixins/_table-row.scss
theme/boost/scss/bootstrap/mixins/_text-emphasis.scss
theme/boost/scss/bootstrap/mixins/_text-hide.scss
theme/boost/scss/bootstrap/mixins/_transition.scss
theme/boost/scss/bootstrap/mixins/_visibility.scss
theme/boost/scss/bootstrap/utilities/_borders.scss
theme/boost/scss/bootstrap/utilities/_display.scss
theme/boost/scss/bootstrap/utilities/_embed.scss
theme/boost/scss/bootstrap/utilities/_flex.scss
theme/boost/scss/bootstrap/utilities/_float.scss
theme/boost/scss/bootstrap/utilities/_overflow.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/utilities/_position.scss
theme/boost/scss/bootstrap/utilities/_shadows.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/utilities/_sizing.scss
theme/boost/scss/bootstrap/utilities/_spacing.scss
theme/boost/scss/bootstrap/utilities/_stretched-link.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/utilities/_text.scss
theme/boost/scss/bootstrap/utilities/_visibility.scss
theme/boost/scss/bootstrap/vendor/_rfs.scss [new file with mode: 0644]
theme/boost/scss/moodle/bs4alphacompat.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/drawer.scss
theme/boost/scss/moodle/forms.scss
theme/boost/style/moodle.css
theme/boost/tests/behat/settingstabs.feature [new file with mode: 0644]
theme/boost/thirdpartylibs.xml
theme/classic/style/moodle.css
theme/upgrade.txt
user/amd/build/status_field.min.js
user/amd/build/status_field.min.js.map
user/amd/src/status_field.js
user/classes/form/defaulthomepage_form.php [new file with mode: 0644]
user/defaulthomepage.php [new file with mode: 0644]
user/editadvanced_form.php
user/index.php
user/lib.php
user/renderer.php
user/tests/behat/filter_participants.feature
user/tests/behat/set_default_homepage.feature
version.php

index e645472..fe02a73 100644 (file)
@@ -74,10 +74,13 @@ theme/boost/amd/src/button.js
 theme/boost/amd/src/carousel.js
 theme/boost/amd/src/collapse.js
 theme/boost/amd/src/dropdown.js
+theme/boost/amd/src/index.js
 theme/boost/amd/src/modal.js
 theme/boost/amd/src/popover.js
+theme/boost/amd/src/sanitizer.js
 theme/boost/amd/src/scrollspy.js
 theme/boost/amd/src/tab.js
+theme/boost/amd/src/toast.js
 theme/boost/amd/src/tooltip.js
 theme/boost/amd/src/util.js
 theme/boost/amd/src/tether.js
index 8020549..2388717 100644 (file)
--- a/.eslintrc
+++ b/.eslintrc
       }
     },
     {
-      files: ["**/amd/src/*.js"],
+      files: ["**/amd/src/*.js", "**/amd/src/**/*.js"],
       // We support es6 now. Woot!
       env: {
         es6: true
index e0b754e..d3e4974 100644 (file)
@@ -75,10 +75,13 @@ theme/boost/amd/src/button.js
 theme/boost/amd/src/carousel.js
 theme/boost/amd/src/collapse.js
 theme/boost/amd/src/dropdown.js
+theme/boost/amd/src/index.js
 theme/boost/amd/src/modal.js
 theme/boost/amd/src/popover.js
+theme/boost/amd/src/sanitizer.js
 theme/boost/amd/src/scrollspy.js
 theme/boost/amd/src/tab.js
+theme/boost/amd/src/toast.js
 theme/boost/amd/src/tooltip.js
 theme/boost/amd/src/util.js
 theme/boost/amd/src/tether.js
index 56a20e8..90edc4f 100644 (file)
@@ -11,7 +11,10 @@ notifications:
 
 language: php
 
-dist: trusty
+dist: xenial
+
+services:
+    - mysql
 
 php:
     # We only run the highest and lowest supported versions to reduce the load on travis-ci.org.
@@ -20,10 +23,6 @@ php:
 
 addons:
   postgresql: "9.6"
-  packages:
-    - mysql-server-5.6
-    - mysql-client-core-5.6
-    - mysql-client-5.6
 
 env:
     # Although we want to run these jobs and see failures as quickly as possible, we also want to get the slowest job to
@@ -71,10 +70,10 @@ install:
         then
             sudo mkdir /mnt/ramdisk
             sudo mount -t tmpfs -o size=1024m tmpfs /mnt/ramdisk
-            sudo stop mysql
+            sudo service mysql stop
             sudo mv /var/lib/mysql /mnt/ramdisk
             sudo ln -s /mnt/ramdisk/mysql /var/lib/mysql
-            sudo start mysql
+            sudo service mysql restart
         fi
     - >
         if [ "$DB" = 'pgsql' ];
index 765a5a2..771bed1 100644 (file)
@@ -64,7 +64,14 @@ module.exports = function(grunt) {
     var inAMD = path.basename(cwd) == 'amd';
 
     // Globbing pattern for matching all AMD JS source files.
-    var amdSrc = [inAMD ? cwd + '/src/*.js' : '**/amd/src/*.js'];
+    var amdSrc = [];
+    if (inAMD) {
+        amdSrc.push(cwd + "/src/*.js");
+        amdSrc.push(cwd + "/src/**/*.js");
+    } else {
+        amdSrc.push("**/amd/src/*.js");
+        amdSrc.push("**/amd/src/**/*.js");
+    }
 
     /**
      * Function to generate the destination for the uglify task
index e9203ec..2dea6ca 100644 (file)
@@ -157,7 +157,6 @@ $CFG->lang                 = 'en';
 $CFG->dirroot              = dirname(dirname(__DIR__));
 $CFG->libdir               = "$CFG->dirroot/lib";
 $CFG->wwwroot              = "http://localhost";
-$CFG->httpswwwroot         = $CFG->wwwroot;
 $CFG->docroot              = 'http://docs.moodle.org';
 $CFG->running_installer    = true;
 $CFG->early_install_lang   = true;
@@ -389,8 +388,6 @@ if ($interactive) {
     }
 }
 $CFG->wwwroot       = $wwwroot;
-$CFG->httpswwwroot  = $CFG->wwwroot;
-
 
 //We need dataroot before lang download
 $CFG->dataroot = $options['dataroot'];
index 9d4dc14..8bd03eb 100644 (file)
@@ -94,7 +94,7 @@ if ($options['password'] == '' ) {
 
 $errmsg = '';//prevent eclipse warning
 if (!$options['ignore-password-policy'] ) {
-    if (!check_password_policy($password, $errmsg)) {
+    if (!check_password_policy($password, $errmsg, $user)) {
         cli_error(html_to_text($errmsg, 0));
     }
 }
index 88609cf..643e6ce 100644 (file)
@@ -46,25 +46,6 @@ $controller = new $classformode[$mode]();
 
 if (optional_param('submit', false, PARAM_BOOL) && data_submitted() && confirm_sesskey()) {
     $controller->process_submission();
-    $event = null;
-    // Create event depending on mode.
-    switch ($mode) {
-        case 'assign':
-            $event = \core\event\role_allow_assign_updated::create(array('context' => $syscontext));
-            break;
-        case 'override':
-            $event = \core\event\role_allow_override_updated::create(array('context' => $syscontext));
-            break;
-        case 'switch':
-            $event = \core\event\role_allow_switch_updated::create(array('context' => $syscontext));
-            break;
-        case 'view':
-            $event = \core\event\role_allow_view_updated::create(array('context' => $syscontext));
-            break;
-    }
-    if ($event) {
-        $event->trigger();
-    }
     redirect($baseurl);
 }
 
index a89228f..002d944 100644 (file)
@@ -46,4 +46,8 @@ class core_role_allow_assign_page extends core_role_allow_role_page {
     public function get_intro_text() {
         return get_string('configallowassign', 'core_admin');
     }
+
+    protected function get_eventclass() {
+        return \core\event\role_allow_assign_updated::class;
+    }
 }
index 4b160ee..16226a6 100644 (file)
@@ -46,4 +46,8 @@ class core_role_allow_override_page extends core_role_allow_role_page {
     public function get_intro_text() {
         return get_string('configallowoverride2', 'core_admin');
     }
+
+    protected function get_eventclass() {
+        return \core\event\role_allow_override_updated::class;
+    }
 }
index c84d01a..376489f 100644 (file)
@@ -59,12 +59,36 @@ abstract class core_role_allow_role_page {
      */
     public function process_submission() {
         global $DB;
+
+        $context = context_system::instance();
+        $this->load_current_settings();
+
         // Delete all records, then add back the ones that should be allowed.
         $DB->delete_records($this->tablename);
         foreach ($this->roles as $fromroleid => $notused) {
             foreach ($this->roles as $targetroleid => $alsonotused) {
+                $isallowed = $this->allowed[$fromroleid][$targetroleid];
                 if (optional_param('s_' . $fromroleid . '_' . $targetroleid, false, PARAM_BOOL)) {
                     $this->set_allow($fromroleid, $targetroleid);
+                    // Only trigger events if this role allow relationship did not exist and the checkbox element
+                    // has been submitted.
+                    if (!$isallowed) {
+                        $eventclass = $this->get_eventclass();
+                        $eventclass::create([
+                            'context' => $context,
+                            'objectid' => $fromroleid,
+                            'other' => ['targetroleid' => $targetroleid, 'allow' => true]
+                        ])->trigger();
+                    }
+                } else if ($isallowed) {
+                    // When the user has deselect an existing role allow checkbox but it is in the list of roles
+                    // allowances.
+                    $eventclass = $this->get_eventclass();
+                    $eventclass::create([
+                        'context' => $context,
+                        'objectid' => $fromroleid,
+                        'other' => ['targetroleid' => $targetroleid, 'allow' => false]
+                    ])->trigger();
                 }
             }
         }
@@ -161,4 +185,10 @@ abstract class core_role_allow_role_page {
      * @return string
      */
     public abstract function get_intro_text();
+
+    /**
+     * Returns the allow class respective event class name.
+     * @return string
+     */
+    protected abstract function get_eventclass();
 }
index 5b22e1e..195aab7 100644 (file)
@@ -58,4 +58,8 @@ class core_role_allow_switch_page extends core_role_allow_role_page {
     public function get_intro_text() {
         return get_string('configallowswitch', 'core_admin');
     }
+
+    protected function get_eventclass() {
+        return \core\event\role_allow_switch_updated::class;
+    }
 }
index f1a1031..d331322 100644 (file)
@@ -74,4 +74,8 @@ class core_role_allow_view_page extends core_role_allow_role_page {
     public function get_intro_text() {
         return get_string('configallowview', 'core_admin');
     }
+
+    protected function get_eventclass() {
+        return \core\event\role_allow_view_updated::class;
+    }
 }
index d5f6c18..265119f 100644 (file)
@@ -434,7 +434,7 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
     }
 
     public function save_changes() {
-        global $DB;
+        global $DB, $USER;
 
         if (!$this->roleid) {
             // Creating role.
@@ -444,6 +444,20 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
             // Updating role.
             $DB->update_record('role', $this->role);
 
+            // Trigger role updated event.
+            \core\event\role_updated::create([
+                'userid' => $USER->id,
+                'objectid' => $this->role->id,
+                'context' => $this->context,
+                'other' => [
+                    'name' => $this->role->name,
+                    'shortname' => $this->role->shortname,
+                    'description' => $this->role->description,
+                    'archetype' => $this->role->archetype,
+                    'contextlevels' => $this->contextlevels
+                ]
+            ])->trigger();
+
             // This will ensure the course contacts cache is purged so name changes get updated in
             // the UI. It would be better to do this only when we know that fields affected are
             // updated. But thats getting into the weeds of the coursecat cache and role edits
@@ -473,10 +487,17 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
         $addfunction = "core_role_set_{$type}_allowed";
         $deltable = 'role_allow_'.$type;
         $field = 'allow'.$type;
+        $eventclass = "\\core\\event\\role_allow_" . $type . "_updated";
+        $context = context_system::instance();
 
         foreach ($current as $roleid) {
             if (!in_array($roleid, $wanted)) {
                 $DB->delete_records($deltable, array('roleid'=>$this->roleid, $field=>$roleid));
+                $eventclass::create([
+                    'context' => $context,
+                    'objectid' => $this->roleid,
+                    'other' => ['targetroleid' => $roleid, 'allow' => false]
+                ])->trigger();
                 continue;
             }
             $key = array_search($roleid, $wanted);
@@ -488,6 +509,14 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
                 $roleid = $this->roleid;
             }
             $addfunction($this->roleid, $roleid);
+
+            if (in_array($roleid, $wanted)) {
+                $eventclass::create([
+                    'context' => $context,
+                    'objectid' => $this->roleid,
+                    'other' => ['targetroleid' => $roleid, 'allow' => true]
+                ])->trigger();
+            }
         }
     }
 
index 1a90cdd..fdf144d 100644 (file)
@@ -200,19 +200,6 @@ if (optional_param('cancel', false, PARAM_BOOL)) {
 if (optional_param('savechanges', false, PARAM_BOOL) && confirm_sesskey() && $definitiontable->is_submission_valid()) {
     $definitiontable->save_changes();
     $tableroleid = $definitiontable->get_role_id();
-    // Trigger event.
-    $event = \core\event\role_capabilities_updated::create(
-        array(
-            'context' => $systemcontext,
-            'objectid' => $tableroleid
-        )
-    );
-    $event->set_legacy_logdata(array(SITEID, 'role', $action, 'admin/roles/define.php?action=view&roleid=' . $tableroleid,
-        $definitiontable->get_role_name(), '', $USER->id));
-    if (!empty($role)) {
-        $event->add_record_snapshot('role', $role);
-    }
-    $event->trigger();
 
     if ($action === 'add') {
         redirect(new moodle_url('/admin/roles/define.php', array('action'=>'view', 'roleid'=>$definitiontable->get_role_id())));
index 878c820..f3d393b 100644 (file)
@@ -134,22 +134,6 @@ $overridestable->read_submitted_permissions();
 if (optional_param('savechanges', false, PARAM_BOOL) && confirm_sesskey()) {
     $overridestable->save_changes();
     $rolename = $overridableroles[$roleid];
-    // Trigger event.
-    $event = \core\event\role_capabilities_updated::create(
-        array(
-            'context' => $context,
-            'objectid' => $roleid,
-        )
-    );
-
-    $event->set_legacy_logdata(
-        array(
-            $course->id, 'role', 'override', 'admin/roles/override.php?contextid=' . $context->id . '&roleid=' . $roleid,
-            $rolename, '', $USER->id
-        )
-    );
-    $event->add_record_snapshot('role', $role);
-    $event->trigger();
 
     redirect($returnurl);
 }
index ace90d3..2525f22 100644 (file)
@@ -206,6 +206,11 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
     $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_role_assignments', new lang_string('generalroleassignments','backup'), new lang_string('configgeneralroleassignments','backup'), array('value'=>1, 'locked'=>0)));
     $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_activities', new lang_string('generalactivities','backup'), new lang_string('configgeneralactivities','backup'), array('value'=>1, 'locked'=>0)));
     $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_blocks', new lang_string('generalblocks','backup'), new lang_string('configgeneralblocks','backup'), array('value'=>1, 'locked'=>0)));
+    $temp->add(new admin_setting_configcheckbox_with_lock(
+            'backup/backup_general_files',
+            new lang_string('generalfiles', 'backup'),
+            new lang_string('configgeneralfiles', 'backup'),
+            array('value' => '1', 'locked' => 0)));
     $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_filters', new lang_string('generalfilters','backup'), new lang_string('configgeneralfilters','backup'), array('value'=>1, 'locked'=>0)));
     $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_comments', new lang_string('generalcomments','backup'), new lang_string('configgeneralcomments','backup'), array('value'=>1, 'locked'=>0)));
     $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_badges', new lang_string('generalbadges','backup'), new lang_string('configgeneralbadges','backup'), array('value'=>1,'locked'=>0)));
@@ -341,6 +346,10 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
     $temp->add(new admin_setting_configcheckbox('backup/backup_auto_role_assignments', new lang_string('generalroleassignments','backup'), new lang_string('configgeneralroleassignments','backup'), 1));
     $temp->add(new admin_setting_configcheckbox('backup/backup_auto_activities', new lang_string('generalactivities','backup'), new lang_string('configgeneralactivities','backup'), 1));
     $temp->add(new admin_setting_configcheckbox('backup/backup_auto_blocks', new lang_string('generalblocks','backup'), new lang_string('configgeneralblocks','backup'), 1));
+    $temp->add(new admin_setting_configcheckbox(
+            'backup/backup_auto_files',
+            new lang_string('generalfiles', 'backup'),
+            new lang_string('configgeneralfiles', 'backup'), '1'));
     $temp->add(new admin_setting_configcheckbox('backup/backup_auto_filters', new lang_string('generalfilters','backup'), new lang_string('configgeneralfilters','backup'), 1));
     $temp->add(new admin_setting_configcheckbox('backup/backup_auto_comments', new lang_string('generalcomments','backup'), new lang_string('configgeneralcomments','backup'), 1));
     $temp->add(new admin_setting_configcheckbox('backup/backup_auto_badges', new lang_string('generalbadges','backup'), new lang_string('configgeneralbadges','backup'), 1));
index a869ce6..cfb2f21 100644 (file)
@@ -172,7 +172,10 @@ $temp->add(new admin_setting_configselect('tempdatafoldercleanup', new lang_stri
 
 $ADMIN->add('server', $temp);
 
-
+    $temp->add(new admin_setting_configduration('filescleanupperiod',
+        new lang_string('filescleanupperiod', 'admin'),
+        new lang_string('filescleanupperiod_help', 'admin'),
+        86400));
 
 $ADMIN->add('server', new admin_externalpage('environment', new lang_string('environment','admin'), "$CFG->wwwroot/$CFG->admin/environment.php"));
 $ADMIN->add('server', new admin_externalpage('phpinfo', new lang_string('phpinfo'), "$CFG->wwwroot/$CFG->admin/phpinfo.php"));
index 63648a9..47cd54a 100644 (file)
@@ -198,7 +198,7 @@ class models_list implements \renderable, \templatable {
             // Get predictions.
             if (!$onlycli && $modeldata->enabled && !empty($modeldata->timesplitting)) {
                 $urlparams['action'] = 'scheduledanalysis';
-                $url = new \moodle_url('model.php', $urlparams);
+                $url = new \moodle_url('/admin/tool/analytics/model.php', $urlparams);
                 $icon = new \action_menu_link_secondary($url,
                     new \pix_icon('i/notifications', get_string('executescheduledanalysis', 'tool_analytics')),
                     get_string('executescheduledanalysis', 'tool_analytics'));
@@ -225,7 +225,7 @@ class models_list implements \renderable, \templatable {
                 $evaluateparams = [$actionid, $trainedonlyexternally, $modeltimesplittingmethods];
                 $PAGE->requires->js_call_amd('tool_analytics/model', 'selectEvaluationOptions', $evaluateparams);
                 $urlparams['action'] = 'evaluate';
-                $url = new \moodle_url('model.php', $urlparams);
+                $url = new \moodle_url('/admin/tool/analytics/model.php', $urlparams);
                 $icon = new \action_menu_link_secondary($url, new \pix_icon('i/calc', get_string('evaluate', 'tool_analytics')),
                     get_string('evaluate', 'tool_analytics'), ['data-action-id' => $actionid]);
                 $actionsmenu->add($icon);
@@ -234,7 +234,7 @@ class models_list implements \renderable, \templatable {
             // Machine-learning-based models evaluation log.
             if (!$model->is_static() && $model->get_logs()) {
                 $urlparams['action'] = 'log';
-                $url = new \moodle_url('model.php', $urlparams);
+                $url = new \moodle_url('/admin/tool/analytics/model.php', $urlparams);
                 $icon = new \action_menu_link_secondary($url, new \pix_icon('i/report', get_string('viewlog', 'tool_analytics')),
                     get_string('viewlog', 'tool_analytics'));
                 $actionsmenu->add($icon);
@@ -242,7 +242,7 @@ class models_list implements \renderable, \templatable {
 
             // Edit model.
             $urlparams['action'] = 'edit';
-            $url = new \moodle_url('model.php', $urlparams);
+            $url = new \moodle_url('/admin/tool/analytics/model.php', $urlparams);
             $icon = new \action_menu_link_secondary($url, new \pix_icon('t/edit', get_string('edit')), get_string('edit'));
             $actionsmenu->add($icon);
 
@@ -259,7 +259,7 @@ class models_list implements \renderable, \templatable {
                     $icontype = 'i/checked';
                 }
                 $urlparams['action'] = $action;
-                $url = new \moodle_url('model.php', $urlparams);
+                $url = new \moodle_url('/admin/tool/analytics/model.php', $urlparams);
                 $icon = new \action_menu_link_secondary($url, new \pix_icon($icontype, $text), $text);
                 $actionsmenu->add($icon);
             }
@@ -272,7 +272,7 @@ class models_list implements \renderable, \templatable {
 
                 if ($fullysetup || $istrained) {
 
-                    $url = new \moodle_url('model.php', $urlparams);
+                    $url = new \moodle_url('/admin/tool/analytics/model.php', $urlparams);
                     // Clear the previous action param from the URL, we will set it in JS.
                     $url->remove_params('action');
 
@@ -300,7 +300,7 @@ class models_list implements \renderable, \templatable {
             $analyser = $model->get_analyser(['notimesplitting' => true]);
             if (!$analyser instanceof \core_analytics\local\analyser\sitewide) {
                 $urlparams['action'] = 'invalidanalysables';
-                $url = new \moodle_url('model.php', $urlparams);
+                $url = new \moodle_url('/admin/tool/analytics/model.php', $urlparams);
                 $pix = new \pix_icon('i/report', get_string('invalidanalysables', 'tool_analytics'));
                 $icon = new \action_menu_link_secondary($url, $pix, get_string('invalidanalysables', 'tool_analytics'));
                 $actionsmenu->add($icon);
@@ -311,7 +311,7 @@ class models_list implements \renderable, \templatable {
                 $actionid = 'clear-' . $model->get_id();
                 $PAGE->requires->js_call_amd('tool_analytics/model', 'confirmAction', [$actionid, 'clear']);
                 $urlparams['action'] = 'clear';
-                $url = new \moodle_url('model.php', $urlparams);
+                $url = new \moodle_url('/admin/tool/analytics/model.php', $urlparams);
                 $icon = new \action_menu_link_secondary($url, new \pix_icon('e/cleanup_messy_code',
                     get_string('clearpredictions', 'tool_analytics')), get_string('clearpredictions', 'tool_analytics'),
                     ['data-action-id' => $actionid]);
@@ -322,7 +322,7 @@ class models_list implements \renderable, \templatable {
             $actionid = 'delete-' . $model->get_id();
             $PAGE->requires->js_call_amd('tool_analytics/model', 'confirmAction', [$actionid, 'delete']);
             $urlparams['action'] = 'delete';
-            $url = new \moodle_url('model.php', $urlparams);
+            $url = new \moodle_url('/admin/tool/analytics/model.php', $urlparams);
             $icon = new \action_menu_link_secondary($url, new \pix_icon('t/delete',
                 get_string('delete', 'tool_analytics')), get_string('delete', 'tool_analytics'),
                 ['data-action-id' => $actionid]);
index 6ea0d0d..4551e79 100644 (file)
@@ -59,6 +59,9 @@ class predict_models extends \core\task\scheduled_task {
         }
 
         foreach ($models as $model) {
+
+            $renderer = $PAGE->get_renderer('tool_analytics');
+
             $result = $model->predict();
 
             // Reset the page as some indicators may call external functions that overwrite the page context.
@@ -66,7 +69,6 @@ class predict_models extends \core\task\scheduled_task {
 
             if ($result) {
                 echo $OUTPUT->heading(get_string('modelresults', 'tool_analytics', $model->get_name()));
-                $renderer = $PAGE->get_renderer('tool_analytics');
                 echo $renderer->render_get_predictions_results(false, array(), $result, $model->get_analyser()->get_logs());
             }
         }
index 67c0a3a..c9387d3 100644 (file)
@@ -70,6 +70,8 @@ class train_models extends \core\task\scheduled_task {
                 continue;
             }
 
+            $renderer = $PAGE->get_renderer('tool_analytics');
+
             $result = $model->train();
 
             // Reset the page as some indicators may call external functions that overwrite the page context.
@@ -77,8 +79,6 @@ class train_models extends \core\task\scheduled_task {
 
             if ($result) {
                 echo $OUTPUT->heading(get_string('modelresults', 'tool_analytics', $model->get_name()));
-
-                $renderer = $PAGE->get_renderer('tool_analytics');
                 echo $renderer->render_get_predictions_results($result, $model->get_analyser()->get_logs());
             }
         }
index 067320e..601599e 100644 (file)
@@ -102,6 +102,8 @@ if ($options['reuse-prev-analysed']) {
     mtrace(get_string('evaluationinbatches', 'tool_analytics'));
 }
 
+$renderer = $PAGE->get_renderer('tool_analytics');
+
 $analyseroptions = array(
     'filter' => $options['filter'],
     'timesplitting' => $options['analysisinterval'],
@@ -114,7 +116,6 @@ $results = $model->evaluate($analyseroptions);
 // Reset the page as some indicators may call external functions that overwrite the page context.
 \tool_analytics\output\helper::reset_page();
 
-$renderer = $PAGE->get_renderer('tool_analytics');
 echo $renderer->render_evaluate_results($results, $model->get_analyser()->get_logs());
 
 // Check that we have, at leasa,t 1 valid dataset (not necessarily good) to use.
index 72472e5..1de9317 100644 (file)
@@ -440,6 +440,9 @@ class purpose extends persistent {
         }
         if (!empty($data->sensitivedatareasons) && is_array($data->sensitivedatareasons)) {
             $data->sensitivedatareasons = implode(',', $data->sensitivedatareasons);
+        } else {
+            // Nothing selected. Set default value of null.
+            $data->sensitivedatareasons = null;
         }
 
         // A single value.
index f3d1815..6bfea7f 100644 (file)
@@ -71,7 +71,7 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
 
         $expected = array(
             'wwwroot' => $CFG->wwwroot,
-            'httpswwwroot' => $CFG->httpswwwroot,
+            'httpswwwroot' => $CFG->wwwroot,
             'sitename' => external_format_string($SITE->fullname, $context->id, true),
             'guestlogin' => $CFG->guestloginbutton,
             'rememberusername' => $CFG->rememberusername,
index 6477c4d..b5bde99 100644 (file)
@@ -109,14 +109,15 @@ class category_bin extends base_bin {
         require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
 
         // As far as recycle bin is using MODE_AUTOMATED, it observes the backup_auto_storage
-        // setting (storing backups @ real location. For recycle bin we want to ensure that
-        // backup files are always stored in Moodle file area. In order to achieve that, we
-        // hack the setting here via $CFG->forced_plugin_settings, so it won't interfere other
-        // operations.  See MDL-65218 for more information.
+        // settings (storing backups @ real location and potentially not including files).
+        // For recycle bin we want to ensure that backup files are always stored in Moodle file
+        // area and always contain the users' files. In order to achieve that, we hack the
+        // setting here via $CFG->forced_plugin_settings, so it won't interfere other operations.
+        // See MDL-65218 and MDL-35773 for more information.
         // This hack will be removed once recycle bin switches to use its own backup mode, with
-        // own preferences and 100% appart from MODLE_AUTOMATED.
+        // own preferences and 100% separate from MOODLE_AUTOMATED.
         // TODO: Remove this as part of MDL-65228.
-        $CFG->forced_plugin_settings['backup'] = ['backup_auto_storage' => 0];
+        $CFG->forced_plugin_settings['backup'] = ['backup_auto_storage' => 0, 'backup_auto_files' => 1];
 
         // Backup the course.
         $user = get_admin();
index deec65c..0353c2c 100644 (file)
@@ -113,14 +113,15 @@ class course_bin extends base_bin {
         }
 
         // As far as recycle bin is using MODE_AUTOMATED, it observes the backup_auto_storage
-        // setting (storing backups @ real location. For recycle bin we want to ensure that
-        // backup files are always stored in Moodle file area. In order to achieve that, we
-        // hack the setting here via $CFG->forced_plugin_settings, so it won't interfere other
-        // operations.  See MDL-65218 for more information.
+        // settings (storing backups @ real location and potentially not including files).
+        // For recycle bin we want to ensure that backup files are always stored in Moodle file
+        // area and always contain the users' files. In order to achieve that, we hack the
+        // setting here via $CFG->forced_plugin_settings, so it won't interfere other operations.
+        // See MDL-65218 and MDL-35773 for more information.
         // This hack will be removed once recycle bin switches to use its own backup mode, with
-        // own preferences and 100% appart from MODLE_AUTOMATED.
+        // own preferences and 100% separate from MOODLE_AUTOMATED.
         // TODO: Remove this as part of MDL-65228.
-        $CFG->forced_plugin_settings['backup'] = ['backup_auto_storage' => 0];
+        $CFG->forced_plugin_settings['backup'] = ['backup_auto_storage' => 0, 'backup_auto_files' => 1];
 
         // Backup the activity.
         $user = get_admin();
index 76ec813..9ac8f6b 100644 (file)
Binary files a/admin/tool/templatelibrary/amd/build/display.min.js and b/admin/tool/templatelibrary/amd/build/display.min.js differ
index 183fe03..abaf4bd 100644 (file)
Binary files a/admin/tool/templatelibrary/amd/build/display.min.js.map and b/admin/tool/templatelibrary/amd/build/display.min.js.map differ
index 3979366..222e4bd 100644 (file)
@@ -119,7 +119,7 @@ define(['jquery', 'core/ajax', 'core/log', 'core/notification', 'core/templates'
     var loadTemplate = function(templateName) {
         var parts = templateName.split('/');
         var component = parts.shift();
-        var name = parts.shift();
+        var name = parts.join('/');
 
         var promises = ajax.call([{
             methodname: 'core_output_load_template',
index 7ae2b0e..f0d7a43 100644 (file)
@@ -99,13 +99,34 @@ class api {
 
         foreach ($templatedirs as $templatecomponent => $dirs) {
             foreach ($dirs as $dir) {
+                if (!is_dir($dir) || !is_readable($dir)) {
+                    continue;
+                }
+                $dir = realpath($dir);
+
                 // List it.
-                $files = glob($dir . '/*.mustache');
+                $directory = new \RecursiveDirectoryIterator($dir);
+                $files = new \RecursiveIteratorIterator($directory);
 
                 foreach ($files as $file) {
-                    $templatename = basename($file, '.mustache');
-                    if ($search == '' || strpos($templatename, $search) !== false) {
-                        $results[$templatecomponent . '/' . $templatename] = 1;
+                    if (!$file->isFile()) {
+                        continue;
+                    }
+                    $filename = substr($file->getRealpath(), strlen($dir) + 1);
+                    if (strpos($templatecomponent, 'theme_') === 0) {
+                        if (strpos($filename, '/') !== false && strpos($filename, 'local/') !== 0) {
+                            // Skip any template in a sub-directory of a theme which is not in a local directory.
+                            // These are theme overrides of core templates.
+                            // Note: There is a rare edge case where a theme may override a template and then have additional
+                            // dependant templates and these will not be shown.
+                            continue;
+                        }
+                    }
+                    $templatename = str_replace('.mustache', '', $filename);
+                    $componenttemplatename = "{$templatecomponent}/{$templatename}";
+
+                    if ($search == '' || strpos($componenttemplatename, $search) !== false) {
+                        $results[$componenttemplatename] = 1;
                     }
                 }
             }
@@ -152,5 +173,4 @@ class api {
         return $templatestr;
     }
 
-
 }
index f1c72f5..edda9f2 100644 (file)
@@ -105,7 +105,7 @@ class external extends external_api {
     public static function load_canonical_template_parameters() {
         return new external_function_parameters(
                 array('component' => new external_value(PARAM_COMPONENT, 'component containing the template'),
-                      'template' => new external_value(PARAM_ALPHANUMEXT, 'name of the template'))
+                      'template' => new external_value(PARAM_SAFEPATH, 'name of the template'))
             );
     }
 
index c8d1519..625a195 100644 (file)
@@ -21,6 +21,6 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 defined('MOODLE_INTERNAL') || die();
-$plugin->version   = 2019052000; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2019052002; // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2019051100; // Requires this Moodle version.
 $plugin->component = 'tool_templatelibrary'; // Full name of the plugin (used for diagnostics).
index 019da51..ec37326 100644 (file)
@@ -668,7 +668,7 @@ if ($formdata = $mform2->is_cancelled()) {
                     // Check for passwords that we want to force users to reset next
                     // time they log in.
                     $errmsg = null;
-                    $weak = !check_password_policy($user->password, $errmsg);
+                    $weak = !check_password_policy($user->password, $errmsg, $user);
                     if ($resetpasswords == UU_PWRESET_ALL or ($resetpasswords == UU_PWRESET_WEAK and $weak)) {
                         if ($weak) {
                             $weakpasswords++;
@@ -809,7 +809,7 @@ if ($formdata = $mform2->is_cancelled()) {
                     }
                 } else {
                     $errmsg = null;
-                    $weak = !check_password_policy($user->password, $errmsg);
+                    $weak = !check_password_policy($user->password, $errmsg, $user);
                     if ($resetpasswords == UU_PWRESET_ALL or ($resetpasswords == UU_PWRESET_WEAK and $weak)) {
                         if ($weak) {
                             $weakpasswords++;
index 1e4cdbb..df04ed9 100644 (file)
@@ -203,6 +203,21 @@ class helper {
         return $link;
     }
 
+    /**
+     * Get the link used to duplicate the tour.
+     *
+     * @param   int         $tourid     The ID of the tour to duplicate.
+     * @return  moodle_url              The URL.
+     */
+    public static function get_duplicate_tour_link($tourid) {
+        $link = new \moodle_url('/admin/tool/usertours/configure.php', [
+                'action'    => manager::ACTION_DUPLICATETOUR,
+                'id'        => $tourid,
+        ]);
+
+        return $link;
+    }
+
     /**
      * Get the link used to delete the tour.
      *
index 10de685..3611de8 100644 (file)
@@ -138,6 +138,7 @@ class tour_list extends \flexible_table {
 
         $actions[] = helper::format_icon_link($tour->get_view_link(), 't/viewdetails', get_string('view'));
         $actions[] = helper::format_icon_link($tour->get_edit_link(), 't/edit', get_string('edit'));
+        $actions[] = helper::format_icon_link($tour->get_duplicate_link(), 't/copy', get_string('duplicate'));
         $actions[] = helper::format_icon_link($tour->get_export_link(), 't/export',
                 get_string('exporttour', 'tool_usertours'), 'tool_usertours');
         $actions[] = helper::format_icon_link($tour->get_delete_link(), 't/delete', get_string('delete'), null, [
index f29f481..33cd752 100644 (file)
@@ -78,6 +78,11 @@ class manager {
      */
     const ACTION_VIEWTOUR = 'viewtour';
 
+    /**
+     * @var ACTION_DUPLICATETOUR     The action to duplicate the tour.
+     */
+    const ACTION_DUPLICATETOUR = 'duplicatetour';
+
     /**
      * @var ACTION_NEWSTEP The action to create a new step.
      */
@@ -163,6 +168,10 @@ class manager {
                 $this->view_tour(required_param('id', PARAM_INT));
                 break;
 
+            case self::ACTION_DUPLICATETOUR:
+                $this->duplicate_tour(required_param('id', PARAM_INT));
+                break;
+
             case self::ACTION_HIDETOUR:
                 $this->hide_tour(required_param('id', PARAM_INT));
                 break;
@@ -486,6 +495,39 @@ class manager {
         $this->footer();
     }
 
+    /**
+     * Duplicate an existing tour.
+     *
+     * @param   int         $tourid     The ID of the tour to duplicate.
+     */
+    protected function duplicate_tour($tourid) {
+        $tour = helper::get_tour($tourid);
+
+        $export = $tour->to_record();
+        // Remove the id.
+        unset($export->id);
+
+        // Set the version.
+        $export->version = get_config('tool_usertours', 'version');
+
+        $export->name = get_string('duplicatetour_name', 'tool_usertours', $export->name);
+
+        // Step export.
+        $export->steps = [];
+        foreach ($tour->get_steps() as $step) {
+            $record = $step->to_record();
+            unset($record->id);
+            unset($record->tourid);
+
+            $export->steps[] = $record;
+        }
+
+        $exportstring = json_encode($export);
+        $newtour = self::import_tour_from_json($exportstring);
+
+        redirect($newtour->get_view_link());
+    }
+
     /**
      * Show the tour.
      *
index 5717e24..4c4a201 100644 (file)
@@ -356,6 +356,15 @@ class tour {
         return helper::get_export_tour_link($this->id);
     }
 
+    /**
+     * The link to duplicate this tour.
+     *
+     * @return  moodle_url
+     */
+    public function get_duplicate_link() {
+        return helper::get_duplicate_tour_link($this->id);
+    }
+
     /**
      * The link to remove this tour.
      *
index f61d13c..b3b22cf 100644 (file)
@@ -44,6 +44,8 @@ $string['cssselector'] = 'CSS selector';
 $string['defaultvalue'] = 'Default ({$a})';
 $string['delay'] = 'Delay before showing the step';
 $string['done'] = 'Done';
+$string['duplicatetour'] = 'Duplicate tour';
+$string['duplicatetour_name'] = '{$a} (copy)';
 $string['editstep'] = 'Editing "{$a}"';
 $string['tourisenabled'] = 'Tour is enabled';
 $string['enabled'] = 'Enabled';
diff --git a/admin/tool/usertours/tests/behat/duplicate_tour.feature b/admin/tool/usertours/tests/behat/duplicate_tour.feature
new file mode 100644 (file)
index 0000000..8f80853
--- /dev/null
@@ -0,0 +1,24 @@
+@tool @tool_usertours
+Feature: Duplicate a user tour
+  As an administrator
+  I want to duplicate a user tour
+
+  @javascript
+  Scenario: Tour can be duplicated
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | student1 | Student | 1 | student1@example.com |
+    And I log in as "admin"
+    And I add a new user tour with:
+      | Name                | First tour |
+      | Description         | My first tour |
+      | Apply to URL match  | /my/% |
+      | Tour is enabled     | 0 |
+    And I add steps to the "First tour" tour:
+      | targettype                  | Title             | Content |
+      | Display in middle of page   | Welcome           | Welcome to your personal learning space. We'd like to give you a quick tour to show you some of the areas you may find helpful |
+    And I open the User tour settings page
+    And I should see "1" occurrences of "First tour" in the "admintable" "table"
+    And I click on "Duplicate" "link" in the "My first tour" "table_row"
+    And I open the User tour settings page
+    Then I should see "1" occurrences of "First tour (copy)" in the "admintable" "table"
index 32b5abe..ddb9c6f 100644 (file)
@@ -138,6 +138,9 @@ class analysis {
                 }
             }
         }
+
+        // Force GC to clean up the indicator instances used during the last iteration.
+        $this->analyser->instantiate_indicators();
     }
 
     /**
@@ -474,6 +477,11 @@ class analysis {
                 list($samplesfeatures, $newindicatorcalculations, $indicatornotnulls) = $rangeindicator->calculate($sampleids,
                     $this->analyser->get_samples_origin(), $range['start'], $range['end'], $prevcalculations);
 
+                // Free memory ASAP.
+                unset($rangeindicator);
+                gc_collect_cycles();
+                gc_mem_caches();
+
                 // Copy the features data to the dataset.
                 foreach ($samplesfeatures as $analysersampleid => $features) {
 
@@ -503,7 +511,7 @@ class analysis {
                         $indcalc->endtime = $range['end'];
                         $indcalc->sampleid = $sampleid;
                         $indcalc->sampleorigin = $this->analyser->get_samples_origin();
-                        $indcalc->indicator = $rangeindicator->get_id();
+                        $indcalc->indicator = $indicator->get_id();
                         $indcalc->value = $calculatedvalue;
                         $indcalc->timecreated = $timecreated;
                         $newcalculations[] = $indcalc;
index 185f573..0065f15 100644 (file)
@@ -246,6 +246,11 @@ abstract class base {
         foreach ($this->indicators as $key => $indicator) {
             $this->indicators[$key] = call_user_func(array($indicator, 'instance'));
         }
+
+        // Free memory ASAP.
+        gc_collect_cycles();
+        gc_mem_caches();
+
         return $this->indicators;
     }
 
diff --git a/analytics/tests/behat/manage_models.feature b/analytics/tests/behat/manage_models.feature
new file mode 100644 (file)
index 0000000..8a668c9
--- /dev/null
@@ -0,0 +1,163 @@
+@core @core_analytics @javascript
+Feature: Manage analytics models
+  In order to manage analytics models
+  As a manager
+  I need to create and use a model
+
+  Background:
+    Given the following config values are set as admin:
+      | onlycli  | 0 | analytics |
+    And the following "users" exist:
+      | username | firstname | lastname | email                |
+      | teacher1 | Teacher   | 1        | teacher1@example.com |
+      | manager1 | Manager   | 1        | manager1@example.com |
+      | student0 | Student   | 0        | student0@example.com |
+      | student1 | Student   | 1        | student1@example.com |
+      | student2 | Student   | 2        | student2@example.com |
+      | student3 | Student   | 3        | student3@example.com |
+      | student4 | Student   | 4        | student4@example.com |
+      | student5 | Student   | 5        | student5@example.com |
+      | student6 | Student   | 6        | student6@example.com |
+    And the following "system role assigns" exist:
+      | user     | course               | role      |
+      | manager1 | Acceptance test site | manager   |
+    And the following "courses" exist:
+      | fullname | shortname | category | enddate         | startdate        | enablecompletion |
+      | Course 1 | C1        | 0        | ## yesterday ## | ## 2 days ago ## | 1                |
+      | Course 2 | C2        | 0        | ## yesterday ## | ## 2 days ago ## | 1                |
+      | Course 3 | C3        | 0        | ## tomorrow  ## | ## 2 days ago ## | 1                |
+    And the following "course enrolments" exist:
+      | user     | course | role           | timeend         | timestart         |
+      | teacher1 | C1     | editingteacher | ## 1 day ago ## | ## 2 days ago ##  |
+      | student0 | C1     | student        | ## 1 day ago ## | ## 2 days ago ##  |
+      | student1 | C1     | student        | ## 1 day ago ## | ## 2 days ago ##  |
+      | student2 | C1     | student        | ## 1 day ago ## | ## 2 days ago ##  |
+      | teacher1 | C2     | editingteacher | ## 1 day ago ## | ## 2 days ago ##  |
+      | student3 | C2     | student        | ## 1 day ago ## | ## 2 days ago ##  |
+      | student4 | C2     | student        | ## 1 day ago ## | ## 2 days ago ##  |
+      | teacher1 | C3     | editingteacher | 0               | ## 2 days ago ##  |
+      | manager1 | C3     | manager        | 0               | ## 2 days ago ##  |
+      | student5 | C3     | student        | 0               | ## 2 days ago ##  |
+      | student6 | C3     | student        | 0               | ## 2 days ago ##  |
+    And the following "activities" exist:
+      | activity   | name      | intro   | course | idnumber    | section | completion | completionview |
+      | assign     | assign1   | A1 desc | C1     | assign1     | 0       | 2          | 1              |
+      | assign     | assign2   | A2 desc | C2     | assign2     | 0       | 2          | 1              |
+      | assign     | assign3   | A3 desc | C3     | assign3     | 0       | 2          | 1              |
+    And the following "analytics model" exist:
+      | target                                   | indicators                                 | timesplitting                               | enabled |
+      | \core_course\analytics\target\course_completion | \core\analytics\indicator\any_write_action,\core\analytics\indicator\read_actions |  \core\analytics\time_splitting\single_range | true    |
+    And I log in as "manager1"
+    And I navigate to "Analytics > Analytics models" in site administration
+
+  Scenario: Create a model
+    When I click on "New model" "link"
+    And I click on "Create model" "link"
+    And I set the field "Enabled" to "Enable"
+    And I select "__core_course__analytics__target__course_completion" from the "target" singleselect
+    And I open the autocomplete suggestions list
+    And I click on "Read actions amount" item in the autocomplete list
+    And I open the autocomplete suggestions list
+    And I click on "Any write action in the course" item in the autocomplete list
+    And I select "__core__analytics__time_splitting__single_range" from the "timesplitting" singleselect
+    And I press "Save changes"
+    Then I should see "No predictions available yet" in the "Students at risk of not meeting the course completion conditions" "table_row"
+
+  Scenario: Evaluate a model
+    Given I am on "Course 1" course homepage
+    And I navigate to "Course completion" in current page administration
+    And I expand all fieldsets
+    And I set the following fields to these values:
+      | Assignment - assign1 | 1 |
+    And I click on "Save changes" "button"
+    And I am on "Course 2" course homepage
+    And I navigate to "Course completion" in current page administration
+    And I expand all fieldsets
+    And I set the following fields to these values:
+      | Assignment - assign2 | 1 |
+    And I click on "Save changes" "button"
+    And I am on "Course 3" course homepage
+    And I navigate to "Course completion" in current page administration
+    And I expand all fieldsets
+    And I set the following fields to these values:
+      | Assignment - assign3 | 1 |
+    And I click on "Save changes" "button"
+    And I am on site homepage
+    And I navigate to "Analytics > Analytics models" in site administration
+    And I click on "Actions" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    And I click on "Evaluate" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    And I press "Evaluate"
+    And I should see "Evaluate model"
+    And I press "Continue"
+    # Evaluation log
+    And I click on "Actions" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    And I click on "Evaluation log" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    And I should see "Configuration"
+    And I click on "View" "link"
+    And I should see "Log extra info"
+    And I click on "Close" "button"
+    And I click on "Analytics models" "link"
+    # Execute scheduled analysis
+    And I click on "Actions" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    And I click on "Execute scheduled analysis" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    And I should see "Training results"
+    And I press "Continue"
+    # Check notifications
+    Then I should see "1" in the "#nav-notification-popover-container [data-region='count-container']" "css_element"
+    And I open the notification popover
+    And I click on "View full notification" "link" in the ".popover-region-notifications" "css_element"
+    And I should see "Students at risk in Course 3 course"
+    When I am on site homepage
+    And I navigate to "Analytics > Analytics models" in site administration
+    # View predictions
+    When I select "C3" from the "contextid" singleselect
+    #And I click on "#dropdown-3" "css_element"
+    And I click on "Actions" "link" in the "Student 6" "table_row"
+    And I click on "View prediction details" "link"
+    And I should see "Prediction details"
+    And I should see "Any write action"
+    And I should see "Read actions amount"
+    And I click on "Actions" "link"
+    And I click on "Acknowledged" "link"
+    And I click on "Actions" "link"
+    And I click on "View prediction details" "link"
+    And I click on "Actions" "link"
+    And I click on "Not useful" "link"
+    And I should see "No insights reported"
+    # Clear predictions
+    When I am on site homepage
+    And I navigate to "Analytics > Analytics models" in site administration
+    And I should see "No insights reported" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    And I click on "Actions" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    And I click on "Clear predictions" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    And I press "Clear predictions"
+    Then I should see "No predictions available yet" in the "Students at risk of not meeting the course completion conditions" "table_row"
+
+  Scenario: Edit a model
+    When I click on "Actions" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    And I click on "Edit" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    And I click on "Read actions amount" "text" in the ".form-autocomplete-selection" "css_element"
+    And I press "Save changes"
+    And I should not see "Read actions amount"
+
+  Scenario: Disable a model
+    When I click on "Actions" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    And I click on "Disable" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    Then I should see "Disabled model" in the "Students at risk of not meeting the course completion conditions" "table_row"
+
+  Scenario: Export model
+    When I click on "Actions" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    And I click on "Export" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    And I click on "Actions" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    And following "Export" should download between "100" and "500" bytes
+
+  Scenario: Check invalid site elements
+    When I click on "Actions" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    And I click on "Invalid site elements" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    Then I should see "Invalid analysable elements"
+
+  Scenario: Delete model
+    When I click on "Actions" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    And I click on "Delete" "link" in the "Students at risk of not meeting the course completion conditions" "table_row"
+    And I click on "Delete" "button" in the "Confirm" "dialogue"
+    Then I should not see "Students at risk of not meeting the course completion conditions"
index 7034e97..c98048b 100644 (file)
@@ -72,6 +72,8 @@ class login implements renderable, templatable {
     public $username;
     /** @var string The csrf token to limit login to requests that come from the login form. */
     public $logintoken;
+    /** @var string Maintenance message, if Maintenance is enabled. */
+    public $maintenance;
 
     /**
      * Constructor.
@@ -109,6 +111,10 @@ class login implements renderable, templatable {
             $this->instructions = get_string('loginsteps', 'core', 'signup.php');
         }
 
+        if ($CFG->maintenance_enabled == true && !empty($CFG->maintenance_message)) {
+            $this->maintenance = $CFG->maintenance_message;
+        }
+
         // Identity providers.
         $this->identityproviders = \auth_plugin_base::get_identity_providers($authsequence);
         $this->logintoken = \core\session\manager::get_login_token();
@@ -145,6 +151,7 @@ class login implements renderable, templatable {
         $data->signupurl = $this->signupurl->out(false);
         $data->username = $this->username;
         $data->logintoken = $this->logintoken;
+        $data->maintenance = format_text($this->maintenance, FORMAT_MOODLE);
 
         return $data;
     }
index 448c00e..1d1ff6f 100644 (file)
@@ -36,6 +36,7 @@ use core\oauth2\client;
 
 require_once($CFG->libdir.'/authlib.php');
 require_once($CFG->dirroot.'/user/lib.php');
+require_once($CFG->dirroot.'/user/profile/lib.php');
 
 /**
  * Plugin for oauth2 authentication.
index 2169dae..133df3f 100644 (file)
@@ -36,6 +36,7 @@
 
 module.exports = ({ template, types }) => {
     const fs = require('fs');
+    const path = require('path');
     const glob = require('glob');
     const cwd = process.cwd();
 
@@ -72,7 +73,7 @@ module.exports = ({ template, types }) => {
             var rawContents = fs.readFileSync(file);
             var subplugins = JSON.parse(rawContents);
 
-            for (const [component, path] of Object.entries(subplugins)) {
+            for (const [component, path] of Object.entries(subplugins.plugintypes)) {
                 if (path) {
                     moodlePlugins[path] = component;
                 }
@@ -92,7 +93,7 @@ module.exports = ({ template, types }) => {
      */
     function getModuleNameFromFileName(searchFileName) {
         searchFileName = fs.realpathSync(searchFileName);
-        const relativeFileName = searchFileName.replace(`${cwd}/`, '');
+        const relativeFileName = searchFileName.replace(`${cwd}${path.sep}`, '').replace(/\\/g, '/');
         const [componentPath, file] = relativeFileName.split('/amd/src/');
         const fileName = file.replace('.js', '');
 
@@ -202,4 +203,4 @@ module.exports = ({ template, types }) => {
             }
         }
     };
-};
\ No newline at end of file
+};
index e0606d1..5090f39 100644 (file)
@@ -150,6 +150,12 @@ if (!async_helper::is_async_pending($id, 'course', 'backup')) {
     $loghtml = '';
     if ($backup->get_stage() == backup_ui::STAGE_FINAL) {
 
+        // Before we perform the backup check settings to see if user
+        // or setting defaults are set to exclude files from the backup.
+        if ($backup->get_setting_value('files') == 0) {
+            $renderer->set_samesite_notification();
+        }
+
         if ($backupmode != backup::MODE_ASYNC) {
             // Synchronous backup handling.
 
@@ -180,6 +186,7 @@ if (!async_helper::is_async_pending($id, 'course', 'backup')) {
             // Hide the progress display and first backup step bar (the 'finished' step will show next).
             echo html_writer::end_div();
             echo html_writer::script('document.getElementById("executionprogress").style.display = "none";');
+
         } else {
             // Async backup handling.
             $backup->get_controller()->finish_ui();
@@ -203,6 +210,7 @@ if (!async_helper::is_async_pending($id, 'course', 'backup')) {
                     'restoreurl' => $restoreurl->out(),
                     'headingident' => 'backup'
             );
+
             echo $renderer->render_from_template('core/async_backup_status', $progresssetup);
         }
 
index 9ac5f5b..cf5fcf9 100644 (file)
@@ -271,6 +271,37 @@ class backup_controller extends base_controller {
         return $this->includefiles;
     }
 
+    /**
+     * Returns the default value for $this->includefiles before we consider any settings.
+     *
+     * @return bool
+     * @throws dml_exception
+     */
+    protected function get_include_files_default() : bool {
+        // We normally include files.
+        $includefiles = true;
+
+        // In an import, we don't need to include files.
+        if ($this->get_mode() === backup::MODE_IMPORT) {
+            $includefiles = false;
+        }
+
+        // When a backup is intended for the same site, we don't need to include the files.
+        // Note, this setting is only used for duplication of an entire course.
+        if ($this->get_mode() === backup::MODE_SAMESITE) {
+            $includefiles = false;
+        }
+
+        // If backup is automated and we have set auto backup config to exclude
+        // files then set them to be excluded here.
+        $backupautofiles = (bool) get_config('backup', 'backup_auto_files');
+        if ($this->get_mode() === backup::MODE_AUTOMATED && !$backupautofiles) {
+            $includefiles = false;
+        }
+
+        return $includefiles;
+    }
+
     public function get_operation() {
         return $this->operation;
     }
@@ -326,6 +357,12 @@ class backup_controller extends base_controller {
         // Basic/initial prevention against time/memory limits
         core_php_time_limit::raise(1 * 60 * 60); // 1 hour for 1 course initially granted
         raise_memory_limit(MEMORY_EXTRA);
+
+        // If the controller has decided that we can include files, then check the setting, otherwise do not include files.
+        if ($this->get_include_files()) {
+            $this->set_include_files((bool) $this->get_plan()->get_setting('files')->get_value());
+        }
+
         // If this is not a course backup, or single activity backup (e.g. duplicate) inform the plan we are not
         // including all the activities for sure. This will affect any
         // task/step executed conditionally to stop including information
@@ -386,35 +423,19 @@ class backup_controller extends base_controller {
         $this->log('applying plan defaults', backup::LOG_DEBUG);
         backup_controller_dbops::apply_config_defaults($this);
         $this->set_status(backup::STATUS_CONFIGURED);
-        $this->set_include_files();
+        $this->set_include_files($this->get_include_files_default());
     }
 
     /**
      * Set the initial value for the include_files setting.
      *
+     * @param bool $includefiles
      * @see backup_controller::get_include_files for further information on the purpose of this setting.
-     * @return int Indicates whether files should be included in backups.
      */
-    protected function set_include_files() {
-        // We normally include files.
-        $includefiles = true;
-
-        // In an import, we don't need to include files.
-        if ($this->get_mode() === backup::MODE_IMPORT) {
-            $includefiles = false;
-        }
-
-        // When a backup is intended for the same site, we don't need to include the files.
-        // Note, this setting is only used for duplication of an entire course.
-        if ($this->get_mode() === backup::MODE_SAMESITE) {
-            $includefiles = false;
-        }
-
-        $this->includefiles = (int) $includefiles;
+    protected function set_include_files(bool $includefiles) {
         $this->log("setting file inclusion to {$this->includefiles}", backup::LOG_DEBUG);
-        return $this->includefiles;
+        $this->includefiles = (int) $includefiles;
     }
-
 }
 
 /*
index 03e989d..ee434c1 100644 (file)
@@ -110,6 +110,12 @@ class backup_root_task extends backup_task {
         $this->add_setting($blocks);
         $this->converter_deps($blocks, $converters);
 
+        // Define files.
+        $files = new backup_generic_setting('files', base_setting::IS_BOOLEAN, true);
+        $files->set_ui(new backup_setting_ui_checkbox($files, get_string('rootsettingfiles', 'backup')));
+        $this->add_setting($files);
+        $this->converter_deps($files, $converters);
+
         // Define filters
         $filters = new backup_generic_setting('filters', base_setting::IS_BOOLEAN, true);
         $filters->set_ui(new backup_setting_ui_checkbox($filters, get_string('rootsettingfilters', 'backup')));
index 615f802..d3a650b 100644 (file)
@@ -201,7 +201,8 @@ abstract class backup_plan_dbops extends backup_dbops {
     * @param bool $useidonly only use the ID in the file name
     * @return string The filename to use
     */
-    public static function get_default_backup_filename($format, $type, $id, $users, $anonymised, $useidonly = false) {
+    public static function get_default_backup_filename($format, $type, $id, $users, $anonymised,
+            $useidonly = false, $files = true) {
         global $DB;
 
         // Calculate backup word
@@ -251,6 +252,11 @@ abstract class backup_plan_dbops extends backup_dbops {
             $info = '-an';
         }
 
+        // Indicate if backup doesn't contain files.
+        if (!$files) {
+            $info .= '-nf';
+        }
+
         return $backupword . '-' . $format . '-' . $type . '-' .
                $name . '-' . $date . $info . '.mbz';
     }
index 7ef9117..0911c68 100644 (file)
@@ -1054,17 +1054,40 @@ abstract class restore_dbops {
                     // Create the file in the filepool if it does not exist yet.
                     if (!$fs->file_exists($newcontextid, $component, $filearea, $rec->newitemid, $file->filepath, $file->filename)) {
 
-                        // Even if a file has been deleted since the backup was made, the file metadata will remain in the
-                        // files table, and the file will not be moved to the trashdir.
-                        // Files are not cleared from the files table by cron until several days after deletion.
+                        // Even if a file has been deleted since the backup was made, the file metadata may remain in the
+                        // files table, and the file will not yet have been moved to the trashdir. e.g. a draft file version.
+                        // Try to recover from file table first.
                         if ($foundfiles = $DB->get_records('files', array('contenthash' => $file->contenthash), '', '*', 0, 1)) {
                             // Only grab one of the foundfiles - the file content should be the same for all entries.
                             $foundfile = reset($foundfiles);
                             $fs->create_file_from_storedfile($file_record, $foundfile->id);
                         } else {
-                            // A matching existing file record was not found in the database.
-                            $results[] = self::get_missing_file_result($file);
-                            continue;
+                            $filesystem = $fs->get_file_system();
+                            $restorefile = $file;
+                            $restorefile->contextid = $newcontextid;
+                            $restorefile->itemid = $rec->newitemid;
+                            $storedfile = new stored_file($fs, $restorefile);
+
+                            // Ok, let's try recover this file.
+                            // 1. We check if the file can be fetched locally without attempting to fetch
+                            //    from the trash.
+                            // 2. We check if we can get the remote filepath for the specified stored file.
+                            // 3. We check if the file can be fetched from the trash.
+                            // 4. All failed, say we couldn't find it.
+                            if ($filesystem->is_file_readable_locally_by_storedfile($storedfile)) {
+                                $localpath = $filesystem->get_local_path_from_storedfile($storedfile);
+                                $fs->create_file_from_pathname($file, $localpath);
+                            } else if ($filesystem->is_file_readable_remotely_by_storedfile($storedfile)) {
+                                $url = $filesystem->get_remote_path_from_storedfile($storedfile);
+                                $fs->create_file_from_url($file, $url);
+                            } else if ($filesystem->is_file_readable_locally_by_storedfile($storedfile, true)) {
+                                $localpath = $filesystem->get_local_path_from_storedfile($storedfile, true);
+                                $fs->create_file_from_pathname($file, $localpath);
+                            } else {
+                                // A matching file was not found.
+                                $results[] = self::get_missing_file_result($file);
+                                continue;
+                            }
                         }
                     }
                 }
index 980baf4..68ff822 100644 (file)
@@ -415,8 +415,9 @@ abstract class backup_cron_automated_helper {
             $id = $bc->get_id();
             $users = $bc->get_plan()->get_setting('users')->get_value();
             $anonymised = $bc->get_plan()->get_setting('anonymize')->get_value();
+            $incfiles = (bool)$config->backup_auto_files;
             $bc->get_plan()->get_setting('filename')->set_value(backup_plan_dbops::get_default_backup_filename($format, $type,
-                    $id, $users, $anonymised));
+                    $id, $users, $anonymised, false, $incfiles));
 
             $bc->set_status(backup::STATUS_AWAITING);
 
index fa3bc79..167eae7 100644 (file)
@@ -287,7 +287,15 @@ abstract class backup_helper {
             $config = get_config('backup');
             $dir = $config->backup_auto_destination;
             if ($config->backup_auto_storage == 1 and $dir and is_dir($dir) and is_writable($dir)) {
-                $filedest = $dir.'/'.backup_plan_dbops::get_default_backup_filename($format, $backuptype, $courseid, $hasusers, $isannon, !$config->backup_shortname);
+                $filedest = $dir.'/'
+                        .backup_plan_dbops::get_default_backup_filename(
+                                $format,
+                                $backuptype,
+                                $courseid,
+                                $hasusers,
+                                $isannon,
+                                !$config->backup_shortname,
+                                (bool)$config->backup_auto_files);
                 // first try to move the file, if it is not possible copy and delete instead
                 if (@rename($filepath, $filedest)) {
                     return null;
index 1e1caf8..aff941b 100644 (file)
@@ -155,7 +155,9 @@ class backup_ui_stage_initial extends backup_ui_stage {
                             $this->ui->get_type(),
                             $this->ui->get_controller_id(),
                             $this->ui->get_setting_value('users'),
-                            $this->ui->get_setting_value('anonymize')
+                            $this->ui->get_setting_value('anonymize'),
+                            false,
+                            (bool)$this->ui->get_setting_value('files')
                         );
                         $setting->set_value($filename);
                     }
@@ -457,7 +459,16 @@ class backup_ui_stage_confirmation extends backup_ui_stage {
                         $id = $this->ui->get_controller_id();
                         $users = $this->ui->get_setting_value('users');
                         $anonymised = $this->ui->get_setting_value('anonymize');
-                        $setting->set_value(backup_plan_dbops::get_default_backup_filename($format, $type, $id, $users, $anonymised));
+                        $files = (bool)$this->ui->get_setting_value('files');
+                        $filename = backup_plan_dbops::get_default_backup_filename(
+                                $format,
+                                $type,
+                                $id,
+                                $users,
+                                $anonymised,
+                                false,
+                                $files);
+                        $setting->set_value($filename);
                     }
                     $form->add_setting($setting, $task);
                     break;
@@ -628,6 +639,7 @@ class backup_ui_stage_complete extends backup_ui_stage_final {
         if (!empty($this->results['missing_files_in_pool'])) {
             $output .= $renderer->notification(get_string('missingfilesinpool', 'backup'), 'notifyproblem');
         }
+        $output .= $renderer->get_samesite_notification();
         $output .= $renderer->notification(get_string('executionsuccess', 'backup'), 'notifysuccess');
         $output .= $renderer->continue_button($restorerul);
         $output .= $renderer->box_end();
index 4ee83e9..a46499f 100644 (file)
@@ -43,6 +43,13 @@ require_once($CFG->dirroot . '/backup/moodle2/backup_plan_builder.class.php');
  */
 class core_backup_renderer extends plugin_renderer_base {
 
+    /**
+     * Same site notification display.
+     *
+     * @var string
+     */
+    private $samesitenotification = '';
+
     /**
      * Renderers a progress bar for the backup or restore given the items that make it up.
      *
@@ -80,6 +87,22 @@ class core_backup_renderer extends plugin_renderer_base {
         return $out;
     }
 
+    /**
+     * Set the same site backup notification.
+     *
+     */
+    public function set_samesite_notification() {
+        $this->samesitenotification = $this->output->notification(get_string('samesitenotification', 'backup'), 'info');
+    }
+
+    /**
+     * Get the same site backup notification.
+     *
+     */
+    public function get_samesite_notification() {
+        return $this->samesitenotification;
+    }
+
     /**
      * Prints a dependency notification
      *
index 57e14e5..f58ff47 100644 (file)
Binary files a/blocks/myoverview/amd/build/view.min.js and b/blocks/myoverview/amd/build/view.min.js differ
index 8529c62..ea5cedd 100644 (file)
Binary files a/blocks/myoverview/amd/build/view.min.js.map and b/blocks/myoverview/amd/build/view.min.js.map differ
index 0dc37ba..3e53ea0 100644 (file)
@@ -67,7 +67,7 @@ function(
         NOCOURSES: 'core_course/no-courses'
     };
 
-    var NUMCOURSES_PERPAGE = [12, 24, 48];
+    var NUMCOURSES_PERPAGE = [12, 24, 48, 96, 0];
 
     var loadedPages = [];
 
@@ -421,19 +421,24 @@ function(
     var initializePagedContent = function(root) {
         namespace = "block_myoverview_" + root.attr('id') + "_" + Math.random();
 
-        var itemsPerPage = NUMCOURSES_PERPAGE;
         var pagingLimit = parseInt(root.find(Selectors.courseView.region).attr('data-paging'), 10);
-        if (pagingLimit) {
-            itemsPerPage = NUMCOURSES_PERPAGE.map(function(value) {
-                var active = false;
-                if (value == pagingLimit) {
-                    active = true;
-                }
+        var itemsPerPage = NUMCOURSES_PERPAGE.map(function(value) {
+            var active = false;
+            if (value == pagingLimit) {
+                active = true;
+            }
+
+            return {
+                value: value,
+                active: active
+            };
+        });
 
-                return {
-                    value: value,
-                    active: active
-                };
+        // Filter out all pagination options which are too large for the amount of courses user is enrolled in.
+        var totalCourseCount = parseInt(root.find(Selectors.courseView.region).attr('data-totalcoursecount'), 10);
+        if (totalCourseCount) {
+            itemsPerPage = itemsPerPage.filter(function(pagingOption) {
+                return pagingOption.value < totalCourseCount;
             });
         }
 
@@ -448,7 +453,7 @@ function(
 
                 pagesData.forEach(function(pageData) {
                     var currentPage = pageData.pageNumber;
-                    var limit = pageData.limit;
+                    var limit = (pageData.limit > 0) ? pageData.limit : 0;
 
                     // Reset local variables if limits have changed
                     if (lastLimit != limit) {
@@ -491,7 +496,7 @@ function(
                             }
                         } else {
                             nextPageStart = pageData.limit;
-                            pageCourses = courses.slice(0, pageData.limit);
+                            pageCourses = (pageData.limit > 0) ? courses.slice(0, pageData.limit) : courses;
                         }
 
                         // Finished setting up the current page
@@ -500,7 +505,7 @@ function(
                         };
 
                         // Set up the next page
-                        var remainingCourses = courses.slice(nextPageStart, courses.length);
+                        var remainingCourses = nextPageStart ? courses.slice(nextPageStart, courses.length) : [];
                         if (remainingCourses.length) {
                             loadedPages[currentPage + 1] = {
                                 courses: remainingCourses
index 8df2b47..e1c92a9 100644 (file)
@@ -94,7 +94,11 @@ class main implements renderable, templatable {
     public function __construct($grouping, $sort, $view, $paging) {
         $this->grouping = $grouping ? $grouping : BLOCK_MYOVERVIEW_GROUPING_ALL;
         $this->sort = $sort ? $sort : BLOCK_MYOVERVIEW_SORTING_TITLE;
-        $this->paging = $paging ? $paging : BLOCK_MYOVERVIEW_PAGING_12;
+        if ($paging == BLOCK_MYOVERVIEW_PAGING_ALL) {
+            $this->paging = BLOCK_MYOVERVIEW_PAGING_ALL;
+        } else {
+            $this->paging = $paging ? $paging : BLOCK_MYOVERVIEW_PAGING_12;
+        }
 
         $config = get_config('block_myoverview');
         if (!$config->displaycategories) {
@@ -178,8 +182,11 @@ class main implements renderable, templatable {
      *
      * @param \renderer_base $output
      * @return array Context variables for the template
+     * @throws \coding_exception
+     *
      */
     public function export_for_template(renderer_base $output) {
+        global $USER;
 
         $nocoursesurl = $output->image_url('courses', 'block_myoverview')->out();
 
@@ -187,6 +194,7 @@ class main implements renderable, templatable {
         $availablelayouts = $this->get_formatted_available_layouts_for_export();
 
         $defaultvariables = [
+            'totalcoursecount' => count(enrol_get_all_users_courses($USER->id, true)),
             'nocoursesimg' => $nocoursesurl,
             'grouping' => $this->grouping,
             'sort' => $this->sort == BLOCK_MYOVERVIEW_SORTING_TITLE ? 'fullname' : 'ul.timeaccess desc',
index 652177f..7acbaff 100644 (file)
@@ -54,6 +54,8 @@ define('BLOCK_MYOVERVIEW_VIEW_SUMMARY', 'summary');
 define('BLOCK_MYOVERVIEW_PAGING_12', 12);
 define('BLOCK_MYOVERVIEW_PAGING_24', 24);
 define('BLOCK_MYOVERVIEW_PAGING_48', 48);
+define('BLOCK_MYOVERVIEW_PAGING_96', 96);
+define('BLOCK_MYOVERVIEW_PAGING_ALL', 0);
 
 /**
  * Constants for the admin category display setting
@@ -115,7 +117,9 @@ function block_myoverview_user_preferences() {
         'choices' => array(
             BLOCK_MYOVERVIEW_PAGING_12,
             BLOCK_MYOVERVIEW_PAGING_24,
-            BLOCK_MYOVERVIEW_PAGING_48
+            BLOCK_MYOVERVIEW_PAGING_48,
+            BLOCK_MYOVERVIEW_PAGING_96,
+            BLOCK_MYOVERVIEW_PAGING_ALL
         )
     );
 
index 951cef9..5be3c98 100644 (file)
@@ -35,6 +35,7 @@
     data-prev-display="{{view}}"
     data-paging="{{paging}}"
     data-nocoursesimg="{{nocoursesimg}}"
+    data-totalcoursecount="{{totalcoursecount}}"
     data-displaycategories="{{displaycategories}}">
     <div data-region="course-view-content">
         {{> block_myoverview/placeholders }}
index 4135c0a..6e0e107 100644 (file)
@@ -38,20 +38,20 @@ Feature: The my overview block allows users to persistence of their page limits
 
   Scenario: Toggle the page limit between page reloads
     Given I log in as "student1"
-    When I click on "Show 12 items per page" "button" in the "Course overview" "block"
-    And I click on "24" "link"
-    Then I should see "Course 9"
+    When I click on "[data-toggle='dropdown']" "css_element" in the "Course overview" "block"
+    And I click on "All" "link"
+    Then I should see "Course 13"
     And I reload the page
-    Then I should see "Course 9"
-    And I should see "24" in the "[data-action='limit-toggle']" "css_element"
+    Then I should see "Course 13"
+    And I should see "All" in the "[data-action='limit-toggle']" "css_element"
     And I log out
 
   Scenario: Toggle the page limit between grouping changes
     Given I log in as "student1"
-    When I click on "Show 12 items per page" "button" in the "Course overview" "block"
-    And I click on "24" "link"
+    When I click on "[data-toggle='dropdown']" "css_element" in the "Course overview" "block"
+    And I click on "All" "link"
     And I click on "All" "button" in the "Course overview" "block"
     And I click on "In progress" "link" in the "Course overview" "block"
-    Then I should see "Course 9"
-    And I should see "24" in the "[data-action='limit-toggle']" "css_element"
+    Then I should see "Course 13"
+    And I should see "All" in the "[data-action='limit-toggle']" "css_element"
     And I log out
index 25b7194..4c93e24 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2019060400;         // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2019070400;         // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2019051100;         // Requires this Moodle version.
 $plugin->component = 'block_myoverview'; // Full name of the plugin (used for diagnostics).
index 0dc8b87..b5f8133 100644 (file)
@@ -1,3 +1,32 @@
+{{!
+    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/>.
+}}
+{{!
+    @template block_search_forums/search_form
+
+    This template renders the search form.
+
+    Example context (json):
+    {
+        "actionurl": "https://domain.example/mod/forum/search.php",
+        "courseid": "2",
+        "advancedsearchurl": "https://domain.example/mod/forum/search.php?id=2",
+        "helpicon": "<a class='btn'><i class='icon fa fa-question-circle'></i></a>"
+    }
+}}
 <div class="searchform">
     <form action="{{actionurl}}" class="form-inline">
         <input type="hidden" name="id" value="{{courseid}}">
index 765122a..2ae6011 100644 (file)
@@ -1,3 +1,32 @@
+{{!
+    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/>.
+}}
+{{!
+    @template block_settings/search_form
+
+    This template renders the search form.
+
+    Example context (json):
+    {
+        "action": "https://domain.example/admin/search.php",
+        "label": "Search in settings",
+        "searchvalue": "Find this setting",
+        "quote": "Search"
+    }
+}}
 <form method="get" action="{{action}}" class="adminsearchform form-inline" role="search">
     <div class="form-group">
         <label class="sr-only" for="adminsearchquery">{{label}}</label>
index ac73012..5e4ae97 100644 (file)
@@ -620,6 +620,12 @@ $CFG->admin = 'admin';
 //
 //      $CFG->debugsessionlock = 5;
 //
+// Uninstall plugins from CLI only. This stops admins from uninstalling plugins from the graphical admin
+// user interface, and forces plugins to be uninstalled from the Command Line tool only, found at
+// admin/cli/plugin_uninstall.php.
+//
+//      $CFG->uninstallclionly = true;
+//
 //=========================================================================
 // 7. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
 //=========================================================================
index e5382f8..9622e70 100644 (file)
@@ -1289,16 +1289,36 @@ function course_module_flag_for_async_deletion($cmid) {
  * Checks whether the given course has any course modules scheduled for adhoc deletion.
  *
  * @param int $courseid the id of the course.
+ * @param bool $onlygradable whether to check only gradable modules or all modules.
  * @return bool true if the course contains any modules pending deletion, false otherwise.
  */
-function course_modules_pending_deletion($courseid) {
+function course_modules_pending_deletion(int $courseid, bool $onlygradable = false) : bool {
     if (empty($courseid)) {
         return false;
     }
+
+    if ($onlygradable) {
+        // Fetch modules with grade items.
+        if (!$coursegradeitems = grade_item::fetch_all(['itemtype' => 'mod', 'courseid' => $courseid])) {
+            // Return early when there is none.
+            return false;
+        }
+    }
+
     $modinfo = get_fast_modinfo($courseid);
     foreach ($modinfo->get_cms() as $module) {
         if ($module->deletioninprogress == '1') {
-            return true;
+            if ($onlygradable) {
+                // Check if the module being deleted is in the list of course modules with grade items.
+                foreach ($coursegradeitems as $coursegradeitem) {
+                    if ($coursegradeitem->itemmodule == $module->modname && $coursegradeitem->iteminstance == $module->instance) {
+                        // The module being deleted is within the gradable  modules.
+                        return true;
+                    }
+                }
+            } else {
+                return true;
+            }
         }
     }
     return false;
index c2e729a..d709096 100644 (file)
     You should have received a copy of the GNU General Public License
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
+{{!
+    @template core_course/course_search_form
+
+    This template renders the search form.
+
+    Example context (json):
+    {
+        "searchurl": "https://domain.example/course/search.php",
+        "id": "coursesearch",
+        "inputid": "coursesearchbox",
+        "inputsize": "30",
+        "value": "Find in course",
+        "helpicon": "<a class='btn'><i class='icon fa fa-question-circle'></i></a>"
+    }
+}}
 <form action="{{searchurl}}" id="{{id}}" method="get" class="form-inline">
     <fieldset class="coursesearchbox invisiblefieldset">
         <label for="{{inputid}}">{{#str}}searchcourses{{/str}}</label>
index 4a6c032..7f5482f 100644 (file)
@@ -6511,4 +6511,50 @@ class core_course_courselib_testcase extends advanced_testcase {
         $result = course_get_course_dates_for_user_ids($course, [$user2->id]);
         $this->assertEquals($user2start, $result[$user2->id]['start']);
     }
+
+    /**
+     * Data provider for test_course_modules_pending_deletion.
+     *
+     * @return array An array of arrays contain test data
+     */
+    public function provider_course_modules_pending_deletion() {
+        return [
+            'Non-gradable activity, check all'              => [['forum'], 0, false, true],
+            'Gradable activity, check all'                  => [['assign'], 0, false, true],
+            'Non-gradable activity, check gradables'        => [['forum'], 0, true, false],
+            'Gradable activity, check gradables'            => [['assign'], 0, true, true],
+            'Non-gradable within multiple, check all'       => [['quiz', 'forum', 'assign'], 1, false, true],
+            'Non-gradable within multiple, check gradables' => [['quiz', 'forum', 'assign'], 1, true, false],
+            'Gradable within multiple, check all'           => [['quiz', 'forum', 'assign'], 2, false, true],
+            'Gradable within multiple, check gradables'     => [['quiz', 'forum', 'assign'], 2, true, true],
+        ];
+    }
+
+    /**
+     * Tests the function course_modules_pending_deletion.
+     *
+     * @param string[] $modules A complete list aff all available modules before deletion
+     * @param int $indextodelete The index of the module in the $modules array that we want to test with
+     * @param bool $gradable The value to pass to the gradable argument of the course_modules_pending_deletion function
+     * @param bool $expected The expected result
+     * @dataProvider provider_course_modules_pending_deletion
+     */
+    public function test_course_modules_pending_deletion(array $modules, int $indextodelete, bool $gradable, bool $expected) {
+        $this->resetAfterTest();
+
+        // Ensure recyclebin is enabled.
+        set_config('coursebinenable', true, 'tool_recyclebin');
+
+        // Create course and modules.
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+
+        $moduleinstances = [];
+        foreach ($modules as $module) {
+            $moduleinstances[] = $generator->create_module($module, array('course' => $course->id));
+        }
+
+        course_delete_module($moduleinstances[$indextodelete]->cmid, true); // Try to delete the instance asynchronously.
+        $this->assertEquals($expected, course_modules_pending_deletion($course->id, $gradable));
+    }
 }
index f9f4cf4..8c358b2 100644 (file)
@@ -87,6 +87,9 @@ if ($mform->is_cancelled()) {
     redirect($returnurl);
 
 } else if ($data = $mform->get_data()) {
+    if ($data->duration && $data->timeend == 0) {
+        $data->timeend = $data->timestart + $data->duration;
+    }
     if ($manager->edit_enrolment($ue, $data)) {
         redirect($returnurl);
     }
index d1ee2ec..f90e733 100644 (file)
@@ -22,6 +22,8 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+use core_enrol\enrol_helper;
+
 defined('MOODLE_INTERNAL') || die();
 
 require_once("$CFG->libdir/formslib.php");
@@ -34,6 +36,9 @@ class enrol_user_enrolment_form extends moodleform {
         $instancename = $this->_customdata['enrolinstancename'];
         $modal = !empty($this->_customdata['modal']);
 
+        $periodmenu = enrol_get_period_list();
+        $duration = enrol_calculate_duration($ue->timestart, $ue->timeend);
+
         $mform->addElement('static', 'enrolmentmethod', get_string('enrolmentmethod', 'enrol'), $instancename);
 
         $options = array(ENROL_USER_ACTIVE    => get_string('participationactive', 'enrol'),
@@ -44,6 +49,11 @@ class enrol_user_enrolment_form extends moodleform {
 
         $mform->addElement('date_time_selector', 'timestart', get_string('enroltimestart', 'enrol'), array('optional' => true));
 
+        $mform->addElement('select', 'duration', get_string('enrolperiod', 'enrol'), $periodmenu);
+        $mform->setDefault('duration', $duration);
+        $mform->disabledIf('duration', 'timestart[enabled]', 'notchecked', 1);
+        $mform->disabledIf('duration', 'timeend[enabled]', 'checked', 1);
+
         $mform->addElement('date_time_selector', 'timeend', get_string('enroltimeend', 'enrol'), array('optional' => true));
 
         $mform->addElement('static', 'timecreated', get_string('enroltimecreated', 'enrol'), userdate($ue->timecreated));
index 3f182fd..5378aae 100644 (file)
@@ -32,6 +32,7 @@ require_once($CFG->dirroot.'/enrol/locallib.php');
 require_once($CFG->dirroot.'/group/lib.php');
 require_once($CFG->dirroot.'/enrol/manual/locallib.php');
 require_once($CFG->dirroot.'/cohort/lib.php');
+require_once($CFG->dirroot . '/enrol/manual/classes/enrol_users_form.php');
 
 $id      = required_param('id', PARAM_INT); // Course id.
 $action  = required_param('action', PARAM_ALPHANUMEXT);
@@ -94,6 +95,7 @@ switch ($action) {
         $duration = optional_param('duration', 0, PARAM_INT);
         $startdate = optional_param('startdate', 0, PARAM_INT);
         $recovergrades = optional_param('recovergrades', 0, PARAM_INT);
+        $timeend = optional_param_array('timeend', [], PARAM_INT);
 
         if (empty($roleid)) {
             $roleid = null;
@@ -122,12 +124,25 @@ switch ($action) {
                 $timestart = $today;
                 break;
         }
-        if ($duration <= 0) {
+        if ($timeend) {
+            $timeend = make_timestamp($timeend['year'], $timeend['month'], $timeend['day'], $timeend['hour'], $timeend['minute']);
+        } else if ($duration <= 0) {
             $timeend = 0;
         } else {
             $timeend = $timestart + $duration;
         }
 
+        $mform = new enrol_manual_enrol_users_form(null, (object)["context" => $context]);
+        $userenroldata = [
+                'startdate' => $timestart,
+                'timeend' => $timeend,
+        ];
+        $mform->set_data($userenroldata);
+        $validationerrors = $mform->validation($userenroldata, null);
+        if (!empty($validationerrors)) {
+            throw new enrol_ajax_exception('invalidenrolduration');
+        }
+
         $instances = $manager->get_enrolment_instances();
         $plugins = $manager->get_enrolment_plugins(true); // Do not allow actions on disabled plugins.
         if (!array_key_exists($enrolid, $instances)) {
index 165a6e9..b7fa663 100644 (file)
@@ -60,14 +60,7 @@ class enrol_manual_enrol_users_form extends moodleform {
         $mform = $this->_form;
         $mform->setDisableShortforms();
         $mform->disable_form_change_checker();
-        // Build the list of options for the enrolment period dropdown.
-        $unlimitedperiod = get_string('unlimited');
-        $periodmenu = array();
-        $periodmenu[''] = $unlimitedperiod;
-        for ($i=1; $i<=365; $i++) {
-            $seconds = $i * 86400;
-            $periodmenu[$seconds] = get_string('numdays', '', $i);
-        }
+        $periodmenu = enrol_get_period_list();
         // Work out the apropriate default settings.
         $defaultperiod = $instance->enrolperiod;
         if ($instance->enrolperiod > 0 && !isset($periodmenu[$instance->enrolperiod])) {
@@ -131,13 +124,15 @@ class enrol_manual_enrol_users_form extends moodleform {
         $mform->addElement('checkbox', 'recovergrades', get_string('recovergrades', 'enrol'));
         $mform->setAdvanced('recovergrades');
         $mform->setDefault('recovergrades', $CFG->recovergradesdefault);
-        $mform->addElement('select', 'duration', get_string('defaultperiod', 'enrol_manual'), $periodmenu);
-        $mform->setDefault('duration', $defaultperiod);
-        $mform->setAdvanced('duration');
         $mform->addElement('select', 'startdate', get_string('startingfrom'), $basemenu);
         $mform->setDefault('startdate', $extendbase);
         $mform->setAdvanced('startdate');
-
+        $mform->addElement('select', 'duration', get_string('enrolperiod', 'enrol'), $periodmenu);
+        $mform->setDefault('duration', $defaultperiod);
+        $mform->setAdvanced('duration');
+        $mform->disabledIf('duration', 'timeend[enabled]', 'checked', 1);
+        $mform->addElement('date_time_selector', 'timeend', get_string('enroltimeend', 'enrol'), ['optional' => true]);
+        $mform->setAdvanced('timeend');
         $mform->addElement('hidden', 'id', $course->id);
         $mform->setType('id', PARAM_INT);
         $mform->addElement('hidden', 'action', 'enrol');
@@ -145,4 +140,22 @@ class enrol_manual_enrol_users_form extends moodleform {
         $mform->addElement('hidden', 'enrolid', $instance->id);
         $mform->setType('enrolid', PARAM_INT);
     }
+
+    /**
+     * Validate the submitted form data.
+     *
+     * @param array $data array of ("fieldname"=>value) of submitted data
+     * @param array $files array of uploaded files "element_name"=>tmp_file_path
+     * @return array of "element_name"=>"error_description" if there are errors,
+     *         or an empty array if everything is OK (true allowed for backwards compatibility too).
+     */
+    public function validation($data, $files) {
+        $errors = parent::validation($data, $files);
+        if (!empty($data['startdate']) && !empty($data['timeend'])) {
+            if ($data['startdate'] >= $data['timeend']) {
+                $errors['timeend'] = get_string('enroltimeendinvalid', 'enrol');
+            }
+        }
+        return $errors;
+    }
 }
index 9eb8758..0df80cf 100644 (file)
@@ -29,6 +29,7 @@ $enrolid      = required_param('enrolid', PARAM_INT);
 $roleid       = optional_param('roleid', -1, PARAM_INT);
 $extendperiod = optional_param('extendperiod', 0, PARAM_INT);
 $extendbase   = optional_param('extendbase', 0, PARAM_INT);
+$timeend      = optional_param_array('timeend', [], PARAM_INT);
 
 $instance = $DB->get_record('enrol', array('id'=>$enrolid, 'enrol'=>'manual'), '*', MUST_EXIST);
 $course = $DB->get_record('course', array('id'=>$instance->courseid), '*', MUST_EXIST);
@@ -135,7 +136,10 @@ if ($canenrol && optional_param('add', false, PARAM_BOOL) && confirm_sesskey())
                     break;
             }
 
-            if ($extendperiod <= 0) {
+            if ($timeend) {
+                $timeend = make_timestamp($timeend['year'], $timeend['month'], $timeend['day'], $timeend['hour'],
+                        $timeend['minute']);
+            } else if ($extendperiod <= 0) {
                 $timeend = 0;
             } else {
                 $timeend = $timestart + $extendperiod;
index 2afa506..e44169e 100644 (file)
@@ -108,11 +108,11 @@ Feature: Teacher can search and enrol users one by one into the course
       | student098  | Student   | 098      | student098@example.com  |
       | student099  | Student   | 099      | student099@example.com  |
     And the following "courses" exist:
-      | fullname    | shortname |
-      | Course 001  | C001      |
+      | fullname   | shortname | format | startdate       |
+      | Course 001 | C001      | weeks  | ##1 month ago## |
     And the following "course enrolments" exist:
-      | user        | course    | role            |
-      | teacher001  | C001      | editingteacher  |
+      | user       | course | role           | timestart       |
+      | teacher001 | C001   | editingteacher | ##1 month ago## |
     And I log in as "teacher001"
     And I am on "Course 001" course homepage
 
@@ -178,3 +178,48 @@ Feature: Teacher can search and enrol users one by one into the course
     When I set the field "Select users" to "student100@example.com"
     And I click on ".form-autocomplete-downarrow" "css_element" in the "Select users" "form_row"
     Then I should see "student100@example.com, 1234567892, 1234567893, ABC1, ABC2"
+
+# The following tests are commented out as a result of MDL-66339.
+#  @javascript
+#  Scenario: Enrol user from participants page
+#    Given I navigate to course participants
+#    # Enrol user to course
+#    And I press "Enrol users"
+#    And I set the field "Select users" to "example.com"
+#    And I expand the "Select users" autocomplete
+#    When I click on "Student 099" item in the autocomplete list
+#    Then I should see "Student 099" in the list of options for the "Select users" autocomplete
+#    And I click on "Show more" "button"
+#    # Fill data to input duration
+#    And "input[name='timeend[enabled]'][checked=checked]" "css_element" should not exist
+#    And the "Enrolment duration" "select" should be enabled
+#    And I set the field "duration" to "2"
+#    # Fill data to input end time
+#    And I set the field "Starting from" to "2"
+#    And I set the field "timeend[enabled]" to "1"
+#    And I set the field "timeend[day]" to "10"
+#    And the "Enrolment duration" "select" should be disabled
+#    And I click on "Enrol users" "button" in the "Enrol users" "dialogue"
+#    And I am on "Course 001" course homepage
+#    And I navigate to course participants
+#    And I should see "Student 099" in the "participants" "table"
+#    And I click on "Edit enrolment" "icon" in the "Student 099" "table_row"
+#    And the field "timeend[day]" matches value "10"
+#
+#  @javascript
+#  Scenario: Update Enrol user
+#    Given I am on "Course 001" course homepage
+#    And I navigate to course participants
+#    When I click on "Edit enrolment" "icon" in the "Teacher 001" "table_row"
+#    Then the "Enrolment duration" "select" should be enabled
+#    # Fill duration
+#    And "input[name='timeend[enabled]'][checked=checked]" "css_element" should not exist
+#    And the "Enrolment duration" "select" should be enabled
+#    And I set the field "duration" to "2"
+#    # Fill end time
+#    And I set the field "timeend[enabled]" to "1"
+#    And I set the field "timeend[day]" to "28"
+#    And the "Enrolment duration" "select" should be disabled
+#    And I press "Save changes"
+#    And I click on "Edit enrolment" "icon" in the "Teacher 001" "table_row"
+#    And the field "timeend[day]" matches value "28"
index 801f176..e44cef6 100644 (file)
@@ -1080,4 +1080,23 @@ class core_enrollib_testcase extends advanced_testcase {
         $this->assertArrayHasKey($roles['student'], $return[$user2->id]);
         $this->assertArrayNotHasKey($roles['teacher'], $return[$user2->id]);
     }
+
+    /**
+     * Test enrol_calculate_duration function
+     */
+    public function test_enrol_calculate_duration() {
+        // Start time 07/01/2019 @ 12:00am (UTC).
+        $timestart = 1561939200;
+        // End time 07/05/2019 @ 12:00am (UTC).
+        $timeend = 1562284800;
+        $duration = enrol_calculate_duration($timestart, $timeend);
+        $durationinday = $duration / DAYSECS;
+        $this->assertEquals(4, $durationinday);
+
+        // End time 07/10/2019 @ 12:00am (UTC).
+        $timeend = 1562716800;
+        $duration = enrol_calculate_duration($timestart, $timeend);
+        $durationinday = $duration / DAYSECS;
+        $this->assertEquals(9, $durationinday);
+    }
 }
index 7a97661..67857cb 100644 (file)
@@ -986,7 +986,7 @@ function print_grade_page_head($courseid, $active_type, $active_plugin=null,
 
     // Put a warning on all gradebook pages if the course has modules currently scheduled for background deletion.
     require_once($CFG->dirroot . '/course/lib.php');
-    if (course_modules_pending_deletion($courseid)) {
+    if (course_modules_pending_deletion($courseid, true)) {
         \core\notification::add(get_string('gradesmoduledeletionpendingwarning', 'grades'),
             \core\output\notification::NOTIFY_WARNING);
     }
index 90e8b70..15cd130 100644 (file)
@@ -1,3 +1,45 @@
+{{!
+    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/>.
+}}
+{{!
+    @template gradereport_history/user_button
+
+    Template which defines a forum post for sending in a single-post HTML email.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+        "name": "showreport",
+        "label": "Select users",
+        "value": "1",
+        "classes": "singlebutton selectusersbutton gradereport_history_plugin",
+        "method": "get",
+        "url": "/grade/report/history/index.php",
+        "formid": "selectusersbutton{generated}",
+        "params": "true",
+        "id": "single_button{generated}",
+        "tooltip": "Select users",
+        "disabled": ""
+    }
+}}
 <div class="{{classes}}">
     <div method="{{method}}" action="{{url}}" id="{{formid}}">
         {{#params}}
index abe592a..82ce32f 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template gradereport_singleview/bulk_insert
+
     Bulk insert attribute.
+
+    Example context (json):
+    {
+        "applyname": "Apply-name",
+        "applylabel": "Perform bulk insert",
+        "label": "Bulk insert",
+        "menuname": "Menu-name",
+        "menulabel": "For",
+        "menuoptions": "JSON object",
+        "value": "all",
+        "selected": "",
+        "name": "All grades",
+        "valuename": "Value-name",
+        "valuelabel": "Insert value",
+        "valuefield": "PARSED HTML"
+    }
 }}
 <div class="enable">
     <input type="checkbox" name="{{applyname}}" value="1" id="{{applyname}}">
index 43fdbcf..5d43733 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template gradereport_singleview/button
+
     Button.
+
+    Example context (json):
+    {
+        "type": "submit",
+        "value": "Save"
+    }
 }}
 <input type="{{type}}" value={{#quote}}{{value}}{{/quote}} class="btn btn-secondary">
index 9f3be1a..436c685 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template gradereport_singleview/dropdown_attribute
+
     Dropdown attribute.
+
+    Example context (json):
+    {
+        "name": "Awesome-test",
+        "disabled": "true",
+        "options": "true",
+        "value": "1",
+        "selected": "true"
+    }
 }}
 <select id="{{name}}" name="{{name}}" class="custom-select" tabindex="1" {{#disabled}}disabled{{/disabled}}>
     {{#options}}
index ffd06e9..6c73b9c 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template gradereport_singleview/text_attribute
+
     Text attribute.
+
+    Example context (json):
+    {
+        "name": "Awesome-report",
+        "label": "Text label",
+        "value": "Text information",
+        "tabindex": "1",
+        "disabled": "true"
+    }
 }}
 <label for="{{name}}" class="accesshide">{{label}}</label>
 <input id="{{name}}" name="{{name}}" type="text" value="{{value}}" class="form-control" {{#tabindex}}tabindex="{{.}}"{{/tabindex}} {{#disabled}}disabled{{/disabled}}>
index 129e286..5c4c539 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template core_grades/edit_tree
+
     Edit tree.
+
+    Example context (json):
+    {
+        "actionurl": "https://domain.example/grade/edit/tree/index.php?id=4",
+        "sesskey": "fakesesskey",
+        "notification": "",
+        "table": "<table class='generaltable simple setup-grades' id='grade_edit_tree_table'><thead> <tr><th>Name</th><th>Weights</th><th>Max grade</th><th>Actions</th> </tr></thead><tbody></tbody></table>",
+        "showsave": "true",
+        "showbulkmove": "true",
+        "bulkmoveoptions": "{'value' : '1', 'name' : 'Test grade category'}",
+        "value": "1",
+        "name": "Test grade category"
+    }
 }}
 
 <form id="gradetreeform" method="post" action="{{actionurl}}">
index 3ee641b..8b9c114 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template core_grades/weight_field
+
     Weight field.
+
+    Example context (json):
+    {
+        "itemname": "Awesome test",
+        "id": "2",
+        "value": "100"
+    }
 }}
 <label class="accesshide" for="weight_{{id}}">
     {{#str}}extracreditvalue, grades, {{itemname}}{{/str}}
index 0ed8292..5d3cffd 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    Weight field.
+    @template core_grades/weight_override_field
+
+    Weight override field.
+
+    Example context (json):
+    {
+        "itemname": "Awesome test",
+        "id": "2",
+        "checked": "true",
+        "value": "100"
+    }
 }}
 <div class="form-inline">
     <label class="accesshide" for="weightoverride_{{id}}">
index 973f833..d2c2f50 100644 (file)
@@ -160,7 +160,6 @@ $CFG->lang                 = $config->lang;
 $CFG->dirroot              = __DIR__;
 $CFG->libdir               = "$CFG->dirroot/lib";
 $CFG->wwwroot              = install_guess_wwwroot(); // can not be changed - ppl must use the real address when installing
-$CFG->httpswwwroot         = $CFG->wwwroot;
 $CFG->dataroot             = $config->dataroot;
 $CFG->tempdir              = $CFG->dataroot.'/temp';
 $CFG->backuptempdir        = $CFG->tempdir.'/backup';
index c87bb86..9815b4e 100644 (file)
@@ -40,6 +40,6 @@ $string['cliunknowoption'] = 'Nieznana opcja:
   {$a}
 Proszę użyć pomocy.';
 $string['cliyesnoprompt'] = 'wpisz y (czyli tak) lub n (czyli nie)';
-$string['environmentrequireinstall'] = 'jest niezbędnę, żeby było zainstalowane/włączone';
+$string['environmentrequireinstall'] = 'jest niezbędne, aby było zainstalowane i włączone';
 $string['environmentrequireversion'] = 'wersja {$a->needed} jest niezbędna a ty używasz wersji {$a->current}';
 $string['upgradekeyset'] = 'Zaktualizuj klucz (pozostaw puste jeśli nie jest to skonfigurowane)';
index 1a0a888..ac5810a 100644 (file)
@@ -564,6 +564,8 @@ $string['experimentalsettings'] = 'Experimental settings';
 $string['extendedusernamechars'] = 'Allow extended characters in usernames';
 $string['extramemorylimit'] = 'Extra PHP memory limit';
 $string['fatalsessionautostart'] = '<p>Serious configuration error detected, please notify server administrator.</p><p> To operate properly, Moodle requires that administrator changes PHP settings.</p><p><code>session.auto_start</code> must be set to <code>off</code>.</p><p>This setting is controlled by editing <code>php.ini</code>, Apache/IIS <br />configuration or <code>.htaccess</code> file on the server.</p>';
+$string['filescleanupperiod'] = 'Clean trash pool files';
+$string['filescleanupperiod_help'] = 'How often trash files are removed. These are files that are associated with a context that no longer exists';
 $string['fileconversioncleanuptask'] = 'Cleanup of temporary records for file conversions.';
 $string['filecreated'] = 'New file created';
 $string['filestoredin'] = 'Save file into folder :';
@@ -1412,10 +1414,6 @@ $string['cachesessionhelp'] = 'User specific cache that expires when the user\'s
 $string['cacheapplication'] = 'Application cache';
 $string['cacheapplicationhelp'] = 'Cached items are shared among all users and expire by a determined time to live (ttl).';
 
-// Deprecated since Moodle 3.3.
-$string['loginpasswordautocomplete'] = 'Prevent password autocompletion on login form';
-$string['loginpasswordautocomplete_help'] = 'If enabled, users are not allowed to save their account password in their browser.';
-$string['unoconvwarning'] = 'The version of unoconv you have installed is not supported.';
 // Deprecated since Moodle 3.4
 $string['moodleorghubname'] = 'Moodle';
 $string['hubs'] = 'Hubs';
index de681c6..9cfe05e 100644 (file)
@@ -127,6 +127,7 @@ $string['configgeneralblocks'] = 'Sets the default for including blocks in a bac
 $string['configgeneralcalendarevents'] = 'Sets the default for including calendar events in a backup.';
 $string['configgeneralcomments'] = 'Sets the default for including comments in a backup.';
 $string['configgeneralcompetencies'] = 'Sets the default for including competencies in a backup.';
+$string['configgeneralfiles'] = 'Sets the default for including files in a backup.';
 $string['configgeneralfilters'] = 'Sets the default for including filters in a backup.';
 $string['configgeneralhistories'] = 'Sets the default for including user history within a backup.';
 $string['configgenerallogs'] = 'If enabled logs will be included in backups by default.';
@@ -202,6 +203,7 @@ $string['generalcalendarevents'] = 'Include calendar events';
 $string['generalcomments'] = 'Include comments';
 $string['generalcompetencies'] = 'Include competencies';
 $string['generalenrolments'] = 'Include enrolment methods';
+$string['generalfiles'] = 'Include files';
 $string['generalfilters'] = 'Include filters';
 $string['generalhistories'] = 'Include histories';
 $string['generalgradehistories'] = 'Include histories';
@@ -328,6 +330,7 @@ $string['rootsettingbadges'] = 'Include badges';
 $string['rootsettingblocks'] = 'Include blocks';
 $string['rootsettingcompetencies'] = 'Include competencies';
 $string['rootsettingfilters'] = 'Include filters';
+$string['rootsettingfiles'] = 'Include files';
 $string['rootsettingcomments'] = 'Include comments';
 $string['rootsettingcalendarevents'] = 'Include calendar events';
 $string['rootsettinguserscompletion'] = 'Include user completion details';
@@ -337,6 +340,7 @@ $string['rootsettinggradehistories'] = 'Include grade history';
 $string['rootsettinggroups'] = 'Include groups and groupings';
 $string['rootsettingimscc1'] = 'Convert to IMS Common Cartridge 1.0';
 $string['rootsettingimscc11'] = 'Convert to IMS Common Cartridge 1.1';
+$string['samesitenotification'] = 'This backup was created with only references to files, not the files themselves. Restoring will only work on this site.';
 $string['sitecourseformatwarning'] = 'This is a front page backup, note that they can only be restored on the front page';
 $string['storagecourseonly'] = 'Course backup filearea';
 $string['storagecourseandexternal'] = 'Course backup filearea and the specified directory';
index 6e4bf66..7b54068 100644 (file)
@@ -555,17 +555,6 @@ $string['version_help'] = 'The version field may be used to keep track of the ba
 $string['warnexpired'] = ' (This badge has expired!)';
 $string['year'] = 'Year(s)';
 
-// Deprecated since Moodle 3.3.
-$string['personaconnection'] = 'Sign in with your email';
-$string['personaconnection_help'] = 'Persona is a system for identifying yourself across the web, using an email address that you own. The Open Badges backpack uses Persona as a login system, so to be able to connect to a backpack you will need a Persona account.
-
-For more information about Persona visit <a href="https://login.persona.org/about">https://login.persona.org/about</a>.';
-$string['error:backpackloginfailed'] = 'You could not be connected to an external backpack for the following reason: {$a}';
-$string['error:badjson'] = 'The connection attempt returned invalid data.';
-$string['error:noassertion'] = 'No assertion was returned by Persona. You may have closed the dialog before completing the login process.';
-$string['error:personaneedsjs'] = 'Currently, Javascript is required to connect to your backpack. If you can, enable Javascript and reload the page.';
-$string['signinwithyouremail'] = 'Sign in with your email';
-
 // Deprecated since Moodle 3.6.
 $string['error:invalidbadgeurl'] = 'Invalid issuer URL format. The URL should have a prefix http:// or https://.';
 $string['backpackbadges'] = 'You have {$a->totalbadges} badge(s) displayed from {$a->totalcollections} collection(s). <a href="mybackpack.php">Change backpack settings</a>.';
index 94894df..c0cf86b 100644 (file)
@@ -231,7 +231,3 @@ $string['withconditions'] = 'With conditions';
 $string['writingcompletiondata'] = 'Writing completion data';
 $string['xdays'] = '{$a} days';
 $string['yourprogress'] = 'Your progress';
-
-// Deprecated since Moodle 3.3.
-$string['completion-title-manual-n'] = 'Mark as complete: {$a}';
-$string['completion-title-manual-y'] = 'Mark as not complete: {$a}';
index 14eafe9..f28f2b2 100644 (file)
@@ -5,17 +5,6 @@ myfilesmanage,core
 mypreferences,core_grades
 myprofile,core
 viewallmyentries,core_blog
-loginpasswordautocomplete,core_admin
-loginpasswordautocomplete_help,core_admin
-deletecomment,core
-personaconnection,core_badges
-personaconnection_help,core_badges
-error:noassertion,core_badges
-error:personaneedsjs,core_badges
-error:badjson,core_badges
-error:backpackloginfailed,core_badges
-signinwithyouremail,core_badges
-sectionusedefaultname,core
 registermoochtips,core_hub
 monthnext,core_access
 monthprev,core_access
@@ -160,3 +149,4 @@ nobackpackcollections,core_badges
 error:nogroups,core_badges
 purgedefinitionsuccess,core_cache
 purgestoresuccess,core_cache
+eventrolecapabilitiesupdated,core_role
\ No newline at end of file
index ea1bb8e..bdb64e4 100644 (file)
@@ -96,6 +96,7 @@ $string['instanceadded'] = 'Method added';
 $string['instanceeditselfwarning'] = 'Warning:';
 $string['instanceeditselfwarningtext'] = 'You are enrolled into this course through this enrolment method, changes may affect your access to this course.';
 $string['invalidenrolinstance'] = 'Invalid enrolment instance';
+$string['invalidenrolduration'] = 'Invalid enrolment duration';
 $string['invalidrole'] = 'Invalid role';
 $string['invalidrequest'] = 'Invalid request';
 $string['manageenrols'] = 'Manage enrol plugins';
index e7b4d21..21389c4 100644 (file)
@@ -39,6 +39,7 @@ $string['blockuserconfirm'] = 'Are you sure you want to block {$a}?';
 $string['blockuserconfirmbutton'] = 'Block';
 $string['blocknoncontacts'] = 'Prevent non-contacts from messaging me';
 $string['cancelselection'] = 'Cancel message selection';
+$string['cantblockuser'] = 'You are unable to block {$a} because they have a role with permission to message all users';
 $string['contactableprivacy'] = 'Accept messages from:';
 $string['contactableprivacy_onlycontacts'] = 'My contacts only';
 $string['contactableprivacy_coursemember'] = 'My contacts and anyone in my courses';
index 9a9d2cb..25e9c45 100644 (file)
@@ -467,6 +467,8 @@ $string['defaultcoursesummary'] = 'Write a concise and interesting paragraph her
 $string['defaultcourseteacher'] = 'Teacher';
 $string['defaultcourseteacherdescription'] = 'Teachers can do anything within a course, including changing the activities and grading students.';
 $string['defaultcourseteachers'] = 'Teachers';
+$string['defaulthomepageuser'] = 'Default home page';
+$string['defaulthomepageuser_help'] = 'This determines the home page for your account';
 $string['delete'] = 'Delete';
 $string['deleteablock'] = 'Delete a block';
 $string['deleteall'] = 'Delete all';
@@ -2195,10 +2197,6 @@ $string['yourwordforx'] = 'Your word for \'{$a}\'';
 $string['zippingbackup'] = 'Zipping backup';
 $string['deprecatedeventname'] = '{$a} (no longer in use)';
 
-// Deprecated since Moodle 3.3.
-$string['deletecomment'] = 'Delete this comment';
-$string['sectionusedefaultname'] = 'Use default section name';
-
 // Deprecated since Moodle 3.4.
 $string['publish'] = 'Publish';
 $string['extendenrol'] = 'Extend enrolment (individual)';
index a01ff27..ead0c9e 100644 (file)
@@ -225,14 +225,16 @@ $string['errorbadroleshortname'] = 'Incorrect role short name';
 $string['errorexistsrolename'] = 'Role name already exists';
 $string['errorexistsroleshortname'] = 'Role name already exists';
 $string['errorroleshortnametoolong'] = 'The short name must not exceed 100 characters';
+$string['eventcapabilityassigned'] = 'Capability assigned';
+$string['eventcapabilityunassigned'] = 'Capability unassigned';
 $string['eventroleallowassignupdated'] = 'Allow role assignment';
 $string['eventroleallowoverrideupdated'] = 'Allow role override';
 $string['eventroleallowswitchupdated'] = 'Allow role switch';
 $string['eventroleallowviewupdated'] = 'Allow role view';
 $string['eventroleassigned'] = 'Role assigned';
-$string['eventrolecapabilitiesupdated'] = 'Role capabilities updated';
 $string['eventroledeleted'] = 'Role deleted';
 $string['eventroleunassigned'] = 'Role unassigned';
+$string['eventroleupdated'] = 'Role updated';
 $string['existingadmins'] = 'Current site administrators';
 $string['existingusers'] = '{$a} existing users';
 $string['explanation'] = 'Explanation';
@@ -495,3 +497,6 @@ $string['privacy:metadata:role_capabilities:tableexplanation'] = 'The capabiliti
 $string['privacy:metadata:role_capabilities:timemodified'] = 'The date when the capability was created or modified.';
 $string['privacy:metadata:role_cohortroles'] = 'Roles to cohort';
 $string['course:togglecompletion'] = 'Manually mark activities as complete';
+
+// Deprecated since Moodle 3.8.
+$string['eventrolecapabilitiesupdated'] = 'Role capabilities updated';
\ No newline at end of file
index 856c556..2383769 100644 (file)
@@ -1379,6 +1379,18 @@ function assign_capability($capability, $permission, $roleid, $contextid, $overw
         }
     }
 
+    // Trigger capability_assigned event.
+    \core\event\capability_assigned::create([
+        'userid' => $cap->modifierid,
+        'context' => $context,
+        'objectid' => $roleid,
+        'other' => [
+            'capability' => $capability,
+            'oldpermission' => $existing->permission ?? CAP_INHERIT,
+            'permission' => $permission
+        ]
+    ])->trigger();
+
     // Reset any cache of this role, including MUC.
     accesslib_clear_role_cache($roleid);
 
@@ -1394,7 +1406,7 @@ function assign_capability($capability, $permission, $roleid, $contextid, $overw
  * @return boolean true or exception
  */
 function unassign_capability($capability, $roleid, $contextid = null) {
-    global $DB;
+    global $DB, $USER;
 
     // Capability must exist.
     if (!$capinfo = get_capability_info($capability)) {
@@ -1413,6 +1425,16 @@ function unassign_capability($capability, $roleid, $contextid = null) {
         $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid));
     }
 
+    // Trigger capability_assigned event.
+    \core\event\capability_unassigned::create([
+        'userid' => $USER->id,
+        'context' => $context ?? context_system::instance(),
+        'objectid' => $roleid,
+        'other' => [
+            'capability' => $capability,
+        ]
+    ])->trigger();
+
     // Reset any cache of this role, including MUC.
     accesslib_clear_role_cache($roleid);
 
index 45b9918..e8b3649 100644 (file)
@@ -2583,6 +2583,19 @@ class admin_setting_confightmleditor extends admin_setting_configtextarea {
         $editor->use_editor($this->get_id(), array('noclean'=>true));
         return parent::output_html($data, $query);
     }
+
+    /**
+     * Checks if data has empty html.
+     *
+     * @param string $data
+     * @return string Empty when no errors.
+     */
+    public function write_setting($data) {
+        if (trim(html_to_text($data)) === '') {
+            $data = '';
+        }
+        return parent::write_setting($data);
+    }
 }
 
 
index 1c24ad2..2d588c1 100644 (file)
Binary files a/lib/amd/build/adapter.min.js and b/lib/amd/build/adapter.min.js differ
index abfd827..7ae1a5b 100644 (file)
Binary files a/lib/amd/build/adapter.min.js.map and b/lib/amd/build/adapter.min.js.map differ
index c6a2fc1..549a653 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js and b/lib/amd/build/form-autocomplete.min.js differ
index c37d9a4..db0aeaf 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js.map and b/lib/amd/build/form-autocomplete.min.js.map differ
index 88b5569..7988e3d 100644 (file)
Binary files a/lib/amd/build/paged_content_factory.min.js and b/lib/amd/build/paged_content_factory.min.js differ
index e525b43..a3c86aa 100644 (file)
Binary files a/lib/amd/build/paged_content_factory.min.js.map and b/lib/amd/build/paged_content_factory.min.js.map differ
index 1affa27..a2dcac9 100644 (file)
Binary files a/lib/amd/build/templates.min.js and b/lib/amd/build/templates.min.js differ
index 801067e..88c4bb0 100644 (file)
Binary files a/lib/amd/build/templates.min.js.map and b/lib/amd/build/templates.min.js.map differ
index 282f7b2..13a5850 100644 (file)
 
 define([], function() {
 return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
-/*
- *  Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
- *
- *  Use of this source code is governed by a BSD-style license
- *  that can be found in the LICENSE file in the root of the source
- *  tree.
- */
- /* eslint-env node */
-'use strict';
-
-var SDPUtils = require('sdp');
-
-function writeMediaSection(transceiver, caps, type, stream, dtlsRole) {
-    var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps);
-
-    // Map ICE parameters (ufrag, pwd) to SDP.
-    sdp += SDPUtils.writeIceParameters(
-        transceiver.iceGatherer.getLocalParameters());
-
-    // Map DTLS parameters to SDP.
-    sdp += SDPUtils.writeDtlsParameters(
-        transceiver.dtlsTransport.getLocalParameters(),
-        type === 'offer' ? 'actpass' : dtlsRole || 'active');
-
-    sdp += 'a=mid:' + transceiver.mid + '\r\n';
-
-    if (transceiver.rtpSender && transceiver.rtpReceiver) {
-        sdp += 'a=sendrecv\r\n';
-    } else if (transceiver.rtpSender) {
-        sdp += 'a=sendonly\r\n';
-    } else if (transceiver.rtpReceiver) {
-        sdp += 'a=recvonly\r\n';
-    } else {
-        sdp += 'a=inactive\r\n';
-    }
-
-    if (transceiver.rtpSender) {
-        var trackId = transceiver.rtpSender._initialTrackId ||
-            transceiver.rtpSender.track.id;
-        transceiver.rtpSender._initialTrackId = trackId;
-        // spec.
-        var msid = 'msid:' + (stream ? stream.id : '-') + ' ' +
-            trackId + '\r\n';
-        sdp += 'a=' + msid;
-        // for Chrome. Legacy should no longer be required.
-        sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
-            ' ' + msid;
-
-        // RTX
-        if (transceiver.sendEncodingParameters[0].rtx) {
-            sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
-                ' ' + msid;
-            sdp += 'a=ssrc-group:FID ' +
-                transceiver.sendEncodingParameters[0].ssrc + ' ' +
-                transceiver.sendEncodingParameters[0].rtx.ssrc +
-                '\r\n';
-        }
-    }
-    // FIXME: this should be written by writeRtpDescription.
-    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
-        ' cname:' + SDPUtils.localCName + '\r\n';
-    if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) {
-        sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
-            ' cname:' + SDPUtils.localCName + '\r\n';
-    }
-    return sdp;
-}
-
-// Edge does not like
-// 1) stun: filtered after 14393 unless ?transport=udp is present
-// 2) turn: that does not have all of turn:host:port?transport=udp
-// 3) turn: with ipv6 addresses
-// 4) turn: occurring muliple times
-function filterIceServers(iceServers, edgeVersion) {
-    var hasTurn = false;
-    iceServers = JSON.parse(JSON.stringify(iceServers));
-    return iceServers.filter(function(server) {
-        if (server && (server.urls || server.url)) {
-            var urls = server.urls || server.url;
-            if (server.url && !server.urls) {
-                console.warn('RTCIceServer.url is deprecated! Use urls instead.');
-            }
-            var isString = typeof urls === 'string';
-            if (isString) {
-                urls = [urls];
-            }
-            urls = urls.filter(function(url) {
-                var validTurn = url.indexOf('turn:') === 0 &&
-                    url.indexOf('transport=udp') !== -1 &&
-                    url.indexOf('turn:[') === -1 &&
-                    !hasTurn;
-
-                if (validTurn) {
-                    hasTurn = true;
-                    return true;
-                }
-                return url.indexOf('stun:') === 0 && edgeVersion >= 14393 &&
-                    url.indexOf('?transport=udp') === -1;
-            });
-
-            delete server.url;
-            server.urls = isString ? urls[0] : urls;
-            return !!urls.length;
-        }
-    });
-}
-
-// Determines the intersection of local and remote capabilities.
-function getCommonCapabilities(localCapabilities, remoteCapabilities) {
-    var commonCapabilities = {
-        codecs: [],
-        headerExtensions: [],
-        fecMechanisms: []
-    };
-
-    var findCodecByPayloadType = function(pt, codecs) {
-        pt = parseInt(pt, 10);
-        for (var i = 0; i < codecs.length; i++) {
-            if (codecs[i].payloadType === pt ||
-                codecs[i].preferredPayloadType === pt) {
-                return codecs[i];
-            }
-        }
-    };
-
-    var rtxCapabilityMatches = function(lRtx, rRtx, lCodecs, rCodecs) {
-        var lCodec = findCodecByPayloadType(lRtx.parameters.apt, lCodecs);
-        var rCodec = findCodecByPayloadType(rRtx.parameters.apt, rCodecs);
-        return lCodec && rCodec &&
-            lCodec.name.toLowerCase() === rCodec.name.toLowerCase();
-    };
-
-    localCapabilities.codecs.forEach(function(lCodec) {
-        for (var i = 0; i < remoteCapabilities.codecs.length; i++) {
-            var rCodec = remoteCapabilities.codecs[i];
-            if (lCodec.name.toLowerCase() === rCodec.name.toLowerCase() &&
-                lCodec.clockRate === rCodec.clockRate) {
-                if (lCodec.name.toLowerCase() === 'rtx' &&
-                    lCodec.parameters && rCodec.parameters.apt) {
-                    // for RTX we need to find the local rtx that has a apt
-                    // which points to the same local codec as the remote one.
-                    if (!rtxCapabilityMatches(lCodec, rCodec,
-                        localCapabilities.codecs, remoteCapabilities.codecs)) {
-                        continue;
-                    }
-                }
-                rCodec = JSON.parse(JSON.stringify(rCodec)); // deepcopy
-                // number of channels is the highest common number of channels
-                rCodec.numChannels = Math.min(lCodec.numChannels,
-                    rCodec.numChannels);
-                // push rCodec so we reply with offerer payload type
-                commonCapabilities.codecs.push(rCodec);
-
-                // determine common feedback mechanisms
-                rCodec.rtcpFeedback = rCodec.rtcpFeedback.filter(function(fb) {
-                    for (var j = 0; j < lCodec.rtcpFeedback.length; j++) {
-                        if (lCodec.rtcpFeedback[j].type === fb.type &&
-                            lCodec.rtcpFeedback[j].parameter === fb.parameter) {
-                            return true;
-                        }
-                    }
-                    return false;
-                });
-                // FIXME: also need to determine .parameters
-                //  see https://github.com/openpeer/ortc/issues/569
-                break;
-            }
-        }
-    });
-
-    localCapabilities.headerExtensions.forEach(function(lHeaderExtension) {
-        for (var i = 0; i < remoteCapabilities.headerExtensions.length;
-             i++) {
-            var rHeaderExtension = remoteCapabilities.headerExtensions[i];
-            if (lHeaderExtension.uri === rHeaderExtension.uri) {
-                commonCapabilities.headerExtensions.push(rHeaderExtension);
-                break;
-            }
-        }
-    });
-
-    // FIXME: fecMechanisms
-    return commonCapabilities;
-}
-
-// is action=setLocalDescription with type allowed in signalingState
-function isActionAllowedInSignalingState(action, type, signalingState) {
-    return {
-        offer: {
-            setLocalDescription: ['stable', 'have-local-offer'],
-            setRemoteDescription: ['stable', 'have-remote-offer']
-        },
-        answer: {
-            setLocalDescription: ['have-remote-offer', 'have-local-pranswer'],
-            setRemoteDescription: ['have-local-offer', 'have-remote-pranswer']
+        /*
+         *  Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
+         *
+         *  Use of this source code is governed by a BSD-style license
+         *  that can be found in the LICENSE file in the root of the source
+         *  tree.
+         */
+        /* eslint-env node */
+        'use strict';
+
+        var SDPUtils = require('sdp');
+
+        function fixStatsType(stat) {
+            return {
+                inboundrtp: 'inbound-rtp',
+                outboundrtp: 'outbound-rtp',
+                candidatepair: 'candidate-pair',
+                localcandidate: 'local-candidate',
+                remotecandidate: 'remote-candidate'
+            }[stat.type] || stat.type;
         }
-    }[type][action].indexOf(signalingState) !== -1;
-}
-
-function maybeAddCandidate(iceTransport, candidate) {
-    // Edge's internal representation adds some fields therefore
-    // not all fieldÑ• are taken into account.
-    var alreadyAdded = iceTransport.getRemoteCandidates()
-        .find(function(remoteCandidate) {
-            return candidate.foundation === remoteCandidate.foundation &&
-                candidate.ip === remoteCandidate.ip &&
-                candidate.port === remoteCandidate.port &&
-                candidate.priority === remoteCandidate.priority &&
-                candidate.protocol === remoteCandidate.protocol &&
-                candidate.type === remoteCandidate.type;
-        });
-    if (!alreadyAdded) {
-        iceTransport.addRemoteCandidate(candidate);
-    }
-    return !alreadyAdded;
-}
-
-
-function makeError(name, description) {
-    var e = new Error(description);
-    e.name = name;
-    // legacy error codes from https://heycam.github.io/webidl/#idl-DOMException-error-names
-    e.code = {
-        NotSupportedError: 9,
-        InvalidStateError: 11,
-        InvalidAccessError: 15,
-        TypeError: undefined,
-        OperationError: undefined
-    }[name];
-    return e;
-}
-
-module.exports = function(window, edgeVersion) {
-    // https://w3c.github.io/mediacapture-main/#mediastream
-    // Helper function to add the track to the stream and
-    // dispatch the event ourselves.
-    function addTrackToStreamAndFireEvent(track, stream) {
-        stream.addTrack(track);
-        stream.dispatchEvent(new window.MediaStreamTrackEvent('addtrack',
-            {track: track}));
-    }
-
-    function removeTrackFromStreamAndFireEvent(track, stream) {
-        stream.removeTrack(track);
-        stream.dispatchEvent(new window.MediaStreamTrackEvent('removetrack',
-            {track: track}));
-    }
-
-    function fireAddTrack(pc, track, receiver, streams) {
-        var trackEvent = new Event('track');
-        trackEvent.track = track;
-        trackEvent.receiver = receiver;
-        trackEvent.transceiver = {receiver: receiver};
-        trackEvent.streams = streams;
-        window.setTimeout(function() {
-            pc._dispatchEvent('track', trackEvent);
-        });
-    }
-
-    var RTCPeerConnection = function(config) {
-        var pc = this;
-
-        var _eventTarget = document.createDocumentFragment();
-        ['addEventListener', 'removeEventListener', 'dispatchEvent']
-            .forEach(function(method) {
-                pc[method] = _eventTarget[method].bind(_eventTarget);
-            });
-
-        this.canTrickleIceCandidates = null;
 
-        this.needNegotiation = false;
+        function writeMediaSection(transceiver, caps, type, stream, dtlsRole) {
+            var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps);
 
-        this.localStreams = [];
-        this.remoteStreams = [];
+            // Map ICE parameters (ufrag, pwd) to SDP.
+            sdp += SDPUtils.writeIceParameters(
+                transceiver.iceGatherer.getLocalParameters());
 
-        this.localDescription = null;
-        this.remoteDescription = null;
+            // Map DTLS parameters to SDP.
+            sdp += SDPUtils.writeDtlsParameters(
+                transceiver.dtlsTransport.getLocalParameters(),
+                type === 'offer' ? 'actpass' : dtlsRole || 'active');
 
-        this.signalingState = 'stable';
-        this.iceConnectionState = 'new';
-        this.connectionState = 'new';
-        this.iceGatheringState = 'new';
+            sdp += 'a=mid:' + transceiver.mid + '\r\n';
 
-        config = JSON.parse(JSON.stringify(config || {}));
-
-        this.usingBundle = config.bundlePolicy === 'max-bundle';
-        if (config.rtcpMuxPolicy === 'negotiate') {
-            throw(makeError('NotSupportedError',
-                'rtcpMuxPolicy \'negotiate\' is not supported'));
-        } else if (!config.rtcpMuxPolicy) {
-            config.rtcpMuxPolicy = 'require';
-        }
-
-        switch (config.iceTransportPolicy) {
-            case 'all':
-            case 'relay':
-                break;
-            default:
-                config.iceTransportPolicy = 'all';
-                break;
-        }
+            if (transceiver.rtpSender && transceiver.rtpReceiver) {
+                sdp += 'a=sendrecv\r\n';
+            } else if (transceiver.rtpSender) {
+                sdp += 'a=sendonly\r\n';
+            } else if (transceiver.rtpReceiver) {
+                sdp += 'a=recvonly\r\n';
+            } else {
+                sdp += 'a=inactive\r\n';
+            }
 
-        switch (config.bundlePolicy) {
-            case 'balanced':
-            case 'max-compat':
-            case 'max-bundle':
-                break;
-            default:
-                config.bundlePolicy = 'balanced';
-                break;
+            if (transceiver.rtpSender) {
+                var trackId = transceiver.rtpSender._initialTrackId ||
+                    transceiver.rtpSender.track.id;
+                transceiver.rtpSender._initialTrackId = trackId;
+                // spec.
+                var msid = 'msid:' + (stream ? stream.id : '-') + ' ' +
+                    trackId + '\r\n';
+                sdp += 'a=' + msid;
+                // for Chrome. Legacy should no longer be required.
+                sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+                    ' ' + msid;
+
+                // RTX
+                if (transceiver.sendEncodingParameters[0].rtx) {
+                    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+                        ' ' + msid;
+                    sdp += 'a=ssrc-group:FID ' +
+                        transceiver.sendEncodingParameters[0].ssrc + ' ' +
+                        transceiver.sendEncodingParameters[0].rtx.ssrc +
+                        '\r\n';
+                }
+            }
+            // FIXME: this should be written by writeRtpDescription.
+            sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+                ' cname:' + SDPUtils.localCName + '\r\n';
+            if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) {
+                sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+                    ' cname:' + SDPUtils.localCName + '\r\n';
+            }
+            return sdp;
         }
 
-        config.iceServers = filterIceServers(config.iceServers || [], edgeVersion);
+        // Edge does not like
+        // 1) stun: filtered after 14393 unless ?transport=udp is present
+        // 2) turn: that does not have all of turn:host:port?transport=udp
+        // 3) turn: with ipv6 addresses
+        // 4) turn: occurring muliple times
+        function filterIceServers(iceServers, edgeVersion) {
+            var hasTurn = false;
+            iceServers = JSON.parse(JSON.stringify(iceServers));
+            return iceServers.filter(function(server) {
+                if (server && (server.urls || server.url)) {
+                    var urls = server.urls || server.url;
+                    if (server.url && !server.urls) {
+                        console.warn('RTCIceServer.url is deprecated! Use urls instead.');
+                    }
+                    var isString = typeof urls === 'string';
+                    if (isString) {
+                        urls = [urls];
+                    }
+                    urls = urls.filter(function(url) {
+                        var validTurn = url.indexOf('turn:') === 0 &&
+                            url.indexOf('transport=udp') !== -1 &&
+                            url.indexOf('turn:[') === -1 &&
+                            !hasTurn;
+
+                        if (validTurn) {
+                            hasTurn = true;
+                            return true;
+                        }
+                        return url.indexOf('stun:') === 0 && edgeVersion >= 14393 &&
+                            url.indexOf('?transport=udp') === -1;
+                    });
 
-        this._iceGatherers = [];
-        if (config.iceCandidatePoolSize) {
-            for (var i = config.iceCandidatePoolSize; i > 0; i--) {
-                this._iceGatherers.push(new window.RTCIceGatherer({
-                    iceServers: config.iceServers,
-                    gatherPolicy: config.iceTransportPolicy
-                }));
-            }
-        } else {
-            config.iceCandidatePoolSize = 0;
+                    delete server.url;
+                    server.urls = isString ? urls[0] : urls;
+                    return !!urls.length;
+                }
+            });
         }
 
-        this._config = config;
-
-        // per-track iceGathers, iceTransports, dtlsTransports, rtpSenders, ...
-        // everything that is needed to describe a SDP m-line.
-        this.transceivers = [];
+        // Determines the intersection of local and remote capabilities.
+        function getCommonCapabilities(localCapabilities, remoteCapabilities) {
+            var commonCapabilities = {
+                codecs: [],
+                headerExtensions: [],
+                fecMechanisms: []
+            };
 
-        this._sdpSessionId = SDPUtils.generateSessionId();
-        this._sdpSessionVersion = 0;
+            var findCodecByPayloadType = function(pt, codecs) {
+                pt = parseInt(pt, 10);
+                for (var i = 0; i < codecs.length; i++) {
+                    if (codecs[i].payloadType === pt ||
+                        codecs[i].preferredPayloadType === pt) {
+                        return codecs[i];
+                    }
+                }
+            };
 
-        this._dtlsRole = undefined; // role for a=setup to use in answers.
+            var rtxCapabilityMatches = function(lRtx, rRtx, lCodecs, rCodecs) {
+                var lCodec = findCodecByPayloadType(lRtx.parameters.apt, lCodecs);
+                var rCodec = findCodecByPayloadType(rRtx.parameters.apt, rCodecs);
+                return lCodec && rCodec &&
+                    lCodec.name.toLowerCase() === rCodec.name.toLowerCase();
+            };
 
-        this._isClosed = false;
-    };
+            localCapabilities.codecs.forEach(function(lCodec) {
+                for (var i = 0; i < remoteCapabilities.codecs.length; i++) {
+                    var rCodec = remoteCapabilities.codecs[i];
+                    if (lCodec.name.toLowerCase() === rCodec.name.toLowerCase() &&
+                        lCodec.clockRate === rCodec.clockRate) {
+                        if (lCodec.name.toLowerCase() === 'rtx' &&
+                            lCodec.parameters && rCodec.parameters.apt) {
+                            // for RTX we need to find the local rtx that has a apt
+                            // which points to the same local codec as the remote one.
+                            if (!rtxCapabilityMatches(lCodec, rCodec,
+                                    localCapabilities.codecs, remoteCapabilities.codecs)) {
+                                continue;
+                            }
+                        }
+                        rCodec = JSON.parse(JSON.stringify(rCodec)); // deepcopy
+                        // number of channels is the highest common number of channels
+                        rCodec.numChannels = Math.min(lCodec.numChannels,
+                            rCodec.numChannels);
+                        // push rCodec so we reply with offerer payload type
+                        commonCapabilities.codecs.push(rCodec);
+
+                        // determine common feedback mechanisms
+                        rCodec.rtcpFeedback = rCodec.rtcpFeedback.filter(function(fb) {
+                            for (var j = 0; j < lCodec.rtcpFeedback.length; j++) {
+                                if (lCodec.rtcpFeedback[j].type === fb.type &&
+                                    lCodec.rtcpFeedback[j].parameter === fb.parameter) {
+                                    return true;
+                                }
+                            }
+                            return false;
+                        });
+                        // FIXME: also need to determine .parameters
+                        //  see https://github.com/openpeer/ortc/issues/569
+                        break;
+                    }
+                }
+            });
 
-    // set up event handlers on prototype
-    RTCPeerConnection.prototype.onicecandidate = null;
-    RTCPeerConnection.prototype.onaddstream = null;
-    RTCPeerConnection.prototype.ontrack = null;
-    RTCPeerConnection.prototype.onremovestream = null;
-    RTCPeerConnection.prototype.onsignalingstatechange = null;
-    RTCPeerConnection.prototype.oniceconnectionstatechange = null;
-    RTCPeerConnection.prototype.onconnectionstatechange = null;
-    RTCPeerConnection.prototype.onicegatheringstatechange = null;
-    RTCPeerConnection.prototype.onnegotiationneeded = null;
-    RTCPeerConnection.prototype.ondatachannel = null;
+            localCapabilities.headerExtensions.forEach(function(lHeaderExtension) {
+                for (var i = 0; i < remoteCapabilities.headerExtensions.length;
+                     i++) {
+                    var rHeaderExtension = remoteCapabilities.headerExtensions[i];
+                    if (lHeaderExtension.uri === rHeaderExtension.uri) {
+                        commonCapabilities.headerExtensions.push(rHeaderExtension);
+                        break;
+                    }
+                }
+            });
 
-    RTCPeerConnection.prototype._dispatchEvent = function(name, event) {
-        if (this._isClosed) {
-            return;
-        }
-        this.dispatchEvent(event);
-        if (typeof this['on' + name] === 'function') {
-            this['on' + name](event);
-        }
-    };
-
-    RTCPeerConnection.prototype._emitGatheringStateChange = function() {
-        var event = new Event('icegatheringstatechange');
-        this._dispatchEvent('icegatheringstatechange', event);
-    };
-
-    RTCPeerConnection.prototype.getConfiguration = function() {
-        return this._config;
-    };
-
-    RTCPeerConnection.prototype.getLocalStreams = function() {
-        return this.localStreams;
-    };
-
-    RTCPeerConnection.prototype.getRemoteStreams = function() {
-        return this.remoteStreams;
-    };
-
-    // internal helper to create a transceiver object.
-    // (which is not yet the same as the WebRTC 1.0 transceiver)
-    RTCPeerConnection.prototype._createTransceiver = function(kind, doNotAdd) {
-        var hasBundleTransport = this.transceivers.length > 0;
-        var transceiver = {
-            track: null,
-            iceGatherer: null,
-            iceTransport: null,
-            dtlsTransport: null,
-            localCapabilities: null,
-            remoteCapabilities: null,
-            rtpSender: null,
-            rtpReceiver: null,
-            kind: kind,
-            mid: null,
-            sendEncodingParameters: null,
-            recvEncodingParameters: null,
-            stream: null,
-            associatedRemoteMediaStreams: [],
-            wantReceive: true
-        };
-        if (this.usingBundle && hasBundleTransport) {
-            transceiver.iceTransport = this.transceivers[0].iceTransport;
-            transceiver.dtlsTransport = this.transceivers[0].dtlsTransport;
-        } else {
-            var transports = this._createIceAndDtlsTransports();
-            transceiver.iceTransport = transports.iceTransport;
-            transceiver.dtlsTransport = transports.dtlsTransport;
+            // FIXME: fecMechanisms
+            return commonCapabilities;
         }
-        if (!doNotAdd) {
-            this.transceivers.push(transceiver);
+
+        // is action=setLocalDescription with type allowed in signalingState
+        function isActionAllowedInSignalingState(action, type, signalingState) {
+            return {
+                offer: {
+                    setLocalDescription: ['stable', 'have-local-offer'],
+                    setRemoteDescription: ['stable', 'have-remote-offer']
+                },
+                answer: {
+                    setLocalDescription: ['have-remote-offer', 'have-local-pranswer'],
+                    setRemoteDescription: ['have-local-offer', 'have-remote-pranswer']
+                }
+            }[type][action].indexOf(signalingState) !== -1;
         }
-        return transceiver;
-    };
 
-    RTCPeerConnection.prototype.addTrack = function(track, stream) {
-        if (this._isClosed) {
-            throw makeError('InvalidStateError',
-                'Attempted to call addTrack on a closed peerconnection.');
+        function maybeAddCandidate(iceTransport, candidate) {
+            // Edge's internal representation adds some fields therefore
+            // not all fieldѕ are taken into account.
+            var alreadyAdded = iceTransport.getRemoteCandidates()
+                .find(function(remoteCandidate) {
+                    return candidate.foundation === remoteCandidate.foundation &&
+                        candidate.ip === remoteCandidate.ip &&
+                        candidate.port === remoteCandidate.port &&
+                        candidate.priority === remoteCandidate.priority &&
+                        candidate.protocol === remoteCandidate.protocol &&
+                        candidate.type === remoteCandidate.type;
+                });
+            if (!alreadyAdded) {
+                iceTransport.addRemoteCandidate(candidate);
+            }
+            return !alreadyAdded;
         }
 
-        var alreadyExists = this.transceivers.find(function(s) {
-            return s.track === track;
-        });
 
-        if (alreadyExists) {
-            throw makeError('InvalidAccessError', 'Track already exists.');
+        function makeError(name, description) {
+            var e = new Error(description);
+            e.name = name;
+            // legacy error codes from https://heycam.github.io/webidl/#idl-DOMException-error-names
+            e.code = {
+                NotSupportedError: 9,
+                InvalidStateError: 11,
+                InvalidAccessError: 15,
+                TypeError: undefined,
+                OperationError: undefined
+            }[name];
+            return e;
         }
 
-        var transceiver;
-        for (var i = 0; i < this.transceivers.length; i++) {
-            if (!this.transceivers[i].track &&
-                this.transceivers[i].kind === track.kind) {
-                transceiver = this.transceivers[i];
+        module.exports = function(window, edgeVersion) {
+            // https://w3c.github.io/mediacapture-main/#mediastream
+            // Helper function to add the track to the stream and
+            // dispatch the event ourselves.
+            function addTrackToStreamAndFireEvent(track, stream) {
+                stream.addTrack(track);
+                stream.dispatchEvent(new window.MediaStreamTrackEvent('addtrack',
+                    {track: track}));
             }
-        }
-        if (!transceiver) {
-            transceiver = this._createTransceiver(track.kind);
-        }
-
-        this._maybeFireNegotiationNeeded();
 
-        if (this.localStreams.indexOf(stream) === -1) {
-            this.localStreams.push(stream);
-        }
+            function removeTrackFromStreamAndFireEvent(track, stream) {
+                stream.removeTrack(track);
+                stream.dispatchEvent(new window.MediaStreamTrackEvent('removetrack',
+                    {track: track}));
+            }
 
-        transceiver.track = track;
-        transceiver.stream = stream;
-        transceiver.rtpSender = new window.RTCRtpSender(track,
-            transceiver.dtlsTransport);
-        return transceiver.rtpSender;
-    };
-
-    RTCPeerConnection.prototype.addStream = function(stream) {
-        var pc = this;
-        if (edgeVersion >= 15025) {
-            stream.getTracks().forEach(function(track) {
-                pc.addTrack(track, stream);
-            });
-        } else {
-            // Clone is necessary for local demos mostly, attaching directly
-            // to two different senders does not work (build 10547).
-            // Fixed in 15025 (or earlier)
-            var clonedStream = stream.clone();
-            stream.getTracks().forEach(function(track, idx) {
-                var clonedTrack = clonedStream.getTracks()[idx];
-                track.addEventListener('enabled', function(event) {
-                    clonedTrack.enabled = event.enabled;
+            function fireAddTrack(pc, track, receiver, streams) {
+                var trackEvent = new Event('track');
+                trackEvent.track = track;
+                trackEvent.receiver = receiver;
+                trackEvent.transceiver = {receiver: receiver};
+                trackEvent.streams = streams;
+                window.setTimeout(function() {
+                    pc._dispatchEvent('track', trackEvent);
                 });
-            });
-            clonedStream.getTracks().forEach(function(track) {
-                pc.addTrack(track, clonedStream);
-            });
-        }
-    };
+            }
 
-    RTCPeerConnection.prototype.removeTrack = function(sender) {
-        if (this._isClosed) {
-            throw makeError('InvalidStateError',
-                'Attempted to call removeTrack on a closed peerconnection.');
-        }
+            var RTCPeerConnection = function(config) {
+                var pc = this;
 
-        if (!(sender instanceof window.RTCRtpSender)) {
-            throw new TypeError('Argument 1 of RTCPeerConnection.removeTrack ' +
-                'does not implement interface RTCRtpSender.');
-        }
+                var _eventTarget = document.createDocumentFragment();
+                ['addEventListener', 'removeEventListener', 'dispatchEvent']
+                    .forEach(function(method) {
+                        pc[method] = _eventTarget[method].bind(_eventTarget);
+                    });
 
-        var transceiver = this.transceivers.find(function(t) {
-            return t.rtpSender === sender;
-        });
+                this.canTrickleIceCandidates = null;
 
-        if (!transceiver) {
-            throw makeError('InvalidAccessError',
-                'Sender was not created by this connection.');
-        }
-        var stream = transceiver.stream;
-
-        transceiver.rtpSender.stop();
-        transceiver.rtpSender = null;
-        transceiver.track = null;
-        transceiver.stream = null;
-
-        // remove the stream from the set of local streams
-        var localStreams = this.transceivers.map(function(t) {
-            return t.stream;
-        });
-        if (localStreams.indexOf(stream) === -1 &&
-            this.localStreams.indexOf(stream) > -1) {
-            this.localStreams.splice(this.localStreams.indexOf(stream), 1);
-        }
+                this.needNegotiation = false;
 
-        this._maybeFireNegotiationNeeded();
-    };
+                this.localStreams = [];
+                this.remoteStreams = [];
 
-    RTCPeerConnection.prototype.removeStream = function(stream) {
-        var pc = this;
-        stream.getTracks().forEach(function(track) {
-            var sender = pc.getSenders().find(function(s) {
-                return s.track === track;
-            });
-            if (sender) {
-                pc.removeTrack(sender);
-            }
-        });
-    };
-
-    RTCPeerConnection.prototype.getSenders = function() {
-        return this.transceivers.filter(function(transceiver) {
-            return !!transceiver.rtpSender;
-        })
-            .map(function(transceiver) {
-                return transceiver.rtpSender;
-            });
-    };
-
-    RTCPeerConnection.prototype.getReceivers = function() {
-        return this.transceivers.filter(function(transceiver) {
-            return !!transceiver.rtpReceiver;
-        })
-            .map(function(transceiver) {
-                return transceiver.rtpReceiver;
-            });
-    };
+                this._localDescription = null;
+                this._remoteDescription = null;
 
+                this.signalingState = 'stable';
+                this.iceConnectionState = 'new';
+                this.connectionState = 'new';
+                this.iceGatheringState = 'new';
 
-    RTCPeerConnection.prototype._createIceGatherer = function(sdpMLineIndex,
-                                                              usingBundle) {
-        var pc = this;
-        if (usingBundle && sdpMLineIndex > 0) {
-            return this.transceivers[0].iceGatherer;
-        } else if (this._iceGatherers.length) {
-            return this._iceGatherers.shift();
-        }
-        var iceGatherer = new window.RTCIceGatherer({
-            iceServers: this._config.iceServers,
-            gatherPolicy: this._config.iceTransportPolicy
-        });
-        Object.defineProperty(iceGatherer, 'state',
-            {value: 'new', writable: true}
-        );
-
-        this.transceivers[sdpMLineIndex].bufferedCandidateEvents = [];
-        this.transceivers[sdpMLineIndex].bufferCandidates = function(event) {
-            var end = !event.candidate || Object.keys(event.candidate).length === 0;
-            // polyfill since RTCIceGatherer.state is not implemented in
-            // Edge 10547 yet.
-            iceGatherer.state = end ? 'completed' : 'gathering';
-            if (pc.transceivers[sdpMLineIndex].bufferedCandidateEvents !== null) {
-                pc.transceivers[sdpMLineIndex].bufferedCandidateEvents.push(event);
-            }
-        };
-        iceGatherer.addEventListener('localcandidate',
-            this.transceivers[sdpMLineIndex].bufferCandidates);
-        return iceGatherer;
-    };
-
-    // start gathering from an RTCIceGatherer.
-    RTCPeerConnection.prototype._gather = function(mid, sdpMLineIndex) {
-        var pc = this;
-        var iceGatherer = this.transceivers[sdpMLineIndex].iceGatherer;
-        if (iceGatherer.onlocalcandidate) {
-            return;
-        }
-        var bufferedCandidateEvents =
-            this.transceivers[sdpMLineIndex].bufferedCandidateEvents;
-        this.transceivers[sdpMLineIndex].bufferedCandidateEvents = null;
-        iceGatherer.removeEventListener('localcandidate',
-            this.transceivers[sdpMLineIndex].bufferCandidates);
-        iceGatherer.onlocalcandidate = function(evt) {
-            if (pc.usingBundle && sdpMLineIndex > 0) {
-                // if we know that we use bundle we can drop candidates with
-                // Ñ•dpMLineIndex > 0. If we don't do this then our state gets
-                // confused since we dispose the extra ice gatherer.
-                return;
-            }
-            var event = new Event('icecandidate');
-            event.candidate = {sdpMid: mid, sdpMLineIndex: sdpMLineIndex};
-
-            var cand = evt.candidate;
-            // Edge emits an empty object for RTCIceCandidateComplete‥
-            var end = !cand || Object.keys(cand).length === 0;
-            if (end) {
-                // polyfill since RTCIceGatherer.state is not implemented in
-                // Edge 10547 yet.
-                if (iceGatherer.state === 'new' || iceGatherer.state === 'gathering') {
-                    iceGatherer.state = 'completed';
-                }
-            } else {
-                if (iceGatherer.state === 'new') {
-                    iceGatherer.state = 'gathering';
-                }
-                // RTCIceCandidate doesn't have a component, needs to be added
-                cand.component = 1;
-                // also the usernameFragment. TODO: update SDP to take both variants.
-                cand.ufrag = iceGatherer.getLocalParameters().usernameFragment;
-
-                var serializedCandidate = SDPUtils.writeCandidate(cand);
-                event.candidate = Object.assign(event.candidate,
-                    SDPUtils.parseCandidate(serializedCandidate));
-
-                event.candidate.candidate = serializedCandidate;
-                event.candidate.toJSON = function() {
-                    return {
-                        candidate: event.candidate.candidate,
-                        sdpMid: event.candidate.sdpMid,
-                        sdpMLineIndex: event.candidate.sdpMLineIndex,
-                        usernameFragment: event.candidate.usernameFragment
-                    };
-                };
-            }
+                config = JSON.parse(JSON.stringify(config || {}));
 
-            // update local description.
-            var sections = SDPUtils.getMediaSections(pc.localDescription.sdp);
-            if (!end) {
-                sections[event.candidate.sdpMLineIndex] +=
-                    'a=' + event.candidate.candidate + '\r\n';
-            } else {
-                sections[event.candidate.sdpMLineIndex] +=
-                    'a=end-of-candidates\r\n';
-            }
-            pc.localDescription.sdp =
-                SDPUtils.getDescription(pc.localDescription.sdp) +
-                sections.join('');
-            var complete = pc.transceivers.every(function(transceiver) {
-                return transceiver.iceGatherer &&
-                    transceiver.iceGatherer.state === 'completed';
-            });
+                this.usingBundle = config.bundlePolicy === 'max-bundle';
+                if (config.rtcpMuxPolicy === 'negotiate') {
+                    throw(makeError('NotSupportedError',
+                        'rtcpMuxPolicy \'negotiate\' is not supported'));
+                } else if (!config.rtcpMuxPolicy) {
+                    config.rtcpMuxPolicy = 'require';
+                }
 
-            if (pc.iceGatheringState !== 'gathering') {
-                pc.iceGatheringState = 'gathering';
-                pc._emitGatheringStateChange();
-            }
+                switch (config.iceTransportPolicy) {
+                    case 'all':
+                    case 'relay':
+                        break;
+                    default:
+                        config.iceTransportPolicy = 'all';
+                        break;
+                }
 
-            // Emit candidate. Also emit null candidate when all gatherers are
-            // complete.
-            if (!end) {
-                pc._dispatchEvent('icecandidate', event);
-            }
-            if (complete) {
-                pc._dispatchEvent('icecandidate', new Event('icecandidate'));
-                pc.iceGatheringState = 'complete';
-                pc._emitGatheringStateChange();
-            }
-        };
+                switch (config.bundlePolicy) {
+                    case 'balanced':
+                    case 'max-compat':
+                    case 'max-bundle':
+                        break;
+                    default:
+                        config.bundlePolicy = 'balanced';
+                        break;
+                }
 
-        // emit already gathered candidates.
-        window.setTimeout(function() {
-            bufferedCandidateEvents.forEach(function(e) {
-                iceGatherer.onlocalcandidate(e);
-            });
-        }, 0);
-    };
-
-    // Create ICE transport and DTLS transport.
-    RTCPeerConnection.prototype._createIceAndDtlsTransports = function() {
-        var pc = this;
-        var iceTransport = new window.RTCIceTransport(null);
-        iceTransport.onicestatechange = function() {
-            pc._updateIceConnectionState();
-            pc._updateConnectionState();
-        };
+                config.iceServers = filterIceServers(config.iceServers || [], edgeVersion);
 
-        var dtlsTransport = new window.RTCDtlsTransport(iceTransport);
-        dtlsTransport.ondtlsstatechange = function() {
-            pc._updateConnectionState();
-        };
-        dtlsTransport.onerror = function() {
-            // onerror does not set state to failed by itself.
-            Object.defineProperty(dtlsTransport, 'state',
-                {value: 'failed', writable: true});