diff --git a/admin/cli/generate_key.php b/admin/cli/generate_key.php new file mode 100644 index 00000000000..28fd0af3660 --- /dev/null +++ b/admin/cli/generate_key.php @@ -0,0 +1,77 @@ +. + +/** + * Generates a secure key for the current server (presuming it does not already exist). + * + * @package core_admin + * @copyright 2020 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use \core\encryption; + +define('CLI_SCRIPT', true); + +require(__DIR__ . '/../../config.php'); +require_once($CFG->libdir . '/clilib.php'); + +// Get cli options. +[$options, $unrecognized] = cli_get_params( + ['help' => false, 'method' => null], + ['h' => 'help']); + +if ($unrecognized) { + $unrecognized = implode("\n ", $unrecognized); + cli_error(get_string('cliunknowoption', 'admin', $unrecognized)); +} + +if ($options['help']) { + echo "Generate secure key + +This script manually creates a secure key within the secret data root folder (configured in +config.php as \$CFG->secretdataroot). You must run it using an account with access to write +to that folder. + +In normal use Moodle automatically creates the key; this script is intended when setting up +a new Moodle system, for cases where the secure folder is not on shared storage and the key +may be manually installed on multiple servers. + +Options: +-h, --help Print out this help +--method Generate key for specified encryption method instead of default. + * sodium + * openssl-aes-256-ctr + +Example: +php admin/cli/generate_key.php +"; + exit; +} + +$method = $options['method']; + +if (encryption::key_exists($method)) { + echo 'Key already exists: ' . encryption::get_key_file($method) . "\n"; + exit; +} + +// Creates key with default permissions (no chmod). +echo "Generating key...\n"; +encryption::create_key($method, false); + +echo "\nKey created: " . encryption::get_key_file($method) . "\n\n"; +echo "If the key folder is not shared storage, then key files should be copied to all servers.\n"; diff --git a/admin/templates/setting_encryptedpassword.mustache b/admin/templates/setting_encryptedpassword.mustache new file mode 100644 index 00000000000..e2a98cf7db0 --- /dev/null +++ b/admin/templates/setting_encryptedpassword.mustache @@ -0,0 +1,64 @@ +{{! + 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 . +}} +{{! + @template core_admin/admin_setting_encryptedpassword + + Admin encrypted password template. + + Context variables required for this template: + * name - form element name + * set - whether it is set or empty + * id - element id + + Example context (json): + { + "name": "test", + "id": "test0", + "set": true + } +}} +
+ {{#set}} + {{# str }} encryptedpassword_set, admin {{/ str }} + {{/set}} + {{^set}} + + {{# str }} novalueclicktoset, form {{/ str }} + {{# pix }} t/passwordunmask-edit, core, {{# str }} passwordunmaskedithint, form {{/ str }}{{/ pix }} + + {{/set}} + + {{! + Using buttons instead of links here allows them to be connected to the label, so the button + works if you click the label. + }} + {{#set}} + + {{/set}} + +
+ +{{#js}} +require(['core_form/encryptedpassword'], function(encryptedpassword) { + new encryptedpassword.EncryptedPassword("{{ id }}"); +}); +{{/js}} diff --git a/admin/tests/behat/behat_admin.php b/admin/tests/behat/behat_admin.php index 98f5d91de10..d71af457568 100644 --- a/admin/tests/behat/behat_admin.php +++ b/admin/tests/behat/behat_admin.php @@ -89,7 +89,7 @@ class behat_admin extends behat_base { } /** - * Sets the specified site settings. A table with | config | value | (optional)plugin | is expected. + * Sets the specified site settings. A table with | config | value | (optional)plugin | (optional)encrypted | is expected. * * @Given /^the following config values are set as admin:$/ * @param TableNode $table @@ -103,11 +103,20 @@ class behat_admin extends behat_base { foreach ($data as $config => $value) { // Default plugin value is null. $plugin = null; + $encrypted = false; if (is_array($value)) { $plugin = $value[1]; + if (array_key_exists(2, $value)) { + $encrypted = $value[2] === 'encrypted'; + } $value = $value[0]; } + + if ($encrypted) { + $value = \core\encryption::encrypt($value); + } + set_config($config, $value, $plugin); } } diff --git a/admin/tool/behat/tests/behat/edit_permissions.feature b/admin/tool/behat/tests/behat/edit_permissions.feature index e3ffc92aef1..7394512c5ce 100644 --- a/admin/tool/behat/tests/behat/edit_permissions.feature +++ b/admin/tool/behat/tests/behat/edit_permissions.feature @@ -6,14 +6,18 @@ Feature: Edit capabilities Background: Given the following "users" exist: - | username | firstname | lastname | email | - | teacher1 | Teacher | 1 | teacher1@example.com | + | username | firstname | lastname | + | teacher1 | Teacher | 1 | + | tutor | Teaching | Assistant | + | student | Student | One | And the following "courses" exist: - | fullname | shortname | category | - | Course 1 | C1 | 0 | + | fullname | shortname | + | Course 1 | C1 | And the following "course enrolments" exist: - | user | course | role | - | teacher1 | C1 | editingteacher | + | user | course | role | + | teacher1 | C1 | editingteacher | + | tutor | C1 | teacher | + | student | C1 | student | Scenario: Default system capabilities modification Given I log in as "admin" @@ -60,3 +64,25 @@ Feature: Edit capabilities Then "mod/forum:deleteanypost" capability has "Prohibit" permission And "mod/forum:editanypost" capability has "Prevent" permission And "mod/forum:addquestion" capability has "Allow" permission + + @javascript + Scenario: Edit permissions escapes role names correctly + When I am on the "C1" "Course" page logged in as "admin" + And I navigate to "Edit settings" in current page administration + And I set the following fields to these values: + | Your word for 'Teacher' | Teacher >= editing | + | Your word for 'Non-editing teacher' | Teacher < "editing" | + | Your word for 'Student' | Studier & 'learner' | + And I press "Save and display" + And I navigate to course participants + Then I should see "Teacher >= editing (Teacher)" in the "Teacher 1" "table_row" + And I should see "Teacher < \"editing\" (Non-editing teacher)" in the "Teaching Assistant" "table_row" + And I should see "Studier & 'learner' (Student)" in the "Student One" "table_row" + And I navigate to "Users > Permissions" in current page administration + And I should see "Teacher >= editing" in the "mod/forum:replypost" "table_row" + And I should see "Teacher < \"editing\"" in the "mod/forum:replypost" "table_row" + And I should see "Studier & 'learner'" in the "mod/forum:replypost" "table_row" + And I follow "Prohibit" + And "Teacher >= editing" "button" in the "Prohibit role" "dialogue" should be visible + And "Teacher < \"editing\"" "button" in the "Prohibit role" "dialogue" should be visible + And "Studier & 'learner'" "button" in the "Prohibit role" "dialogue" should be visible diff --git a/admin/tool/behat/tests/behat/keyboard.feature b/admin/tool/behat/tests/behat/keyboard.feature index ca9a74f02f3..cdcb0812f4f 100644 --- a/admin/tool/behat/tests/behat/keyboard.feature +++ b/admin/tool/behat/tests/behat/keyboard.feature @@ -28,15 +28,16 @@ Feature: Verify that keyboard steps work as expected And I press the shift tab key And the focused element is "Username" "field" - @javascript - Scenario: Using the arrow keys allows me to navigate through menus - Given the following "users" exist: - | username | email | firstname | lastname | - | saffronr | saffron.rutledge@example.com | Saffron | Rutledge | - And I log in as "saffronr" - And I click on "Saffron Rutledge" "link" in the ".usermenu" "css_element" - When I press the up key - Then the focused element is "Log out" "link" +# TODO: Uncomment the following when MDL-66979 is integrated. +# @javascript +# Scenario: Using the arrow keys allows me to navigate through menus +# Given the following "users" exist: +# | username | email | firstname | lastname | +# | saffronr | saffron.rutledge@example.com | Saffron | Rutledge | +# And I log in as "saffronr" +# And I click on "Saffron Rutledge" "link" in the ".usermenu" "css_element" +# When I press the up key +# Then the focused element is "Log out" "link" @javascript Scenario: The escape key can be used to close a dialogue diff --git a/admin/upgrade.txt b/admin/upgrade.txt index d3adb1808f7..c3ab30f87e4 100644 --- a/admin/upgrade.txt +++ b/admin/upgrade.txt @@ -1,5 +1,10 @@ This files describes API changes in /admin/*. +=== 3.11 === + +* New admin setting admin_setting_encryptedpassword allows passwords in admin settings to be + encrypted (with the new \core\encryption API) so that even the admin cannot read them. + === 3.9 === * The following functions, previously used (exclusively) by upgrade steps are not available anymore because of the upgrade cleanup performed for this version. See MDL-65809 for more info: diff --git a/analytics/classes/manager.php b/analytics/classes/manager.php index 8bbdf4d2cd4..4216bbe4cfd 100644 --- a/analytics/classes/manager.php +++ b/analytics/classes/manager.php @@ -603,8 +603,8 @@ class manager { */ public static function add_builtin_models() { - debugging('core_analytics\manager::add_builtin_models() has been deprecated. Core models are now automatically '. - 'updated according to their declaration in the lib/db/analytics.php file.', DEBUG_DEVELOPER); + throw new \coding_exception('core_analytics\manager::add_builtin_models() has been removed. Core models ' . + 'are now automatically updated according to their declaration in the lib/db/analytics.php file.'); } /** diff --git a/analytics/upgrade.txt b/analytics/upgrade.txt index 9501afbeeae..ff24228eda0 100644 --- a/analytics/upgrade.txt +++ b/analytics/upgrade.txt @@ -4,6 +4,10 @@ information provided here is intended especially for developers. === 3.11 === * Final deprecation get_enabled_time_splitting_methods. Method has been removed. Use get_time_splitting_methods_for_evaluation instead. +* Final deprecation add_builtin_models. Method has been removed. The functionality + has been replaced with automatic update of models provided by the core moodle component. + There is no need to call this method explicitly any more. Instead, adding new models can be achieved + by updating the lib/db/analytics.php file and bumping the core version. === 3.8 === diff --git a/config-dist.php b/config-dist.php index b8c5d51cb99..89330247dae 100644 --- a/config-dist.php +++ b/config-dist.php @@ -727,6 +727,22 @@ $CFG->admin = 'admin'; // // $CFG->maxcoursesincategory = 10000; // +// Admin setting encryption +// +// $CFG->secretdataroot = '/var/www/my_secret_folder'; +// +// Location to store encryption keys. By default this is $CFG->dataroot/secret; set this if +// you want to use a different location for increased security (e.g. if too many people have access +// to the main dataroot, or if you want to avoid using shared storage). Your web server user needs +// read access to this location, and write access unless you manually create the keys. +// +// $CFG->nokeygeneration = false; +// +// If you change this to true then the server will give an error if keys don't exist, instead of +// automatically generating them. This is only needed if you want to ensure that keys are consistent +// across a cluster when not using shared storage. If you stop the server generating keys, you will +// need to manually generate them by running 'php admin/cli/generate_key.php'. + //========================================================================= // 7. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!! //========================================================================= diff --git a/course/amd/build/local/activitychooser/dialogue.min.js b/course/amd/build/local/activitychooser/dialogue.min.js index d517d5b68fa..8028476a932 100644 --- a/course/amd/build/local/activitychooser/dialogue.min.js +++ b/course/amd/build/local/activitychooser/dialogue.min.js @@ -1,2 +1,2 @@ -function _typeof(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){_typeof=function(a){return typeof a}}else{_typeof=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return _typeof(a)}define ("core_course/local/activitychooser/dialogue",["exports","jquery","core/modal_events","core_course/local/activitychooser/selectors","core/templates","core/key_codes","core/loadingicon","core_course/local/activitychooser/repository","core/notification","core/utils"],function(a,b,c,d,e,f,g,h,i,j){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.displayChooser=void 0;b=m(b);c=l(c);d=m(d);e=l(e);h=l(h);i=m(i);var v="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};function k(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;k=function(){return a};return a}function l(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=k();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var e in a){if(Object.prototype.hasOwnProperty.call(a,e)){var f=d?Object.getOwnPropertyDescriptor(a,e):null;if(f&&(f.get||f.set)){Object.defineProperty(c,e,f)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}function m(a){return a&&a.__esModule?a:{default:a}}function n(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function o(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var h=a.apply(b,c);function f(a){n(h,d,e,f,g,"next",a)}function g(a){n(h,d,e,f,g,"throw",a)}f(void 0)})}}function p(a,b){return u(a)||t(a,b)||r(a,b)||q()}function q(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function r(a,b){if(!a)return;if("string"==typeof a)return s(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);if("Object"===c&&a.constructor)c=a.constructor.name;if("Map"===c||"Set"===c)return Array.from(c);if("Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c))return s(a,b)}function s(a,b){if(null==b||b>a.length)b=a.length;for(var c=0,d=Array(b);ca.length)b=a.length;for(var c=0,d=Array(b);c.\n\n/**\n * A type of dialogue used as for choosing options.\n *\n * @module core_course/local/chooser/dialogue\n * @package core\n * @copyright 2019 Mihail Geshoski \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport * as ModalEvents from 'core/modal_events';\nimport selectors from 'core_course/local/activitychooser/selectors';\nimport * as Templates from 'core/templates';\nimport {end, arrowLeft, arrowRight, home, enter, space} from 'core/key_codes';\nimport {addIconToContainer} from 'core/loadingicon';\nimport * as Repository from 'core_course/local/activitychooser/repository';\nimport Notification from 'core/notification';\nimport {debounce} from 'core/utils';\nconst getPlugin = pluginName => import(pluginName);\n\n/**\n * Given an event from the main module 'page' navigate to it's help section via a carousel.\n *\n * @method showModuleHelp\n * @param {jQuery} carousel Our initialized carousel to manipulate\n * @param {Object} moduleData Data of the module to carousel to\n * @param {jQuery} modal We need to figure out if the current modal has a footer.\n */\nconst showModuleHelp = (carousel, moduleData, modal = null) => {\n // If we have a real footer then we need to change temporarily.\n if (modal !== null && moduleData.showFooter === true) {\n modal.setFooter(Templates.render('core_course/local/activitychooser/footer_partial', moduleData));\n }\n const help = carousel.find(selectors.regions.help)[0];\n help.innerHTML = '';\n help.classList.add('m-auto');\n\n // Add a spinner.\n const spinnerPromise = addIconToContainer(help);\n\n // Used later...\n let transitionPromiseResolver = null;\n const transitionPromise = new Promise(resolve => {\n transitionPromiseResolver = resolve;\n });\n\n // Build up the html & js ready to place into the help section.\n const contentPromise = Templates.renderForPromise('core_course/local/activitychooser/help', moduleData);\n\n // Wait for the content to be ready, and for the transition to be complet.\n Promise.all([contentPromise, spinnerPromise, transitionPromise])\n .then(([{html, js}]) => Templates.replaceNodeContents(help, html, js))\n .then(() => {\n help.querySelector(selectors.regions.chooserSummary.header).focus();\n return help;\n })\n .catch(Notification.exception);\n\n // Move to the next slide, and resolve the transition promise when it's done.\n carousel.one('slid.bs.carousel', () => {\n transitionPromiseResolver();\n });\n // Trigger the transition between 'pages'.\n carousel.carousel('next');\n};\n\n/**\n * Given a user wants to change the favourite state of a module we either add or remove the status.\n * We also propergate this change across our map of modals.\n *\n * @method manageFavouriteState\n * @param {HTMLElement} modalBody The DOM node of the modal to manipulate\n * @param {HTMLElement} caller\n * @param {Function} partialFavourite Partially applied function we need to manage favourite status\n */\nconst manageFavouriteState = async(modalBody, caller, partialFavourite) => {\n const isFavourite = caller.dataset.favourited;\n const id = caller.dataset.id;\n const name = caller.dataset.name;\n const internal = caller.dataset.internal;\n // Switch on fave or not.\n if (isFavourite === 'true') {\n await Repository.unfavouriteModule(name, id);\n\n partialFavourite(internal, false, modalBody);\n } else {\n await Repository.favouriteModule(name, id);\n\n partialFavourite(internal, true, modalBody);\n }\n\n};\n\n/**\n * Register chooser related event listeners.\n *\n * @method registerListenerEvents\n * @param {Promise} modal Our modal that we are working with\n * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}\n * @param {Function} partialFavourite Partially applied function we need to manage favourite status\n * @param {Object} footerData Our base footer object.\n */\nconst registerListenerEvents = (modal, mappedModules, partialFavourite, footerData) => {\n const bodyClickListener = async(e) => {\n if (e.target.closest(selectors.actions.optionActions.showSummary)) {\n const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));\n\n const module = e.target.closest(selectors.regions.chooserOption.container);\n const moduleName = module.dataset.modname;\n const moduleData = mappedModules.get(moduleName);\n // We need to know if the overall modal has a footer so we know when to show a real / vs fake footer.\n moduleData.showFooter = modal.hasFooterContent();\n showModuleHelp(carousel, moduleData, modal);\n }\n\n if (e.target.closest(selectors.actions.optionActions.manageFavourite)) {\n const caller = e.target.closest(selectors.actions.optionActions.manageFavourite);\n await manageFavouriteState(modal.getBody()[0], caller, partialFavourite);\n const activeSectionId = modal.getBody()[0].querySelector(selectors.elements.activetab).getAttribute(\"href\");\n const sectionChooserOptions = modal.getBody()[0]\n .querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));\n const firstChooserOption = sectionChooserOptions\n .querySelector(selectors.regions.chooserOption.container);\n toggleFocusableChooserOption(firstChooserOption, true);\n initChooserOptionsKeyboardNavigation(modal.getBody()[0], mappedModules, sectionChooserOptions, modal);\n }\n\n // From the help screen go back to the module overview.\n if (e.target.matches(selectors.actions.closeOption)) {\n const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));\n\n // Trigger the transition between 'pages'.\n carousel.carousel('prev');\n carousel.on('slid.bs.carousel', () => {\n const allModules = modal.getBody()[0].querySelector(selectors.regions.modules);\n const caller = allModules.querySelector(selectors.regions.getModuleSelector(e.target.dataset.modname));\n caller.focus();\n });\n }\n\n // The \"clear search\" button is triggered.\n if (e.target.closest(selectors.actions.clearSearch)) {\n // Clear the entered search query in the search bar and hide the search results container.\n const searchInput = modal.getBody()[0].querySelector(selectors.actions.search);\n searchInput.value = \"\";\n searchInput.focus();\n toggleSearchResultsView(modal, mappedModules, searchInput.value);\n }\n };\n\n // We essentially have two types of footer.\n // A fake one that is handled within the template for chooser_help and then all of the stuff for\n // modal.footer. We need to ensure we know exactly what type of footer we are using so we know what we\n // need to manage. The below code handles a real footer going to a mnet carousel item.\n const footerClickListener = async(e) => {\n if (footerData.footer === true) {\n const footerjs = await getPlugin(footerData.customfooterjs);\n await footerjs.footerClickListener(e, footerData, modal);\n }\n };\n\n modal.getBodyPromise()\n\n // The return value of getBodyPromise is a jquery object containing the body NodeElement.\n .then(body => body[0])\n\n // Set up the carousel.\n .then(body => {\n $(body.querySelector(selectors.regions.carousel))\n .carousel({\n interval: false,\n pause: true,\n keyboard: false\n });\n\n return body;\n })\n\n // Add the listener for clicks on the body.\n .then(body => {\n body.addEventListener('click', bodyClickListener);\n return body;\n })\n\n // Add a listener for an input change in the activity chooser's search bar.\n .then(body => {\n const searchInput = body.querySelector(selectors.actions.search);\n // The search input is triggered.\n searchInput.addEventListener('input', debounce(() => {\n // Display the search results.\n toggleSearchResultsView(modal, mappedModules, searchInput.value);\n }, 300));\n return body;\n })\n\n // Register event listeners related to the keyboard navigation controls.\n .then(body => {\n // Get the active chooser options section.\n const activeSectionId = body.querySelector(selectors.elements.activetab).getAttribute(\"href\");\n const sectionChooserOptions = body.querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));\n const firstChooserOption = sectionChooserOptions.querySelector(selectors.regions.chooserOption.container);\n\n toggleFocusableChooserOption(firstChooserOption, true);\n initTabsKeyboardNavigation(body);\n initChooserOptionsKeyboardNavigation(body, mappedModules, sectionChooserOptions, modal);\n\n return body;\n })\n .catch();\n\n modal.getFooterPromise()\n\n // The return value of getBodyPromise is a jquery object containing the body NodeElement.\n .then(footer => footer[0])\n // Add the listener for clicks on the footer.\n .then(footer => {\n footer.addEventListener('click', footerClickListener);\n return footer;\n })\n .catch();\n};\n\n/**\n * Initialise the keyboard navigation controls for the tab list items.\n *\n * @method initTabsKeyboardNavigation\n * @param {HTMLElement} body Our modal that we are working with\n */\nconst initTabsKeyboardNavigation = (body) => {\n // Set up the tab handlers.\n const favTabNav = body.querySelector(selectors.regions.favouriteTabNav);\n const recommendedTabNav = body.querySelector(selectors.regions.recommendedTabNav);\n const defaultTabNav = body.querySelector(selectors.regions.defaultTabNav);\n const activityTabNav = body.querySelector(selectors.regions.activityTabNav);\n const resourceTabNav = body.querySelector(selectors.regions.resourceTabNav);\n const tabNavArray = [favTabNav, recommendedTabNav, defaultTabNav, activityTabNav, resourceTabNav];\n tabNavArray.forEach((element) => {\n return element.addEventListener('keydown', (e) => {\n // The first visible navigation tab link.\n const firstLink = e.target.parentElement.querySelector(selectors.elements.visibletabs);\n // The last navigation tab link. It would always be the default activities tab link.\n const lastLink = e.target.parentElement.lastElementChild;\n\n if (e.keyCode === arrowRight) {\n const nextLink = e.target.nextElementSibling;\n if (nextLink === null) {\n e.target.tabIndex = -1;\n firstLink.tabIndex = 0;\n firstLink.focus();\n } else if (nextLink.classList.contains('d-none')) {\n e.target.tabIndex = -1;\n lastLink.tabIndex = 0;\n lastLink.focus();\n } else {\n e.target.tabIndex = -1;\n nextLink.tabIndex = 0;\n nextLink.focus();\n }\n }\n if (e.keyCode === arrowLeft) {\n const previousLink = e.target.previousElementSibling;\n if (previousLink === null) {\n e.target.tabIndex = -1;\n lastLink.tabIndex = 0;\n lastLink.focus();\n } else if (previousLink.classList.contains('d-none')) {\n e.target.tabIndex = -1;\n firstLink.tabIndex = 0;\n firstLink.focus();\n } else {\n e.target.tabIndex = -1;\n previousLink.tabIndex = 0;\n previousLink.focus();\n }\n }\n if (e.keyCode === home) {\n e.target.tabIndex = -1;\n firstLink.tabIndex = 0;\n firstLink.focus();\n }\n if (e.keyCode === end) {\n e.target.tabIndex = -1;\n lastLink.tabIndex = 0;\n lastLink.focus();\n }\n if (e.keyCode === space) {\n e.preventDefault();\n e.target.click();\n }\n });\n });\n};\n\n/**\n * Initialise the keyboard navigation controls for the chooser options.\n *\n * @method initChooserOptionsKeyboardNavigation\n * @param {HTMLElement} body Our modal that we are working with\n * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}\n * @param {HTMLElement} chooserOptionsContainer The section that contains the chooser items\n * @param {Object} modal Our created modal for the section\n */\nconst initChooserOptionsKeyboardNavigation = (body, mappedModules, chooserOptionsContainer, modal = null) => {\n const chooserOptions = chooserOptionsContainer.querySelectorAll(selectors.regions.chooserOption.container);\n\n Array.from(chooserOptions).forEach((element) => {\n return element.addEventListener('keydown', (e) => {\n\n // Check for enter/ space triggers for showing the help.\n if (e.keyCode === enter || e.keyCode === space) {\n if (e.target.matches(selectors.actions.optionActions.showSummary)) {\n e.preventDefault();\n const module = e.target.closest(selectors.regions.chooserOption.container);\n const moduleName = module.dataset.modname;\n const moduleData = mappedModules.get(moduleName);\n const carousel = $(body.querySelector(selectors.regions.carousel));\n carousel.carousel({\n interval: false,\n pause: true,\n keyboard: false\n });\n\n // We need to know if the overall modal has a footer so we know when to show a real / vs fake footer.\n moduleData.showFooter = modal.hasFooterContent();\n showModuleHelp(carousel, moduleData, modal);\n }\n }\n\n // Next.\n if (e.keyCode === arrowRight) {\n e.preventDefault();\n const currentOption = e.target.closest(selectors.regions.chooserOption.container);\n const nextOption = currentOption.nextElementSibling;\n const firstOption = chooserOptionsContainer.firstElementChild;\n const toFocusOption = clickErrorHandler(nextOption, firstOption);\n focusChooserOption(toFocusOption, currentOption);\n }\n\n // Previous.\n if (e.keyCode === arrowLeft) {\n e.preventDefault();\n const currentOption = e.target.closest(selectors.regions.chooserOption.container);\n const previousOption = currentOption.previousElementSibling;\n const lastOption = chooserOptionsContainer.lastElementChild;\n const toFocusOption = clickErrorHandler(previousOption, lastOption);\n focusChooserOption(toFocusOption, currentOption);\n }\n\n if (e.keyCode === home) {\n e.preventDefault();\n const currentOption = e.target.closest(selectors.regions.chooserOption.container);\n const firstOption = chooserOptionsContainer.firstElementChild;\n focusChooserOption(firstOption, currentOption);\n }\n\n if (e.keyCode === end) {\n e.preventDefault();\n const currentOption = e.target.closest(selectors.regions.chooserOption.container);\n const lastOption = chooserOptionsContainer.lastElementChild;\n focusChooserOption(lastOption, currentOption);\n }\n });\n });\n};\n\n/**\n * Focus on a chooser option element and remove the previous chooser element from the focus order\n *\n * @method focusChooserOption\n * @param {HTMLElement} currentChooserOption The current chooser option element that we want to focus\n * @param {HTMLElement|null} previousChooserOption The previous focused option element\n */\nconst focusChooserOption = (currentChooserOption, previousChooserOption = null) => {\n if (previousChooserOption !== null) {\n toggleFocusableChooserOption(previousChooserOption, false);\n }\n\n toggleFocusableChooserOption(currentChooserOption, true);\n currentChooserOption.focus();\n};\n\n/**\n * Add or remove a chooser option from the focus order.\n *\n * @method toggleFocusableChooserOption\n * @param {HTMLElement} chooserOption The chooser option element which should be added or removed from the focus order\n * @param {Boolean} isFocusable Whether the chooser element is focusable or not\n */\nconst toggleFocusableChooserOption = (chooserOption, isFocusable) => {\n const chooserOptionLink = chooserOption.querySelector(selectors.actions.addChooser);\n const chooserOptionHelp = chooserOption.querySelector(selectors.actions.optionActions.showSummary);\n const chooserOptionFavourite = chooserOption.querySelector(selectors.actions.optionActions.manageFavourite);\n\n if (isFocusable) {\n // Set tabindex to 0 to add current chooser option element to the focus order.\n chooserOption.tabIndex = 0;\n chooserOptionLink.tabIndex = 0;\n chooserOptionHelp.tabIndex = 0;\n chooserOptionFavourite.tabIndex = 0;\n } else {\n // Set tabindex to -1 to remove the previous chooser option element from the focus order.\n chooserOption.tabIndex = -1;\n chooserOptionLink.tabIndex = -1;\n chooserOptionHelp.tabIndex = -1;\n chooserOptionFavourite.tabIndex = -1;\n }\n};\n\n/**\n * Small error handling function to make sure the navigated to object exists\n *\n * @method clickErrorHandler\n * @param {HTMLElement} item What we want to check exists\n * @param {HTMLElement} fallback If we dont match anything fallback the focus\n * @return {HTMLElement}\n */\nconst clickErrorHandler = (item, fallback) => {\n if (item !== null) {\n return item;\n } else {\n return fallback;\n }\n};\n\n/**\n * Render the search results in a defined container\n *\n * @method renderSearchResults\n * @param {HTMLElement} searchResultsContainer The container where the data should be rendered\n * @param {Object} searchResultsData Data containing the module items that satisfy the search criteria\n */\nconst renderSearchResults = async(searchResultsContainer, searchResultsData) => {\n const templateData = {\n 'searchresultsnumber': searchResultsData.length,\n 'searchresults': searchResultsData\n };\n // Build up the html & js ready to place into the help section.\n const {html, js} = await Templates.renderForPromise('core_course/local/activitychooser/search_results', templateData);\n await Templates.replaceNodeContents(searchResultsContainer, html, js);\n};\n\n/**\n * Toggle (display/hide) the search results depending on the value of the search query\n *\n * @method toggleSearchResultsView\n * @param {Object} modal Our created modal for the section\n * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}\n * @param {String} searchQuery The search query\n */\nconst toggleSearchResultsView = async(modal, mappedModules, searchQuery) => {\n const modalBody = modal.getBody()[0];\n const searchResultsContainer = modalBody.querySelector(selectors.regions.searchResults);\n const chooserContainer = modalBody.querySelector(selectors.regions.chooser);\n const clearSearchButton = modalBody.querySelector(selectors.actions.clearSearch);\n\n if (searchQuery.length > 0) { // Search query is present.\n const searchResultsData = searchModules(mappedModules, searchQuery);\n await renderSearchResults(searchResultsContainer, searchResultsData);\n const searchResultItemsContainer = searchResultsContainer.querySelector(selectors.regions.searchResultItems);\n const firstSearchResultItem = searchResultItemsContainer.querySelector(selectors.regions.chooserOption.container);\n if (firstSearchResultItem) {\n // Set the first result item to be focusable.\n toggleFocusableChooserOption(firstSearchResultItem, true);\n // Register keyboard events on the created search result items.\n initChooserOptionsKeyboardNavigation(modalBody, mappedModules, searchResultItemsContainer, modal);\n }\n // Display the \"clear\" search button in the activity chooser search bar.\n clearSearchButton.classList.remove('d-none');\n // Hide the default chooser options container.\n chooserContainer.setAttribute('hidden', 'hidden');\n // Display the search results container.\n searchResultsContainer.removeAttribute('hidden');\n } else { // Search query is not present.\n // Hide the \"clear\" search button in the activity chooser search bar.\n clearSearchButton.classList.add('d-none');\n // Hide the search results container.\n searchResultsContainer.setAttribute('hidden', 'hidden');\n // Display the default chooser options container.\n chooserContainer.removeAttribute('hidden');\n }\n};\n\n/**\n * Return the list of modules which have a name or description that matches the given search term.\n *\n * @method searchModules\n * @param {Array} modules List of available modules\n * @param {String} searchTerm The search term to match\n * @return {Array}\n */\nconst searchModules = (modules, searchTerm) => {\n if (searchTerm === '') {\n return modules;\n }\n searchTerm = searchTerm.toLowerCase();\n const searchResults = [];\n modules.forEach((activity) => {\n const activityName = activity.title.toLowerCase();\n const activityDesc = activity.help.toLowerCase();\n if (activityName.includes(searchTerm) || activityDesc.includes(searchTerm)) {\n searchResults.push(activity);\n }\n });\n\n return searchResults;\n};\n\n/**\n * Set up our tabindex information across the chooser.\n *\n * @method setupKeyboardAccessibility\n * @param {Promise} modal Our created modal for the section\n * @param {Map} mappedModules A map of all of the built module information\n */\nconst setupKeyboardAccessibility = (modal, mappedModules) => {\n modal.getModal()[0].tabIndex = -1;\n\n modal.getBodyPromise().then(body => {\n $(selectors.elements.tab).on('shown.bs.tab', (e) => {\n const activeSectionId = e.target.getAttribute(\"href\");\n const activeSectionChooserOptions = body[0]\n .querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));\n const firstChooserOption = activeSectionChooserOptions\n .querySelector(selectors.regions.chooserOption.container);\n const prevActiveSectionId = e.relatedTarget.getAttribute(\"href\");\n const prevActiveSectionChooserOptions = body[0]\n .querySelector(selectors.regions.getSectionChooserOptions(prevActiveSectionId));\n\n // Disable the focus of every chooser option in the previous active section.\n disableFocusAllChooserOptions(prevActiveSectionChooserOptions);\n // Enable the focus of the first chooser option in the current active section.\n toggleFocusableChooserOption(firstChooserOption, true);\n initChooserOptionsKeyboardNavigation(body[0], mappedModules, activeSectionChooserOptions, modal);\n });\n return;\n }).catch(Notification.exception);\n};\n\n/**\n * Disable the focus of all chooser options in a specific container (section).\n *\n * @method disableFocusAllChooserOptions\n * @param {HTMLElement} sectionChooserOptions The section that contains the chooser items\n */\nconst disableFocusAllChooserOptions = (sectionChooserOptions) => {\n const allChooserOptions = sectionChooserOptions.querySelectorAll(selectors.regions.chooserOption.container);\n allChooserOptions.forEach((chooserOption) => {\n toggleFocusableChooserOption(chooserOption, false);\n });\n};\n\n/**\n * Display the module chooser.\n *\n * @method displayChooser\n * @param {Promise} modalPromise Our created modal for the section\n * @param {Array} sectionModules An array of all of the built module information\n * @param {Function} partialFavourite Partially applied function we need to manage favourite status\n * @param {Object} footerData Our base footer object.\n */\nexport const displayChooser = (modalPromise, sectionModules, partialFavourite, footerData) => {\n // Make a map so we can quickly fetch a specific module's object for either rendering or searching.\n const mappedModules = new Map();\n sectionModules.forEach((module) => {\n mappedModules.set(module.componentname + '_' + module.link, module);\n });\n\n // Register event listeners.\n modalPromise.then(modal => {\n registerListenerEvents(modal, mappedModules, partialFavourite, footerData);\n\n // We want to focus on the first chooser option element as soon as the modal is opened.\n setupKeyboardAccessibility(modal, mappedModules);\n\n // We want to focus on the action select when the dialog is closed.\n modal.getRoot().on(ModalEvents.hidden, () => {\n modal.destroy();\n });\n\n return modal;\n }).catch();\n};\n"],"file":"dialogue.min.js"} \ No newline at end of file +{"version":3,"sources":["../../../src/local/activitychooser/dialogue.js"],"names":["getPlugin","pluginName","showModuleHelp","carousel","moduleData","modal","showFooter","setFooter","Templates","render","help","find","selectors","regions","innerHTML","classList","add","spinnerPromise","transitionPromiseResolver","transitionPromise","Promise","resolve","contentPromise","renderForPromise","all","then","html","js","replaceNodeContents","querySelector","chooserSummary","header","focus","catch","Notification","exception","one","manageFavouriteState","modalBody","caller","partialFavourite","isFavourite","dataset","favourited","id","name","internal","Repository","unfavouriteModule","favouriteModule","registerListenerEvents","mappedModules","footerData","bodyClickListener","e","target","closest","actions","optionActions","showSummary","getBody","module","chooserOption","container","moduleName","modname","get","hasFooterContent","manageFavourite","activeSectionId","elements","activetab","getAttribute","sectionChooserOptions","getSectionChooserOptions","firstChooserOption","toggleFocusableChooserOption","initChooserOptionsKeyboardNavigation","matches","closeOption","on","allModules","modules","getModuleSelector","clearSearch","searchInput","search","value","toggleSearchResultsView","footerClickListener","footer","customfooterjs","footerjs","getBodyPromise","body","interval","pause","keyboard","addEventListener","getFooterPromise","chooserOptionsContainer","chooserOptions","querySelectorAll","Array","from","forEach","element","keyCode","enter","space","preventDefault","arrowRight","currentOption","nextOption","nextElementSibling","firstOption","firstElementChild","toFocusOption","clickErrorHandler","focusChooserOption","arrowLeft","previousOption","previousElementSibling","lastOption","lastElementChild","home","end","currentChooserOption","previousChooserOption","isFocusable","chooserOptionLink","addChooser","chooserOptionHelp","chooserOptionFavourite","tabIndex","item","fallback","renderSearchResults","searchResultsContainer","searchResultsData","templateData","length","searchQuery","searchResults","chooserContainer","chooser","clearSearchButton","searchModules","searchResultItemsContainer","searchResultItems","firstSearchResultItem","remove","setAttribute","removeAttribute","searchTerm","toLowerCase","activity","activityName","title","activityDesc","includes","push","setupKeyboardAccessibility","getModal","tab","activeSectionChooserOptions","prevActiveSectionId","relatedTarget","prevActiveSectionChooserOptions","disableFocusAllChooserOptions","allChooserOptions","displayChooser","modalPromise","sectionModules","Map","set","componentname","link","getRoot","ModalEvents","hidden","destroy"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * A type of dialogue used as for choosing options.\n *\n * @module core_course/local/chooser/dialogue\n * @package core\n * @copyright 2019 Mihail Geshoski \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport * as ModalEvents from 'core/modal_events';\nimport selectors from 'core_course/local/activitychooser/selectors';\nimport * as Templates from 'core/templates';\nimport {end, arrowLeft, arrowRight, home, enter, space} from 'core/key_codes';\nimport {addIconToContainer} from 'core/loadingicon';\nimport * as Repository from 'core_course/local/activitychooser/repository';\nimport Notification from 'core/notification';\nimport {debounce} from 'core/utils';\nconst getPlugin = pluginName => import(pluginName);\n\n/**\n * Given an event from the main module 'page' navigate to it's help section via a carousel.\n *\n * @method showModuleHelp\n * @param {jQuery} carousel Our initialized carousel to manipulate\n * @param {Object} moduleData Data of the module to carousel to\n * @param {jQuery} modal We need to figure out if the current modal has a footer.\n */\nconst showModuleHelp = (carousel, moduleData, modal = null) => {\n // If we have a real footer then we need to change temporarily.\n if (modal !== null && moduleData.showFooter === true) {\n modal.setFooter(Templates.render('core_course/local/activitychooser/footer_partial', moduleData));\n }\n const help = carousel.find(selectors.regions.help)[0];\n help.innerHTML = '';\n help.classList.add('m-auto');\n\n // Add a spinner.\n const spinnerPromise = addIconToContainer(help);\n\n // Used later...\n let transitionPromiseResolver = null;\n const transitionPromise = new Promise(resolve => {\n transitionPromiseResolver = resolve;\n });\n\n // Build up the html & js ready to place into the help section.\n const contentPromise = Templates.renderForPromise('core_course/local/activitychooser/help', moduleData);\n\n // Wait for the content to be ready, and for the transition to be complet.\n Promise.all([contentPromise, spinnerPromise, transitionPromise])\n .then(([{html, js}]) => Templates.replaceNodeContents(help, html, js))\n .then(() => {\n help.querySelector(selectors.regions.chooserSummary.header).focus();\n return help;\n })\n .catch(Notification.exception);\n\n // Move to the next slide, and resolve the transition promise when it's done.\n carousel.one('slid.bs.carousel', () => {\n transitionPromiseResolver();\n });\n // Trigger the transition between 'pages'.\n carousel.carousel('next');\n};\n\n/**\n * Given a user wants to change the favourite state of a module we either add or remove the status.\n * We also propergate this change across our map of modals.\n *\n * @method manageFavouriteState\n * @param {HTMLElement} modalBody The DOM node of the modal to manipulate\n * @param {HTMLElement} caller\n * @param {Function} partialFavourite Partially applied function we need to manage favourite status\n */\nconst manageFavouriteState = async(modalBody, caller, partialFavourite) => {\n const isFavourite = caller.dataset.favourited;\n const id = caller.dataset.id;\n const name = caller.dataset.name;\n const internal = caller.dataset.internal;\n // Switch on fave or not.\n if (isFavourite === 'true') {\n await Repository.unfavouriteModule(name, id);\n\n partialFavourite(internal, false, modalBody);\n } else {\n await Repository.favouriteModule(name, id);\n\n partialFavourite(internal, true, modalBody);\n }\n\n};\n\n/**\n * Register chooser related event listeners.\n *\n * @method registerListenerEvents\n * @param {Promise} modal Our modal that we are working with\n * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}\n * @param {Function} partialFavourite Partially applied function we need to manage favourite status\n * @param {Object} footerData Our base footer object.\n */\nconst registerListenerEvents = (modal, mappedModules, partialFavourite, footerData) => {\n const bodyClickListener = async(e) => {\n if (e.target.closest(selectors.actions.optionActions.showSummary)) {\n const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));\n\n const module = e.target.closest(selectors.regions.chooserOption.container);\n const moduleName = module.dataset.modname;\n const moduleData = mappedModules.get(moduleName);\n // We need to know if the overall modal has a footer so we know when to show a real / vs fake footer.\n moduleData.showFooter = modal.hasFooterContent();\n showModuleHelp(carousel, moduleData, modal);\n }\n\n if (e.target.closest(selectors.actions.optionActions.manageFavourite)) {\n const caller = e.target.closest(selectors.actions.optionActions.manageFavourite);\n await manageFavouriteState(modal.getBody()[0], caller, partialFavourite);\n const activeSectionId = modal.getBody()[0].querySelector(selectors.elements.activetab).getAttribute(\"href\");\n const sectionChooserOptions = modal.getBody()[0]\n .querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));\n const firstChooserOption = sectionChooserOptions\n .querySelector(selectors.regions.chooserOption.container);\n toggleFocusableChooserOption(firstChooserOption, true);\n initChooserOptionsKeyboardNavigation(modal.getBody()[0], mappedModules, sectionChooserOptions, modal);\n }\n\n // From the help screen go back to the module overview.\n if (e.target.matches(selectors.actions.closeOption)) {\n const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));\n\n // Trigger the transition between 'pages'.\n carousel.carousel('prev');\n carousel.on('slid.bs.carousel', () => {\n const allModules = modal.getBody()[0].querySelector(selectors.regions.modules);\n const caller = allModules.querySelector(selectors.regions.getModuleSelector(e.target.dataset.modname));\n caller.focus();\n });\n }\n\n // The \"clear search\" button is triggered.\n if (e.target.closest(selectors.actions.clearSearch)) {\n // Clear the entered search query in the search bar and hide the search results container.\n const searchInput = modal.getBody()[0].querySelector(selectors.actions.search);\n searchInput.value = \"\";\n searchInput.focus();\n toggleSearchResultsView(modal, mappedModules, searchInput.value);\n }\n };\n\n // We essentially have two types of footer.\n // A fake one that is handled within the template for chooser_help and then all of the stuff for\n // modal.footer. We need to ensure we know exactly what type of footer we are using so we know what we\n // need to manage. The below code handles a real footer going to a mnet carousel item.\n const footerClickListener = async(e) => {\n if (footerData.footer === true) {\n const footerjs = await getPlugin(footerData.customfooterjs);\n await footerjs.footerClickListener(e, footerData, modal);\n }\n };\n\n modal.getBodyPromise()\n\n // The return value of getBodyPromise is a jquery object containing the body NodeElement.\n .then(body => body[0])\n\n // Set up the carousel.\n .then(body => {\n $(body.querySelector(selectors.regions.carousel))\n .carousel({\n interval: false,\n pause: true,\n keyboard: false\n });\n\n return body;\n })\n\n // Add the listener for clicks on the body.\n .then(body => {\n body.addEventListener('click', bodyClickListener);\n return body;\n })\n\n // Add a listener for an input change in the activity chooser's search bar.\n .then(body => {\n const searchInput = body.querySelector(selectors.actions.search);\n // The search input is triggered.\n searchInput.addEventListener('input', debounce(() => {\n // Display the search results.\n toggleSearchResultsView(modal, mappedModules, searchInput.value);\n }, 300));\n return body;\n })\n\n // Register event listeners related to the keyboard navigation controls.\n .then(body => {\n // Get the active chooser options section.\n const activeSectionId = body.querySelector(selectors.elements.activetab).getAttribute(\"href\");\n const sectionChooserOptions = body.querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));\n const firstChooserOption = sectionChooserOptions.querySelector(selectors.regions.chooserOption.container);\n\n toggleFocusableChooserOption(firstChooserOption, true);\n initChooserOptionsKeyboardNavigation(body, mappedModules, sectionChooserOptions, modal);\n\n return body;\n })\n .catch();\n\n modal.getFooterPromise()\n\n // The return value of getBodyPromise is a jquery object containing the body NodeElement.\n .then(footer => footer[0])\n // Add the listener for clicks on the footer.\n .then(footer => {\n footer.addEventListener('click', footerClickListener);\n return footer;\n })\n .catch();\n};\n\n/**\n * Initialise the keyboard navigation controls for the chooser options.\n *\n * @method initChooserOptionsKeyboardNavigation\n * @param {HTMLElement} body Our modal that we are working with\n * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}\n * @param {HTMLElement} chooserOptionsContainer The section that contains the chooser items\n * @param {Object} modal Our created modal for the section\n */\nconst initChooserOptionsKeyboardNavigation = (body, mappedModules, chooserOptionsContainer, modal = null) => {\n const chooserOptions = chooserOptionsContainer.querySelectorAll(selectors.regions.chooserOption.container);\n\n Array.from(chooserOptions).forEach((element) => {\n return element.addEventListener('keydown', (e) => {\n\n // Check for enter/ space triggers for showing the help.\n if (e.keyCode === enter || e.keyCode === space) {\n if (e.target.matches(selectors.actions.optionActions.showSummary)) {\n e.preventDefault();\n const module = e.target.closest(selectors.regions.chooserOption.container);\n const moduleName = module.dataset.modname;\n const moduleData = mappedModules.get(moduleName);\n const carousel = $(body.querySelector(selectors.regions.carousel));\n carousel.carousel({\n interval: false,\n pause: true,\n keyboard: false\n });\n\n // We need to know if the overall modal has a footer so we know when to show a real / vs fake footer.\n moduleData.showFooter = modal.hasFooterContent();\n showModuleHelp(carousel, moduleData, modal);\n }\n }\n\n // Next.\n if (e.keyCode === arrowRight) {\n e.preventDefault();\n const currentOption = e.target.closest(selectors.regions.chooserOption.container);\n const nextOption = currentOption.nextElementSibling;\n const firstOption = chooserOptionsContainer.firstElementChild;\n const toFocusOption = clickErrorHandler(nextOption, firstOption);\n focusChooserOption(toFocusOption, currentOption);\n }\n\n // Previous.\n if (e.keyCode === arrowLeft) {\n e.preventDefault();\n const currentOption = e.target.closest(selectors.regions.chooserOption.container);\n const previousOption = currentOption.previousElementSibling;\n const lastOption = chooserOptionsContainer.lastElementChild;\n const toFocusOption = clickErrorHandler(previousOption, lastOption);\n focusChooserOption(toFocusOption, currentOption);\n }\n\n if (e.keyCode === home) {\n e.preventDefault();\n const currentOption = e.target.closest(selectors.regions.chooserOption.container);\n const firstOption = chooserOptionsContainer.firstElementChild;\n focusChooserOption(firstOption, currentOption);\n }\n\n if (e.keyCode === end) {\n e.preventDefault();\n const currentOption = e.target.closest(selectors.regions.chooserOption.container);\n const lastOption = chooserOptionsContainer.lastElementChild;\n focusChooserOption(lastOption, currentOption);\n }\n });\n });\n};\n\n/**\n * Focus on a chooser option element and remove the previous chooser element from the focus order\n *\n * @method focusChooserOption\n * @param {HTMLElement} currentChooserOption The current chooser option element that we want to focus\n * @param {HTMLElement|null} previousChooserOption The previous focused option element\n */\nconst focusChooserOption = (currentChooserOption, previousChooserOption = null) => {\n if (previousChooserOption !== null) {\n toggleFocusableChooserOption(previousChooserOption, false);\n }\n\n toggleFocusableChooserOption(currentChooserOption, true);\n currentChooserOption.focus();\n};\n\n/**\n * Add or remove a chooser option from the focus order.\n *\n * @method toggleFocusableChooserOption\n * @param {HTMLElement} chooserOption The chooser option element which should be added or removed from the focus order\n * @param {Boolean} isFocusable Whether the chooser element is focusable or not\n */\nconst toggleFocusableChooserOption = (chooserOption, isFocusable) => {\n const chooserOptionLink = chooserOption.querySelector(selectors.actions.addChooser);\n const chooserOptionHelp = chooserOption.querySelector(selectors.actions.optionActions.showSummary);\n const chooserOptionFavourite = chooserOption.querySelector(selectors.actions.optionActions.manageFavourite);\n\n if (isFocusable) {\n // Set tabindex to 0 to add current chooser option element to the focus order.\n chooserOption.tabIndex = 0;\n chooserOptionLink.tabIndex = 0;\n chooserOptionHelp.tabIndex = 0;\n chooserOptionFavourite.tabIndex = 0;\n } else {\n // Set tabindex to -1 to remove the previous chooser option element from the focus order.\n chooserOption.tabIndex = -1;\n chooserOptionLink.tabIndex = -1;\n chooserOptionHelp.tabIndex = -1;\n chooserOptionFavourite.tabIndex = -1;\n }\n};\n\n/**\n * Small error handling function to make sure the navigated to object exists\n *\n * @method clickErrorHandler\n * @param {HTMLElement} item What we want to check exists\n * @param {HTMLElement} fallback If we dont match anything fallback the focus\n * @return {HTMLElement}\n */\nconst clickErrorHandler = (item, fallback) => {\n if (item !== null) {\n return item;\n } else {\n return fallback;\n }\n};\n\n/**\n * Render the search results in a defined container\n *\n * @method renderSearchResults\n * @param {HTMLElement} searchResultsContainer The container where the data should be rendered\n * @param {Object} searchResultsData Data containing the module items that satisfy the search criteria\n */\nconst renderSearchResults = async(searchResultsContainer, searchResultsData) => {\n const templateData = {\n 'searchresultsnumber': searchResultsData.length,\n 'searchresults': searchResultsData\n };\n // Build up the html & js ready to place into the help section.\n const {html, js} = await Templates.renderForPromise('core_course/local/activitychooser/search_results', templateData);\n await Templates.replaceNodeContents(searchResultsContainer, html, js);\n};\n\n/**\n * Toggle (display/hide) the search results depending on the value of the search query\n *\n * @method toggleSearchResultsView\n * @param {Object} modal Our created modal for the section\n * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}\n * @param {String} searchQuery The search query\n */\nconst toggleSearchResultsView = async(modal, mappedModules, searchQuery) => {\n const modalBody = modal.getBody()[0];\n const searchResultsContainer = modalBody.querySelector(selectors.regions.searchResults);\n const chooserContainer = modalBody.querySelector(selectors.regions.chooser);\n const clearSearchButton = modalBody.querySelector(selectors.actions.clearSearch);\n\n if (searchQuery.length > 0) { // Search query is present.\n const searchResultsData = searchModules(mappedModules, searchQuery);\n await renderSearchResults(searchResultsContainer, searchResultsData);\n const searchResultItemsContainer = searchResultsContainer.querySelector(selectors.regions.searchResultItems);\n const firstSearchResultItem = searchResultItemsContainer.querySelector(selectors.regions.chooserOption.container);\n if (firstSearchResultItem) {\n // Set the first result item to be focusable.\n toggleFocusableChooserOption(firstSearchResultItem, true);\n // Register keyboard events on the created search result items.\n initChooserOptionsKeyboardNavigation(modalBody, mappedModules, searchResultItemsContainer, modal);\n }\n // Display the \"clear\" search button in the activity chooser search bar.\n clearSearchButton.classList.remove('d-none');\n // Hide the default chooser options container.\n chooserContainer.setAttribute('hidden', 'hidden');\n // Display the search results container.\n searchResultsContainer.removeAttribute('hidden');\n } else { // Search query is not present.\n // Hide the \"clear\" search button in the activity chooser search bar.\n clearSearchButton.classList.add('d-none');\n // Hide the search results container.\n searchResultsContainer.setAttribute('hidden', 'hidden');\n // Display the default chooser options container.\n chooserContainer.removeAttribute('hidden');\n }\n};\n\n/**\n * Return the list of modules which have a name or description that matches the given search term.\n *\n * @method searchModules\n * @param {Array} modules List of available modules\n * @param {String} searchTerm The search term to match\n * @return {Array}\n */\nconst searchModules = (modules, searchTerm) => {\n if (searchTerm === '') {\n return modules;\n }\n searchTerm = searchTerm.toLowerCase();\n const searchResults = [];\n modules.forEach((activity) => {\n const activityName = activity.title.toLowerCase();\n const activityDesc = activity.help.toLowerCase();\n if (activityName.includes(searchTerm) || activityDesc.includes(searchTerm)) {\n searchResults.push(activity);\n }\n });\n\n return searchResults;\n};\n\n/**\n * Set up our tabindex information across the chooser.\n *\n * @method setupKeyboardAccessibility\n * @param {Promise} modal Our created modal for the section\n * @param {Map} mappedModules A map of all of the built module information\n */\nconst setupKeyboardAccessibility = (modal, mappedModules) => {\n modal.getModal()[0].tabIndex = -1;\n\n modal.getBodyPromise().then(body => {\n $(selectors.elements.tab).on('shown.bs.tab', (e) => {\n const activeSectionId = e.target.getAttribute(\"href\");\n const activeSectionChooserOptions = body[0]\n .querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));\n const firstChooserOption = activeSectionChooserOptions\n .querySelector(selectors.regions.chooserOption.container);\n const prevActiveSectionId = e.relatedTarget.getAttribute(\"href\");\n const prevActiveSectionChooserOptions = body[0]\n .querySelector(selectors.regions.getSectionChooserOptions(prevActiveSectionId));\n\n // Disable the focus of every chooser option in the previous active section.\n disableFocusAllChooserOptions(prevActiveSectionChooserOptions);\n // Enable the focus of the first chooser option in the current active section.\n toggleFocusableChooserOption(firstChooserOption, true);\n initChooserOptionsKeyboardNavigation(body[0], mappedModules, activeSectionChooserOptions, modal);\n });\n return;\n }).catch(Notification.exception);\n};\n\n/**\n * Disable the focus of all chooser options in a specific container (section).\n *\n * @method disableFocusAllChooserOptions\n * @param {HTMLElement} sectionChooserOptions The section that contains the chooser items\n */\nconst disableFocusAllChooserOptions = (sectionChooserOptions) => {\n const allChooserOptions = sectionChooserOptions.querySelectorAll(selectors.regions.chooserOption.container);\n allChooserOptions.forEach((chooserOption) => {\n toggleFocusableChooserOption(chooserOption, false);\n });\n};\n\n/**\n * Display the module chooser.\n *\n * @method displayChooser\n * @param {Promise} modalPromise Our created modal for the section\n * @param {Array} sectionModules An array of all of the built module information\n * @param {Function} partialFavourite Partially applied function we need to manage favourite status\n * @param {Object} footerData Our base footer object.\n */\nexport const displayChooser = (modalPromise, sectionModules, partialFavourite, footerData) => {\n // Make a map so we can quickly fetch a specific module's object for either rendering or searching.\n const mappedModules = new Map();\n sectionModules.forEach((module) => {\n mappedModules.set(module.componentname + '_' + module.link, module);\n });\n\n // Register event listeners.\n modalPromise.then(modal => {\n registerListenerEvents(modal, mappedModules, partialFavourite, footerData);\n\n // We want to focus on the first chooser option element as soon as the modal is opened.\n setupKeyboardAccessibility(modal, mappedModules);\n\n // We want to focus on the action select when the dialog is closed.\n modal.getRoot().on(ModalEvents.hidden, () => {\n modal.destroy();\n });\n\n return modal;\n }).catch();\n};\n"],"file":"dialogue.min.js"} \ No newline at end of file diff --git a/course/amd/build/local/activitychooser/selectors.min.js b/course/amd/build/local/activitychooser/selectors.min.js index 45f8f4fd399..ea395d6217f 100644 --- a/course/amd/build/local/activitychooser/selectors.min.js +++ b/course/amd/build/local/activitychooser/selectors.min.js @@ -1,2 +1,2 @@ -define ("core_course/local/activitychooser/selectors",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;var b=function(a,b){return"[data-".concat(a,"=\"").concat(b,"\"]")},c={regions:{chooser:b("region","chooser-container"),getSectionChooserOptions:function getSectionChooserOptions(a){return"".concat(a," ").concat(b("region","chooser-options-container"))},chooserOption:{container:b("region","chooser-option-container"),actions:b("region","chooser-option-actions-container"),info:b("region","chooser-option-info-container")},chooserSummary:{container:b("region","chooser-option-summary-container"),content:b("region","chooser-option-summary-content-container"),header:b("region","summary-header"),actions:b("region","chooser-option-summary-actions-container")},carousel:b("region","carousel"),help:b("region","help"),modules:b("region","modules"),favouriteTabNav:b("region","favourite-tab-nav"),recommendedTabNav:b("region","recommended-tab-nav"),defaultTabNav:b("region","default-tab-nav"),activityTabNav:b("region","activity-tab-nav"),resourceTabNav:b("region","resources-tab-nav"),favouriteTab:b("region","favourites"),recommendedTab:b("region","recommended"),defaultTab:b("region","default"),activityTab:b("region","activity"),resourceTab:b("region","resources"),getModuleSelector:function getModuleSelector(a){return"[role=\"menuitem\"][data-modname=\"".concat(a,"\"]")},searchResults:b("region","search-results-container"),searchResultItems:b("region","search-result-items-container")},actions:{optionActions:{showSummary:b("action","show-option-summary"),manageFavourite:b("action","manage-module-favourite")},addChooser:b("action","add-chooser-option"),closeOption:b("action","close-chooser-option-summary"),hide:b("action","hide"),search:b("action","search"),clearSearch:b("action","clearsearch")},render:{favourites:b("render","favourites-area")},elements:{section:".section",sectionmodchooser:"button.section-modchooser-link",sitemenu:".block_site_main_menu",sitetopic:"div.sitetopic",tab:"a[data-toggle=\"tab\"]",activetab:"a[data-toggle=\"tab\"][aria-selected=\"true\"]",visibletabs:"a[data-toggle=\"tab\"]:not(.d-none)"}};a.default=c;return a.default}); +define ("core_course/local/activitychooser/selectors",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;var b=function(a,b){return"[data-".concat(a,"=\"").concat(b,"\"]")},c={regions:{chooser:b("region","chooser-container"),getSectionChooserOptions:function getSectionChooserOptions(a){return"".concat(a," ").concat(b("region","chooser-options-container"))},chooserOption:{container:b("region","chooser-option-container"),actions:b("region","chooser-option-actions-container"),info:b("region","chooser-option-info-container")},chooserSummary:{container:b("region","chooser-option-summary-container"),content:b("region","chooser-option-summary-content-container"),header:b("region","summary-header"),actions:b("region","chooser-option-summary-actions-container")},carousel:b("region","carousel"),help:b("region","help"),modules:b("region","modules"),favouriteTabNav:b("region","favourite-tab-nav"),defaultTabNav:b("region","default-tab-nav"),activityTabNav:b("region","activity-tab-nav"),favouriteTab:b("region","favourites"),recommendedTab:b("region","recommended"),defaultTab:b("region","default"),activityTab:b("region","activity"),resourceTab:b("region","resources"),getModuleSelector:function getModuleSelector(a){return"[role=\"menuitem\"][data-modname=\"".concat(a,"\"]")},searchResults:b("region","search-results-container"),searchResultItems:b("region","search-result-items-container")},actions:{optionActions:{showSummary:b("action","show-option-summary"),manageFavourite:b("action","manage-module-favourite")},addChooser:b("action","add-chooser-option"),closeOption:b("action","close-chooser-option-summary"),hide:b("action","hide"),search:b("action","search"),clearSearch:b("action","clearsearch")},render:{favourites:b("render","favourites-area")},elements:{section:".section",sectionmodchooser:"button.section-modchooser-link",sitemenu:".block_site_main_menu",sitetopic:"div.sitetopic",tab:"a[data-toggle=\"tab\"]",activetab:"a[data-toggle=\"tab\"][aria-selected=\"true\"]",visibletabs:"a[data-toggle=\"tab\"]:not(.d-none)"}};a.default=c;return a.default}); //# sourceMappingURL=selectors.min.js.map diff --git a/course/amd/build/local/activitychooser/selectors.min.js.map b/course/amd/build/local/activitychooser/selectors.min.js.map index 13f9e9f9fcc..25b6d79c3f1 100644 --- a/course/amd/build/local/activitychooser/selectors.min.js.map +++ b/course/amd/build/local/activitychooser/selectors.min.js.map @@ -1 +1 @@ -{"version":3,"sources":["../../../src/local/activitychooser/selectors.js"],"names":["getDataSelector","name","value","regions","chooser","getSectionChooserOptions","containerid","chooserOption","container","actions","info","chooserSummary","content","header","carousel","help","modules","favouriteTabNav","recommendedTabNav","defaultTabNav","activityTabNav","resourceTabNav","favouriteTab","recommendedTab","defaultTab","activityTab","resourceTab","getModuleSelector","modname","searchResults","searchResultItems","optionActions","showSummary","manageFavourite","addChooser","closeOption","hide","search","clearSearch","render","favourites","elements","section","sectionmodchooser","sitemenu","sitetopic","tab","activetab","visibletabs"],"mappings":"gKA+BMA,CAAAA,CAAe,CAAG,SAACC,CAAD,CAAOC,CAAP,CAAiB,CACrC,sBAAgBD,CAAhB,eAAyBC,CAAzB,OACH,C,GAEc,CACXC,OAAO,CAAE,CACLC,OAAO,CAAEJ,CAAe,CAAC,QAAD,CAAW,mBAAX,CADnB,CAELK,wBAAwB,CAAE,kCAAAC,CAAW,kBAAOA,CAAP,aAAsBN,CAAe,CAAC,QAAD,CAAW,2BAAX,CAArC,EAFhC,CAGLO,aAAa,CAAE,CACXC,SAAS,CAAER,CAAe,CAAC,QAAD,CAAW,0BAAX,CADf,CAEXS,OAAO,CAAET,CAAe,CAAC,QAAD,CAAW,kCAAX,CAFb,CAGXU,IAAI,CAAEV,CAAe,CAAC,QAAD,CAAW,+BAAX,CAHV,CAHV,CAQLW,cAAc,CAAE,CACZH,SAAS,CAAER,CAAe,CAAC,QAAD,CAAW,kCAAX,CADd,CAEZY,OAAO,CAAEZ,CAAe,CAAC,QAAD,CAAW,0CAAX,CAFZ,CAGZa,MAAM,CAAEb,CAAe,CAAC,QAAD,CAAW,gBAAX,CAHX,CAIZS,OAAO,CAAET,CAAe,CAAC,QAAD,CAAW,0CAAX,CAJZ,CARX,CAcLc,QAAQ,CAAEd,CAAe,CAAC,QAAD,CAAW,UAAX,CAdpB,CAeLe,IAAI,CAAEf,CAAe,CAAC,QAAD,CAAW,MAAX,CAfhB,CAgBLgB,OAAO,CAAEhB,CAAe,CAAC,QAAD,CAAW,SAAX,CAhBnB,CAiBLiB,eAAe,CAAEjB,CAAe,CAAC,QAAD,CAAW,mBAAX,CAjB3B,CAkBLkB,iBAAiB,CAAElB,CAAe,CAAC,QAAD,CAAW,qBAAX,CAlB7B,CAmBLmB,aAAa,CAAEnB,CAAe,CAAC,QAAD,CAAW,iBAAX,CAnBzB,CAoBLoB,cAAc,CAAEpB,CAAe,CAAC,QAAD,CAAW,kBAAX,CApB1B,CAqBLqB,cAAc,CAAErB,CAAe,CAAC,QAAD,CAAW,mBAAX,CArB1B,CAsBLsB,YAAY,CAAEtB,CAAe,CAAC,QAAD,CAAW,YAAX,CAtBxB,CAuBLuB,cAAc,CAAEvB,CAAe,CAAC,QAAD,CAAW,aAAX,CAvB1B,CAwBLwB,UAAU,CAAExB,CAAe,CAAC,QAAD,CAAW,SAAX,CAxBtB,CAyBLyB,WAAW,CAAEzB,CAAe,CAAC,QAAD,CAAW,UAAX,CAzBvB,CA0BL0B,WAAW,CAAE1B,CAAe,CAAC,QAAD,CAAW,WAAX,CA1BvB,CA2BL2B,iBAAiB,CAAE,2BAAAC,CAAO,qDAAuCA,CAAvC,QA3BrB,CA4BLC,aAAa,CAAE7B,CAAe,CAAC,QAAD,CAAW,0BAAX,CA5BzB,CA6BL8B,iBAAiB,CAAE9B,CAAe,CAAC,QAAD,CAAW,+BAAX,CA7B7B,CADE,CAgCXS,OAAO,CAAE,CACLsB,aAAa,CAAE,CACXC,WAAW,CAAEhC,CAAe,CAAC,QAAD,CAAW,qBAAX,CADjB,CAEXiC,eAAe,CAAEjC,CAAe,CAAC,QAAD,CAAW,yBAAX,CAFrB,CADV,CAKLkC,UAAU,CAAElC,CAAe,CAAC,QAAD,CAAW,oBAAX,CALtB,CAMLmC,WAAW,CAAEnC,CAAe,CAAC,QAAD,CAAW,8BAAX,CANvB,CAOLoC,IAAI,CAAEpC,CAAe,CAAC,QAAD,CAAW,MAAX,CAPhB,CAQLqC,MAAM,CAAErC,CAAe,CAAC,QAAD,CAAW,QAAX,CARlB,CASLsC,WAAW,CAAEtC,CAAe,CAAC,QAAD,CAAW,aAAX,CATvB,CAhCE,CA2CXuC,MAAM,CAAE,CACJC,UAAU,CAAExC,CAAe,CAAC,QAAD,CAAW,iBAAX,CADvB,CA3CG,CA8CXyC,QAAQ,CAAE,CACNC,OAAO,CAAE,UADH,CAENC,iBAAiB,CAAE,gCAFb,CAGNC,QAAQ,CAAE,uBAHJ,CAINC,SAAS,CAAE,eAJL,CAKNC,GAAG,CAAE,wBALC,CAMNC,SAAS,CAAE,gDANL,CAONC,WAAW,CAAE,qCAPP,CA9CC,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Define all of the selectors we will be using on the grading interface.\n *\n * @module core_course/local/chooser/selectors\n * @package core_course\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * A small helper function to build queryable data selectors.\n * @method getDataSelector\n * @param {String} name\n * @param {String} value\n * @return {string}\n */\nconst getDataSelector = (name, value) => {\n return `[data-${name}=\"${value}\"]`;\n};\n\nexport default {\n regions: {\n chooser: getDataSelector('region', 'chooser-container'),\n getSectionChooserOptions: containerid => `${containerid} ${getDataSelector('region', 'chooser-options-container')}`,\n chooserOption: {\n container: getDataSelector('region', 'chooser-option-container'),\n actions: getDataSelector('region', 'chooser-option-actions-container'),\n info: getDataSelector('region', 'chooser-option-info-container'),\n },\n chooserSummary: {\n container: getDataSelector('region', 'chooser-option-summary-container'),\n content: getDataSelector('region', 'chooser-option-summary-content-container'),\n header: getDataSelector('region', 'summary-header'),\n actions: getDataSelector('region', 'chooser-option-summary-actions-container'),\n },\n carousel: getDataSelector('region', 'carousel'),\n help: getDataSelector('region', 'help'),\n modules: getDataSelector('region', 'modules'),\n favouriteTabNav: getDataSelector('region', 'favourite-tab-nav'),\n recommendedTabNav: getDataSelector('region', 'recommended-tab-nav'),\n defaultTabNav: getDataSelector('region', 'default-tab-nav'),\n activityTabNav: getDataSelector('region', 'activity-tab-nav'),\n resourceTabNav: getDataSelector('region', 'resources-tab-nav'),\n favouriteTab: getDataSelector('region', 'favourites'),\n recommendedTab: getDataSelector('region', 'recommended'),\n defaultTab: getDataSelector('region', 'default'),\n activityTab: getDataSelector('region', 'activity'),\n resourceTab: getDataSelector('region', 'resources'),\n getModuleSelector: modname => `[role=\"menuitem\"][data-modname=\"${modname}\"]`,\n searchResults: getDataSelector('region', 'search-results-container'),\n searchResultItems: getDataSelector('region', 'search-result-items-container'),\n },\n actions: {\n optionActions: {\n showSummary: getDataSelector('action', 'show-option-summary'),\n manageFavourite: getDataSelector('action', 'manage-module-favourite'),\n },\n addChooser: getDataSelector('action', 'add-chooser-option'),\n closeOption: getDataSelector('action', 'close-chooser-option-summary'),\n hide: getDataSelector('action', 'hide'),\n search: getDataSelector('action', 'search'),\n clearSearch: getDataSelector('action', 'clearsearch'),\n },\n render: {\n favourites: getDataSelector('render', 'favourites-area'),\n },\n elements: {\n section: '.section',\n sectionmodchooser: 'button.section-modchooser-link',\n sitemenu: '.block_site_main_menu',\n sitetopic: 'div.sitetopic',\n tab: 'a[data-toggle=\"tab\"]',\n activetab: 'a[data-toggle=\"tab\"][aria-selected=\"true\"]',\n visibletabs: 'a[data-toggle=\"tab\"]:not(.d-none)'\n },\n};\n"],"file":"selectors.min.js"} \ No newline at end of file +{"version":3,"sources":["../../../src/local/activitychooser/selectors.js"],"names":["getDataSelector","name","value","regions","chooser","getSectionChooserOptions","containerid","chooserOption","container","actions","info","chooserSummary","content","header","carousel","help","modules","favouriteTabNav","defaultTabNav","activityTabNav","favouriteTab","recommendedTab","defaultTab","activityTab","resourceTab","getModuleSelector","modname","searchResults","searchResultItems","optionActions","showSummary","manageFavourite","addChooser","closeOption","hide","search","clearSearch","render","favourites","elements","section","sectionmodchooser","sitemenu","sitetopic","tab","activetab","visibletabs"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Define all of the selectors we will be using on the grading interface.\n *\n * @module core_course/local/chooser/selectors\n * @package core_course\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * A small helper function to build queryable data selectors.\n * @method getDataSelector\n * @param {String} name\n * @param {String} value\n * @return {string}\n */\nconst getDataSelector = (name, value) => {\n return `[data-${name}=\"${value}\"]`;\n};\n\nexport default {\n regions: {\n chooser: getDataSelector('region', 'chooser-container'),\n getSectionChooserOptions: containerid => `${containerid} ${getDataSelector('region', 'chooser-options-container')}`,\n chooserOption: {\n container: getDataSelector('region', 'chooser-option-container'),\n actions: getDataSelector('region', 'chooser-option-actions-container'),\n info: getDataSelector('region', 'chooser-option-info-container'),\n },\n chooserSummary: {\n container: getDataSelector('region', 'chooser-option-summary-container'),\n content: getDataSelector('region', 'chooser-option-summary-content-container'),\n header: getDataSelector('region', 'summary-header'),\n actions: getDataSelector('region', 'chooser-option-summary-actions-container'),\n },\n carousel: getDataSelector('region', 'carousel'),\n help: getDataSelector('region', 'help'),\n modules: getDataSelector('region', 'modules'),\n favouriteTabNav: getDataSelector('region', 'favourite-tab-nav'),\n defaultTabNav: getDataSelector('region', 'default-tab-nav'),\n activityTabNav: getDataSelector('region', 'activity-tab-nav'),\n favouriteTab: getDataSelector('region', 'favourites'),\n recommendedTab: getDataSelector('region', 'recommended'),\n defaultTab: getDataSelector('region', 'default'),\n activityTab: getDataSelector('region', 'activity'),\n resourceTab: getDataSelector('region', 'resources'),\n getModuleSelector: modname => `[role=\"menuitem\"][data-modname=\"${modname}\"]`,\n searchResults: getDataSelector('region', 'search-results-container'),\n searchResultItems: getDataSelector('region', 'search-result-items-container'),\n },\n actions: {\n optionActions: {\n showSummary: getDataSelector('action', 'show-option-summary'),\n manageFavourite: getDataSelector('action', 'manage-module-favourite'),\n },\n addChooser: getDataSelector('action', 'add-chooser-option'),\n closeOption: getDataSelector('action', 'close-chooser-option-summary'),\n hide: getDataSelector('action', 'hide'),\n search: getDataSelector('action', 'search'),\n clearSearch: getDataSelector('action', 'clearsearch'),\n },\n render: {\n favourites: getDataSelector('render', 'favourites-area'),\n },\n elements: {\n section: '.section',\n sectionmodchooser: 'button.section-modchooser-link',\n sitemenu: '.block_site_main_menu',\n sitetopic: 'div.sitetopic',\n tab: 'a[data-toggle=\"tab\"]',\n activetab: 'a[data-toggle=\"tab\"][aria-selected=\"true\"]',\n visibletabs: 'a[data-toggle=\"tab\"]:not(.d-none)'\n },\n};\n"],"file":"selectors.min.js"} \ No newline at end of file diff --git a/course/amd/src/local/activitychooser/dialogue.js b/course/amd/src/local/activitychooser/dialogue.js index a3bde76d89f..a7c701824ab 100644 --- a/course/amd/src/local/activitychooser/dialogue.js +++ b/course/amd/src/local/activitychooser/dialogue.js @@ -216,7 +216,6 @@ const registerListenerEvents = (modal, mappedModules, partialFavourite, footerDa const firstChooserOption = sectionChooserOptions.querySelector(selectors.regions.chooserOption.container); toggleFocusableChooserOption(firstChooserOption, true); - initTabsKeyboardNavigation(body); initChooserOptionsKeyboardNavigation(body, mappedModules, sectionChooserOptions, modal); return body; @@ -235,77 +234,6 @@ const registerListenerEvents = (modal, mappedModules, partialFavourite, footerDa .catch(); }; -/** - * Initialise the keyboard navigation controls for the tab list items. - * - * @method initTabsKeyboardNavigation - * @param {HTMLElement} body Our modal that we are working with - */ -const initTabsKeyboardNavigation = (body) => { - // Set up the tab handlers. - const favTabNav = body.querySelector(selectors.regions.favouriteTabNav); - const recommendedTabNav = body.querySelector(selectors.regions.recommendedTabNav); - const defaultTabNav = body.querySelector(selectors.regions.defaultTabNav); - const activityTabNav = body.querySelector(selectors.regions.activityTabNav); - const resourceTabNav = body.querySelector(selectors.regions.resourceTabNav); - const tabNavArray = [favTabNav, recommendedTabNav, defaultTabNav, activityTabNav, resourceTabNav]; - tabNavArray.forEach((element) => { - return element.addEventListener('keydown', (e) => { - // The first visible navigation tab link. - const firstLink = e.target.parentElement.querySelector(selectors.elements.visibletabs); - // The last navigation tab link. It would always be the default activities tab link. - const lastLink = e.target.parentElement.lastElementChild; - - if (e.keyCode === arrowRight) { - const nextLink = e.target.nextElementSibling; - if (nextLink === null) { - e.target.tabIndex = -1; - firstLink.tabIndex = 0; - firstLink.focus(); - } else if (nextLink.classList.contains('d-none')) { - e.target.tabIndex = -1; - lastLink.tabIndex = 0; - lastLink.focus(); - } else { - e.target.tabIndex = -1; - nextLink.tabIndex = 0; - nextLink.focus(); - } - } - if (e.keyCode === arrowLeft) { - const previousLink = e.target.previousElementSibling; - if (previousLink === null) { - e.target.tabIndex = -1; - lastLink.tabIndex = 0; - lastLink.focus(); - } else if (previousLink.classList.contains('d-none')) { - e.target.tabIndex = -1; - firstLink.tabIndex = 0; - firstLink.focus(); - } else { - e.target.tabIndex = -1; - previousLink.tabIndex = 0; - previousLink.focus(); - } - } - if (e.keyCode === home) { - e.target.tabIndex = -1; - firstLink.tabIndex = 0; - firstLink.focus(); - } - if (e.keyCode === end) { - e.target.tabIndex = -1; - lastLink.tabIndex = 0; - lastLink.focus(); - } - if (e.keyCode === space) { - e.preventDefault(); - e.target.click(); - } - }); - }); -}; - /** * Initialise the keyboard navigation controls for the chooser options. * diff --git a/course/amd/src/local/activitychooser/selectors.js b/course/amd/src/local/activitychooser/selectors.js index dd20e756c9f..34e9adb112f 100644 --- a/course/amd/src/local/activitychooser/selectors.js +++ b/course/amd/src/local/activitychooser/selectors.js @@ -52,10 +52,8 @@ export default { help: getDataSelector('region', 'help'), modules: getDataSelector('region', 'modules'), favouriteTabNav: getDataSelector('region', 'favourite-tab-nav'), - recommendedTabNav: getDataSelector('region', 'recommended-tab-nav'), defaultTabNav: getDataSelector('region', 'default-tab-nav'), activityTabNav: getDataSelector('region', 'activity-tab-nav'), - resourceTabNav: getDataSelector('region', 'resources-tab-nav'), favouriteTab: getDataSelector('region', 'favourites'), recommendedTab: getDataSelector('region', 'recommended'), defaultTab: getDataSelector('region', 'default'), diff --git a/enrol/cohort/settings.php b/enrol/cohort/settings.php index 8c08a745d8e..50cd5dcddc3 100644 --- a/enrol/cohort/settings.php +++ b/enrol/cohort/settings.php @@ -36,7 +36,7 @@ if ($ADMIN->fulltree) { $student = get_archetype_roles('student'); $student = reset($student); $settings->add(new admin_setting_configselect('enrol_cohort/roleid', - get_string('defaultrole', 'role'), '', $student->id, $options)); + get_string('defaultrole', 'role'), '', $student->id ?? null, $options)); $options = array( ENROL_EXT_REMOVED_UNENROL => get_string('extremovedunenrol', 'enrol'), diff --git a/enrol/database/settings.php b/enrol/database/settings.php index 28c75044f2f..92d0833b3fd 100644 --- a/enrol/database/settings.php +++ b/enrol/database/settings.php @@ -86,7 +86,11 @@ if ($ADMIN->fulltree) { $options = get_default_enrol_roles(context_system::instance()); $student = get_archetype_roles('student'); $student = reset($student); - $settings->add(new admin_setting_configselect('enrol_database/defaultrole', get_string('defaultrole', 'enrol_database'), get_string('defaultrole_desc', 'enrol_database'), $student->id, $options)); + $settings->add(new admin_setting_configselect('enrol_database/defaultrole', + get_string('defaultrole', 'enrol_database'), + get_string('defaultrole_desc', 'enrol_database'), + $student->id ?? null, + $options)); } $settings->add(new admin_setting_configcheckbox('enrol_database/ignorehiddencourses', get_string('ignorehiddencourses', 'enrol_database'), get_string('ignorehiddencourses_desc', 'enrol_database'), 0)); diff --git a/enrol/externallib.php b/enrol/externallib.php index d0fbaa9453a..5189a3a7edd 100644 --- a/enrol/externallib.php +++ b/enrol/externallib.php @@ -679,28 +679,34 @@ class core_enrol_external extends external_api { */ public static function get_enrolled_users_parameters() { return new external_function_parameters( - array( + [ 'courseid' => new external_value(PARAM_INT, 'course id'), 'options' => new external_multiple_structure( new external_single_structure( - array( + [ 'name' => new external_value(PARAM_ALPHANUMEXT, 'option name'), 'value' => new external_value(PARAM_RAW, 'option value') - ) + ] ), 'Option names: * withcapability (string) return only users with this capability. This option requires \'moodle/role:review\' on the course context. * groupid (integer) return only users in this group id. If the course has groups enabled and this param isn\'t defined, returns all the viewable users. This option requires \'moodle/site:accessallgroups\' on the course context if the user doesn\'t belong to the group. - * onlyactive (integer) return only users with active enrolments and matching time restrictions. This option requires \'moodle/course:enrolreview\' on the course context. + * onlyactive (integer) return only users with active enrolments and matching time restrictions. + This option requires \'moodle/course:enrolreview\' on the course context. + Please note that this option can\'t + be used together with onlysuspended (only one can be active). + * onlysuspended (integer) return only suspended users. This option requires + \'moodle/course:enrolreview\' on the course context. Please note that this option can\'t + be used together with onlyactive (only one can be active). * userfields (\'string, string, ...\') return only the values of these user fields. * limitfrom (integer) sql limit from. * limitnumber (integer) maximum number of returned users. * sortby (string) sort by id, firstname or lastname. For ordering like the site does, use siteorder. * sortdirection (string) ASC or DESC', - VALUE_DEFAULT, array()), - ) + VALUE_DEFAULT, []), + ] ); } @@ -714,7 +720,7 @@ class core_enrol_external extends external_api { * } * @return array An array of users */ - public static function get_enrolled_users($courseid, $options = array()) { + public static function get_enrolled_users($courseid, $options = []) { global $CFG, $USER, $DB; require_once($CFG->dirroot . '/course/lib.php'); @@ -722,67 +728,71 @@ class core_enrol_external extends external_api { $params = self::validate_parameters( self::get_enrolled_users_parameters(), - array( + [ 'courseid'=>$courseid, 'options'=>$options - ) + ] ); $withcapability = ''; $groupid = 0; $onlyactive = false; - $userfields = array(); + $onlysuspended = false; + $userfields = []; $limitfrom = 0; $limitnumber = 0; $sortby = 'us.id'; - $sortparams = array(); + $sortparams = []; $sortdirection = 'ASC'; foreach ($options as $option) { switch ($option['name']) { - case 'withcapability': - $withcapability = $option['value']; - break; - case 'groupid': - $groupid = (int)$option['value']; - break; - case 'onlyactive': - $onlyactive = !empty($option['value']); - break; - case 'userfields': - $thefields = explode(',', $option['value']); - foreach ($thefields as $f) { - $userfields[] = clean_param($f, PARAM_ALPHANUMEXT); - } - break; - case 'limitfrom' : - $limitfrom = clean_param($option['value'], PARAM_INT); - break; - case 'limitnumber' : - $limitnumber = clean_param($option['value'], PARAM_INT); - break; - case 'sortby': - $sortallowedvalues = array('id', 'firstname', 'lastname', 'siteorder'); - if (!in_array($option['value'], $sortallowedvalues)) { - throw new invalid_parameter_exception('Invalid value for sortby parameter (value: ' . $option['value'] . '),' . - 'allowed values are: ' . implode(',', $sortallowedvalues)); - } - if ($option['value'] == 'siteorder') { - list($sortby, $sortparams) = users_order_by_sql('us'); - } else { - $sortby = 'us.' . $option['value']; - } - break; - case 'sortdirection': - $sortdirection = strtoupper($option['value']); - $directionallowedvalues = array('ASC', 'DESC'); - if (!in_array($sortdirection, $directionallowedvalues)) { - throw new invalid_parameter_exception('Invalid value for sortdirection parameter + case 'withcapability': + $withcapability = $option['value']; + break; + case 'groupid': + $groupid = (int)$option['value']; + break; + case 'onlyactive': + $onlyactive = !empty($option['value']); + break; + case 'onlysuspended': + $onlysuspended = !empty($option['value']); + break; + case 'userfields': + $thefields = explode(',', $option['value']); + foreach ($thefields as $f) { + $userfields[] = clean_param($f, PARAM_ALPHANUMEXT); + } + break; + case 'limitfrom' : + $limitfrom = clean_param($option['value'], PARAM_INT); + break; + case 'limitnumber' : + $limitnumber = clean_param($option['value'], PARAM_INT); + break; + case 'sortby': + $sortallowedvalues = ['id', 'firstname', 'lastname', 'siteorder']; + if (!in_array($option['value'], $sortallowedvalues)) { + throw new invalid_parameter_exception('Invalid value for sortby parameter (value: ' . + $option['value'] . '), allowed values are: ' . implode(',', $sortallowedvalues)); + } + if ($option['value'] == 'siteorder') { + list($sortby, $sortparams) = users_order_by_sql('us'); + } else { + $sortby = 'us.' . $option['value']; + } + break; + case 'sortdirection': + $sortdirection = strtoupper($option['value']); + $directionallowedvalues = ['ASC', 'DESC']; + if (!in_array($sortdirection, $directionallowedvalues)) { + throw new invalid_parameter_exception('Invalid value for sortdirection parameter (value: ' . $sortdirection . '),' . 'allowed values are: ' . implode(',', $directionallowedvalues)); - } - break; + } + break; } } - $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST); + $course = $DB->get_record('course', ['id' => $courseid], '*', MUST_EXIST); $coursecontext = context_course::instance($courseid, IGNORE_MISSING); if ($courseid == SITEID) { $context = context_system::instance(); @@ -809,11 +819,12 @@ class core_enrol_external extends external_api { require_capability('moodle/site:accessallgroups', $coursecontext); } // to overwrite this option, you need course:enrolereview permission - if ($onlyactive) { + if ($onlyactive || $onlysuspended) { require_capability('moodle/course:enrolreview', $coursecontext); } - list($enrolledsql, $enrolledparams) = get_enrolled_sql($coursecontext, $withcapability, $groupid, $onlyactive); + list($enrolledsql, $enrolledparams) = get_enrolled_sql($coursecontext, $withcapability, $groupid, $onlyactive, + $onlysuspended); $ctxselect = ', ' . context_helper::get_preload_record_columns_sql('ctx'); $ctxjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = u.id AND ctx.contextlevel = :contextlevel)"; $enrolledparams['contextlevel'] = CONTEXT_USER; @@ -829,7 +840,7 @@ class core_enrol_external extends external_api { $enrolledparams = array_merge($enrolledparams, $groupparams); } else { // User doesn't belong to any group, so he can't see any user. Return an empty array. - return array(); + return []; } } $sql = "SELECT us.*, COALESCE(ul.timeaccess, 0) AS lastcourseaccess @@ -845,7 +856,7 @@ class core_enrol_external extends external_api { $enrolledparams['courseid'] = $courseid; $enrolledusers = $DB->get_recordset_sql($sql, $enrolledparams, $limitfrom, $limitnumber); - $users = array(); + $users = []; foreach ($enrolledusers as $user) { context_helper::preload_from_record($user); if ($userdetails = user_get_user_details($user, $course, $userfields)) { @@ -865,7 +876,7 @@ class core_enrol_external extends external_api { public static function get_enrolled_users_returns() { return new external_multiple_structure( new external_single_structure( - array( + [ 'id' => new external_value(PARAM_INT, 'ID of the user'), 'username' => new external_value(PARAM_RAW, 'Username policy is defined in Moodle security config', VALUE_OPTIONAL), 'firstname' => new external_value(PARAM_NOTAGS, 'The first name(s) of the user', VALUE_OPTIONAL), @@ -896,47 +907,47 @@ class core_enrol_external extends external_api { 'profileimageurl' => new external_value(PARAM_URL, 'User image profile URL - big version', VALUE_OPTIONAL), 'customfields' => new external_multiple_structure( new external_single_structure( - array( + [ 'type' => new external_value(PARAM_ALPHANUMEXT, 'The type of the custom field - text field, checkbox...'), 'value' => new external_value(PARAM_RAW, 'The value of the custom field'), 'name' => new external_value(PARAM_RAW, 'The name of the custom field'), 'shortname' => new external_value(PARAM_RAW, 'The shortname of the custom field - to be able to build the field class in the code'), - ) + ] ), 'User custom fields (also known as user profil fields)', VALUE_OPTIONAL), 'groups' => new external_multiple_structure( new external_single_structure( - array( + [ 'id' => new external_value(PARAM_INT, 'group id'), 'name' => new external_value(PARAM_RAW, 'group name'), 'description' => new external_value(PARAM_RAW, 'group description'), 'descriptionformat' => new external_format_value('description'), - ) + ] ), 'user groups', VALUE_OPTIONAL), 'roles' => new external_multiple_structure( new external_single_structure( - array( + [ 'roleid' => new external_value(PARAM_INT, 'role id'), 'name' => new external_value(PARAM_RAW, 'role name'), 'shortname' => new external_value(PARAM_ALPHANUMEXT, 'role shortname'), 'sortorder' => new external_value(PARAM_INT, 'role sortorder') - ) + ] ), 'user roles', VALUE_OPTIONAL), 'preferences' => new external_multiple_structure( new external_single_structure( - array( + [ 'name' => new external_value(PARAM_RAW, 'The name of the preferences'), 'value' => new external_value(PARAM_RAW, 'The value of the custom field'), - ) + ] ), 'User preferences', VALUE_OPTIONAL), 'enrolledcourses' => new external_multiple_structure( new external_single_structure( - array( + [ 'id' => new external_value(PARAM_INT, 'Id of the course'), 'fullname' => new external_value(PARAM_RAW, 'Fullname of the course'), 'shortname' => new external_value(PARAM_RAW, 'Shortname of the course') - ) + ] ), 'Courses where the user is enrolled - limited by which courses the user is able to see', VALUE_OPTIONAL) - ) + ] ) ); } diff --git a/enrol/fee/settings.php b/enrol/fee/settings.php index 2f383b8a474..50d919ea555 100644 --- a/enrol/fee/settings.php +++ b/enrol/fee/settings.php @@ -71,7 +71,7 @@ if ($ADMIN->fulltree) { $student = get_archetype_roles('student'); $student = reset($student); $settings->add(new admin_setting_configselect('enrol_fee/roleid', - get_string('defaultrole', 'enrol_fee'), get_string('defaultrole_desc', 'enrol_fee'), $student->id, $options)); + get_string('defaultrole', 'enrol_fee'), get_string('defaultrole_desc', 'enrol_fee'), $student->id ?? null, $options)); } $settings->add(new admin_setting_configduration('enrol_fee/enrolperiod', diff --git a/enrol/manual/settings.php b/enrol/manual/settings.php index 2056c196dc3..8dcabcc5951 100644 --- a/enrol/manual/settings.php +++ b/enrol/manual/settings.php @@ -63,7 +63,7 @@ if ($ADMIN->fulltree) { $student = get_archetype_roles('student'); $student = reset($student); $settings->add(new admin_setting_configselect('enrol_manual/roleid', - get_string('defaultrole', 'role'), '', $student->id, $options)); + get_string('defaultrole', 'role'), '', $student->id ?? null, $options)); } $options = array(2 => get_string('coursestart'), 3 => get_string('today'), 4 => get_string('now', 'enrol_manual')); diff --git a/enrol/mnet/settings.php b/enrol/mnet/settings.php index fc7517faa89..c74c8e19a0b 100644 --- a/enrol/mnet/settings.php +++ b/enrol/mnet/settings.php @@ -36,6 +36,6 @@ if ($ADMIN->fulltree) { $student = reset($student); $settings->add(new admin_setting_configselect_with_advanced('enrol_mnet/roleid', get_string('defaultrole', 'role'), '', - array('value'=>$student->id, 'adv'=>true), $options)); + array('value' => $student->id ?? null, 'adv' => true), $options)); } } diff --git a/enrol/paypal/settings.php b/enrol/paypal/settings.php index 66e0ae4926b..d9a3a7fea33 100644 --- a/enrol/paypal/settings.php +++ b/enrol/paypal/settings.php @@ -66,7 +66,10 @@ if ($ADMIN->fulltree) { $student = get_archetype_roles('student'); $student = reset($student); $settings->add(new admin_setting_configselect('enrol_paypal/roleid', - get_string('defaultrole', 'enrol_paypal'), get_string('defaultrole_desc', 'enrol_paypal'), $student->id, $options)); + get_string('defaultrole', 'enrol_paypal'), + get_string('defaultrole_desc', 'enrol_paypal'), + $student->id ?? null, + $options)); } $settings->add(new admin_setting_configduration('enrol_paypal/enrolperiod', diff --git a/enrol/self/settings.php b/enrol/self/settings.php index 6375ea2a100..cee5d7fc21b 100644 --- a/enrol/self/settings.php +++ b/enrol/self/settings.php @@ -79,7 +79,10 @@ if ($ADMIN->fulltree) { $student = get_archetype_roles('student'); $student = reset($student); $settings->add(new admin_setting_configselect('enrol_self/roleid', - get_string('defaultrole', 'enrol_self'), get_string('defaultrole_desc', 'enrol_self'), $student->id, $options)); + get_string('defaultrole', 'enrol_self'), + get_string('defaultrole_desc', 'enrol_self'), + $student->id ?? null, + $options)); } $settings->add(new admin_setting_configduration('enrol_self/enrolperiod', diff --git a/enrol/tests/externallib_test.php b/enrol/tests/externallib_test.php index a12df0bb38b..c98df46ca47 100644 --- a/enrol/tests/externallib_test.php +++ b/enrol/tests/externallib_test.php @@ -355,6 +355,95 @@ class core_enrol_externallib_testcase extends externallib_advanced_testcase { } } + /** + * Verify get_enrolled_users() returned users according to their status. + */ + public function test_get_enrolled_users_active_suspended() { + global $USER; + + $this->resetAfterTest(); + + // Create the course and the users. + $course = $this->getDataGenerator()->create_course(); + $coursecontext = context_course::instance($course->id); + $user0 = $this->getDataGenerator()->create_user(['username' => 'user0active']); + $user1 = $this->getDataGenerator()->create_user(['username' => 'user1active']); + $user2 = $this->getDataGenerator()->create_user(['username' => 'user2active']); + $user2su = $this->getDataGenerator()->create_user(['username' => 'user2suspended']); // Suspended user. + $user3 = $this->getDataGenerator()->create_user(['username' => 'user3active']); + $user3su = $this->getDataGenerator()->create_user(['username' => 'user3suspended']); // Suspended user. + + // Enrol the users in the course. + $this->getDataGenerator()->enrol_user($user0->id, $course->id); + $this->getDataGenerator()->enrol_user($user1->id, $course->id); + $this->getDataGenerator()->enrol_user($user2->id, $course->id); + $this->getDataGenerator()->enrol_user($user2su->id, $course->id, null, 'manual', 0, 0, ENROL_USER_SUSPENDED); + $this->getDataGenerator()->enrol_user($user3->id, $course->id); + $this->getDataGenerator()->enrol_user($user3su->id, $course->id, null, 'manual', 0, 0, ENROL_USER_SUSPENDED); + + // Create a role to add the allowedcaps. Users will have this role assigned. + $roleid = $this->getDataGenerator()->create_role(); + // Allow the specified capabilities. + assign_capability('moodle/course:enrolreview', CAP_ALLOW, $roleid, $coursecontext); + assign_capability('moodle/user:viewalldetails', CAP_ALLOW, $roleid, $coursecontext); + + // Switch to the user and assign the role. + $this->setUser($user0); + role_assign($roleid, $USER->id, $coursecontext); + + // Suspended users. + $options = [ + ['name' => 'onlysuspended', 'value' => true], + ['name' => 'userfields', 'value' => 'id,username'] + ]; + $suspendedusers = core_enrol_external::get_enrolled_users($course->id, $options); + + // We need to execute the return values cleaning process to simulate the web service server. + $suspendedusers = external_api::clean_returnvalue(core_enrol_external::get_enrolled_users_returns(), $suspendedusers); + $this->assertCount(2, $suspendedusers); + + foreach ($suspendedusers as $suspendeduser) { + $this->assertStringContainsString('suspended', $suspendeduser['username']); + } + + // Active users. + $options = [ + ['name' => 'onlyactive', 'value' => true], + ['name' => 'userfields', 'value' => 'id,username'] + ]; + $activeusers = core_enrol_external::get_enrolled_users($course->id, $options); + + // We need to execute the return values cleaning process to simulate the web service server. + $activeusers = external_api::clean_returnvalue(core_enrol_external::get_enrolled_users_returns(), $activeusers); + $this->assertCount(4, $activeusers); + + foreach ($activeusers as $activeuser) { + $this->assertStringContainsString('active', $activeuser['username']); + } + + // All enrolled users. + $options = [ + ['name' => 'userfields', 'value' => 'id,username'] + ]; + $allusers = core_enrol_external::get_enrolled_users($course->id, $options); + + // We need to execute the return values cleaning process to simulate the web service server. + $allusers = external_api::clean_returnvalue(core_enrol_external::get_enrolled_users_returns(), $allusers); + $this->assertCount(6, $allusers); + + // Active and suspended. Test exception is thrown. + $options = [ + ['name' => 'onlyactive', 'value' => true], + ['name' => 'onlysuspended', 'value' => true], + ['name' => 'userfields', 'value' => 'id,username'] + ]; + $this->expectException('coding_exception'); + $message = 'Coding error detected, it must be fixed by a programmer: Both onlyactive ' . + 'and onlysuspended are set, this is probably not what you want!'; + $this->expectExceptionMessage($message); + core_enrol_external::get_enrolled_users($course->id, $options); + } + /** * Test get_users_courses */ diff --git a/enrol/upgrade.txt b/enrol/upgrade.txt index bfec2ff74c7..a882f6ee067 100644 --- a/enrol/upgrade.txt +++ b/enrol/upgrade.txt @@ -1,6 +1,10 @@ This files describes API changes in /enrol/* - plugins, information provided here is intended especially for developers. +=== 3.11 === + +* Added onlysuspended option to core_enrol_get_enrolled_users webservice to retrieve only suspended users. + === 3.8 === * Function enrol_manual_plugin::enrol_cohort now return the number of enrolled cohort users. diff --git a/lang/en/admin.php b/lang/en/admin.php index e6103e58024..4e1f6c3ea99 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -575,6 +575,8 @@ $string['enableuserfeedback'] = 'Enable feedback about this software'; $string['enableuserfeedback_desc'] = 'If enabled, a \'Give feedback about this software\' link is displayed in the footer for users to give feedback about the Moodle software to Moodle HQ. If the \'Next feedback reminder\' option is set, the user is also shown a reminder on the Dashboard at the specified interval. Setting \'Next feedback reminder\' to \'Never\' disables the Dashboard reminder, while leaving the \'Give feedback about this software\' link in the footer.'; $string['enablewebservices'] = 'Enable web services'; $string['enablewsdocumentation'] = 'Web services documentation'; +$string['encryptedpassword_set'] = '(Set and encrypted)'; +$string['encryptedpassword_edit'] = 'Enter new value'; $string['enrolinstancedefaults'] = 'Enrolment instance defaults'; $string['enrolinstancedefaults_desc'] = 'Default enrolment settings in new courses.'; $string['enrolmultipleusers'] = 'Enrol the users'; diff --git a/lang/en/error.php b/lang/en/error.php index 8be1b324ea0..096bc7415df 100644 --- a/lang/en/error.php +++ b/lang/en/error.php @@ -236,6 +236,12 @@ $string['duplicaterolename'] = 'There is already a role with this name!'; $string['duplicateroleshortname'] = 'There is already a role with this short name!'; $string['duplicateusername'] = 'Duplicate username - skipping record'; $string['emailfail'] = 'Emailing failed'; +$string['encryption_encryptfailed'] = 'Encryption failed'; +$string['encryption_decryptfailed'] = 'Decryption failed'; +$string['encryption_invalidkey'] = 'Invalid key'; +$string['encryption_keyalreadyexists'] = 'Key already exists'; +$string['encryption_nokey'] = 'Key not found'; +$string['encryption_wrongmethod'] = 'Data does not match a supported encryption method'; $string['enddatebeforestartdate'] = 'The course end date must be after the start date.'; $string['error'] = 'Error occurred'; $string['error_question_answers_missing_in_db'] = 'Failed to find an answer matching "{$a->answer}" in the question_answers database table. This occurred while restoring the question with id {$a->filequestionid} in the backup file, which has been matched to the existing question with id {$a->dbquestionid} in the database.'; diff --git a/lib/adminlib.php b/lib/adminlib.php index 6764caf9af2..ff74c9a1666 100644 --- a/lib/adminlib.php +++ b/lib/adminlib.php @@ -2724,6 +2724,58 @@ class admin_setting_configpasswordunmask_with_advanced extends admin_setting_con } } +/** + * Admin setting class for encrypted values using secure encryption. + * + * @copyright 2019 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class admin_setting_encryptedpassword extends admin_setting { + + /** + * Constructor. Same as parent except that the default value is always an empty string. + * + * @param string $name Internal name used in config table + * @param string $visiblename Name shown on form + * @param string $description Description that appears below field + */ + public function __construct(string $name, string $visiblename, string $description) { + parent::__construct($name, $visiblename, $description, ''); + } + + public function get_setting() { + return $this->config_read($this->name); + } + + public function write_setting($data) { + $data = trim($data); + if ($data === '') { + // Value can really be set to nothing. + $savedata = ''; + } else { + // Encrypt value before saving it. + $savedata = \core\encryption::encrypt($data); + } + return ($this->config_write($this->name, $savedata) ? '' : get_string('errorsetting', 'admin')); + } + + public function output_html($data, $query='') { + global $OUTPUT; + + $default = $this->get_defaultsetting(); + $context = (object) [ + 'id' => $this->get_id(), + 'name' => $this->get_full_name(), + 'set' => $data !== '', + 'novalue' => $this->get_setting() === null + ]; + $element = $OUTPUT->render_from_template('core_admin/setting_encryptedpassword', $context); + + return format_admin_setting($this, $this->visiblename, $element, $this->description, + true, '', $default, $query); + } +} + /** * Empty setting used to allow flags (advanced) on settings that can have no sensible default. * Note: Only advanced makes sense right now - locked does not. diff --git a/lib/amd/build/permissionmanager.min.js b/lib/amd/build/permissionmanager.min.js index f53bd414708..3e200156772 100644 --- a/lib/amd/build/permissionmanager.min.js +++ b/lib/amd/build/permissionmanager.min.js @@ -1,2 +1,2 @@ -define ("core/permissionmanager",["jquery","core/config","core/notification","core/templates","core/yui"],function(a,b,c,d,e){var f={ADDROLE:"a.allowlink, a.prohibitlink",REMOVEROLE:"a.preventlink, a.unprohibitlink",UNPROHIBIT:"a.unprohibitlink"},g=a.Event("rolesloaded"),h,j,k,l,m=null,n=function loadOverideableRoles(){var d={contextid:h,getroles:1,sesskey:b.sesskey};a.post(k+"roles/ajax.php",d,null,"json").done(function(b){try{l=b;n=function loadOverideableRoles(){a("body").trigger(g)};n()}catch(a){c.exception(a)}}).fail(function(a,b,d){c.exception(d)})},o=function(b,e,f){var g={contextid:h,roleid:e,sesskey:M.cfg.sesskey,action:f,capability:b.data("name")};a.post(k+"roles/ajax.php",g,null,"json").done(function(f){var g=f;try{var h={rolename:l[e],roleid:e,adminurl:k,imageurl:M.util.image_url("t/delete","moodle")};switch(g){case"allow":h.spanclass="allowed";h.linkclass="preventlink";h.action="prevent";h.icon="t/delete";h.iconalt=M.util.get_string("deletexrole","core_role",l[e]);break;case"prohibit":h.spanclass="forbidden";h.linkclass="unprohibitlink";h.action="unprohibit";h.icon="t/delete";h.iconalt=M.util.get_string("deletexrole","core_role",l[e]);break;case"prevent":b.find("a[data-role-id=\""+e+"\"]").first().closest(".allowed").remove();return;case"unprohibit":b.find("a[data-role-id=\""+e+"\"]").first().closest(".forbidden").remove();return;default:return;}d.render("core/permissionmanager_role",h).done(function(c){if("allow"==g){a(c).insertBefore(b.find(".allowmore:first"))}else if("prohibit"==g){a(c).insertBefore(b.find(".prohibitmore:first"));var d=b.find(".allowedroles").first().find("a[data-role-id=\""+e+"\"]");if(d){d.first().closest(".allowed").remove()}}m.hide()}).fail(c.exception)}catch(a){c.exception(a)}}).fail(function(a,b,d){c.exception(d)})},p=function(b){b.preventDefault();var g=a(b.currentTarget);a("body").one("rolesloaded",function(){e.use("moodle-core-notification-dialogue",function(){var b=g.data("action"),h=g.closest("tr.rolecap"),k={cap:h.data("humanname"),context:j},n=M.util.get_string("role"+b+"info","core_role",k);if(null===m){m=new M.core.dialogue({draggable:!0,modal:!0,closeButton:!0,width:"450px"})}m.set("headerContent",M.util.get_string("role"+b+"header","core_role"));var p,e,q=[];switch(b){case"allow":e=h.find(f.REMOVEROLE);break;case"prohibit":e=h.find(f.UNPROHIBIT);break;}for(p in l){var r="",s=e.filter("[data-role-id='"+p+"']").length;if(s){r="disabled"}var t={roleid:p,rolename:l[p],disabled:r};q.push(t)}d.render("core/permissionmanager_panelcontent",{message:n,roles:q}).done(function(c){m.set("bodyContent",c);m.show();a("div.role_buttons").on("click","input",function(c){var d=a(c.currentTarget).data("role-id");o(h,d,b)})}).fail(c.exception)})});n()},q=function(b){b.preventDefault();var d=a(b.currentTarget);a("body").one("rolesloaded",function(){var a=d.data("action"),b=d.data("role-id"),e=d.closest("tr.rolecap"),f={role:l[b],cap:e.data("humanname"),context:j};c.confirm(M.util.get_string("confirmunassigntitle","core_role"),M.util.get_string("confirmrole"+a,"core_role",f),M.util.get_string("confirmunassignyes","core_role"),M.util.get_string("confirmunassignno","core_role"),function(){o(e,b,a)})});n()};return{initialize:function initialize(b){h=b.contextid;j=b.contextname;k=b.adminurl;var c=a("body");c.on("click",f.ADDROLE,p);c.on("click",f.REMOVEROLE,q)}}}); +define ("core/permissionmanager",["jquery","core/config","core/notification","core/templates","core/yui"],function(a,b,c,d,e){var f={ADDROLE:"a.allowlink, a.prohibitlink",REMOVEROLE:"a.preventlink, a.unprohibitlink",UNPROHIBIT:"a.unprohibitlink"},g=a.Event("rolesloaded"),h,j,k,l,m=null,n=function loadOverideableRoles(){var d={contextid:h,getroles:1,sesskey:b.sesskey};a.post(k+"roles/ajax.php",d,null,"json").done(function(b){try{l=b;n=function loadOverideableRoles(){a("body").trigger(g)};n()}catch(a){c.exception(a)}}).fail(function(a,b,d){c.exception(d)})},o=function(b,e,f){var g={contextid:h,roleid:e,sesskey:M.cfg.sesskey,action:f,capability:b.data("name")};a.post(k+"roles/ajax.php",g,null,"json").done(function(f){var g=f;try{var h={rolename:l[e],roleid:e,adminurl:k,imageurl:M.util.image_url("t/delete","moodle")};switch(g){case"allow":h.spanclass="allowed";h.linkclass="preventlink";h.action="prevent";h.icon="t/delete";h.iconalt=M.util.get_string("deletexrole","core_role",l[e]);break;case"prohibit":h.spanclass="forbidden";h.linkclass="unprohibitlink";h.action="unprohibit";h.icon="t/delete";h.iconalt=M.util.get_string("deletexrole","core_role",l[e]);break;case"prevent":b.find("a[data-role-id=\""+e+"\"]").first().closest(".allowed").remove();return;case"unprohibit":b.find("a[data-role-id=\""+e+"\"]").first().closest(".forbidden").remove();return;default:return;}d.render("core/permissionmanager_role",h).done(function(c){if("allow"==g){a(c).insertBefore(b.find(".allowmore:first"))}else if("prohibit"==g){a(c).insertBefore(b.find(".prohibitmore:first"));var d=b.find(".allowedroles").first().find("a[data-role-id=\""+e+"\"]");if(d){d.first().closest(".allowed").remove()}}m.hide()}).fail(c.exception)}catch(a){c.exception(a)}}).fail(function(a,b,d){c.exception(d)})},p=function(b){b.preventDefault();var g=a(b.currentTarget);a("body").one("rolesloaded",function(){e.use("moodle-core-notification-dialogue",function(){var b=g.data("action"),h=g.closest("tr.rolecap"),k={cap:h.data("humanname"),context:j},n=M.util.get_string("role"+b+"info","core_role",k);if(null===m){m=new M.core.dialogue({draggable:!0,modal:!0,closeButton:!0,width:"450px"})}m.set("headerContent",M.util.get_string("role"+b+"header","core_role"));var p,e,q=[];switch(b){case"allow":e=h.find(f.REMOVEROLE);break;case"prohibit":e=h.find(f.UNPROHIBIT);break;}for(p in l){var r="",s=e.filter("[data-role-id='"+p+"']").length;if(s){r="disabled"}var t={roleid:p,rolename:l[p],disabled:r};q.push(t)}d.render("core/permissionmanager_panelcontent",{message:n,roles:q}).done(function(c){m.set("bodyContent",c);m.show();a("div.role_buttons").on("click","button",function(c){var d=a(c.currentTarget).data("role-id");o(h,d,b)})}).fail(c.exception)})});n()},q=function(b){b.preventDefault();var d=a(b.currentTarget);a("body").one("rolesloaded",function(){var a=d.data("action"),b=d.data("role-id"),e=d.closest("tr.rolecap"),f={role:l[b],cap:e.data("humanname"),context:j};c.confirm(M.util.get_string("confirmunassigntitle","core_role"),M.util.get_string("confirmrole"+a,"core_role",f),M.util.get_string("confirmunassignyes","core_role"),M.util.get_string("confirmunassignno","core_role"),function(){o(e,b,a)})});n()};return{initialize:function initialize(b){h=b.contextid;j=b.contextname;k=b.adminurl;var c=a("body");c.on("click",f.ADDROLE,p);c.on("click",f.REMOVEROLE,q)}}}); //# sourceMappingURL=permissionmanager.min.js.map diff --git a/lib/amd/build/permissionmanager.min.js.map b/lib/amd/build/permissionmanager.min.js.map index 8413c766c4f..63e284bc594 100644 --- a/lib/amd/build/permissionmanager.min.js.map +++ b/lib/amd/build/permissionmanager.min.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/permissionmanager.js"],"names":["define","$","config","notification","templates","Y","SELECTORS","ADDROLE","REMOVEROLE","UNPROHIBIT","rolesloadedevent","Event","contextid","contextname","adminurl","overideableroles","panel","loadOverideableRoles","params","getroles","sesskey","post","done","data","trigger","err","exception","fail","jqXHR","status","error","changePermissions","row","roleid","action","M","cfg","capability","templatedata","rolename","imageurl","util","image_url","spanclass","linkclass","icon","iconalt","get_string","find","first","closest","remove","render","content","insertBefore","allowedLink","hide","handleAddRole","e","preventDefault","link","currentTarget","one","use","confirmationDetails","cap","context","message","core","dialogue","draggable","modal","closeButton","width","set","i","existingrolelinks","roles","disabled","disable","filter","length","roledetails","push","show","on","handleRemoveRole","questionDetails","role","confirm","initialize","args","body"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n/*\n * @package core\n * @class permissionmanager\n * @copyright 2015 Martin Mastny \n * @since 3.0\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n /**\n * @module admin/permissionmanager\n */\ndefine(['jquery', 'core/config', 'core/notification', 'core/templates', 'core/yui'],\n function($, config, notification, templates, Y) {\n\n /**\n * Used CSS selectors\n * @access private\n */\n var SELECTORS = {\n ADDROLE: 'a.allowlink, a.prohibitlink',\n REMOVEROLE: 'a.preventlink, a.unprohibitlink',\n UNPROHIBIT: 'a.unprohibitlink'\n };\n var rolesloadedevent = $.Event('rolesloaded');\n var contextid;\n var contextname;\n var adminurl;\n var overideableroles;\n var panel = null;\n\n /**\n * Load all possible roles, which could be assigned from server\n *\n * @access private\n * @method loadOverideableRoles\n */\n var loadOverideableRoles = function() {\n var params = {\n contextid: contextid,\n getroles: 1,\n sesskey: config.sesskey\n };\n\n // Need to tell jQuery to expect JSON as the content type may not be correct (MDL-55041).\n $.post(adminurl + 'roles/ajax.php', params, null, 'json')\n .done(function(data) {\n try {\n overideableroles = data;\n loadOverideableRoles = function() {\n $('body').trigger(rolesloadedevent);\n };\n loadOverideableRoles();\n } catch (err) {\n notification.exception(err);\n }\n })\n .fail(function(jqXHR, status, error) {\n notification.exception(error);\n });\n };\n\n /**\n * Perform the UI changes after server change\n *\n * @access private\n * @method changePermissions\n * @param {JQuery} row\n * @param {int} roleid\n * @param {string} action\n */\n var changePermissions = function(row, roleid, action) {\n var params = {\n contextid: contextid,\n roleid: roleid,\n sesskey: M.cfg.sesskey,\n action: action,\n capability: row.data('name')\n };\n $.post(adminurl + 'roles/ajax.php', params, null, 'json')\n .done(function(data) {\n var action = data;\n try {\n var templatedata = {rolename: overideableroles[roleid],\n roleid: roleid,\n adminurl: adminurl,\n imageurl: M.util.image_url('t/delete', 'moodle')\n };\n switch (action) {\n case 'allow':\n templatedata.spanclass = 'allowed';\n templatedata.linkclass = 'preventlink';\n templatedata.action = 'prevent';\n templatedata.icon = 't/delete';\n templatedata.iconalt = M.util.get_string('deletexrole', 'core_role', overideableroles[roleid]);\n break;\n case 'prohibit':\n templatedata.spanclass = 'forbidden';\n templatedata.linkclass = 'unprohibitlink';\n templatedata.action = 'unprohibit';\n templatedata.icon = 't/delete';\n templatedata.iconalt = M.util.get_string('deletexrole', 'core_role', overideableroles[roleid]);\n break;\n case 'prevent':\n row.find('a[data-role-id=\"' + roleid + '\"]').first().closest('.allowed').remove();\n return;\n case 'unprohibit':\n row.find('a[data-role-id=\"' + roleid + '\"]').first().closest('.forbidden').remove();\n return;\n default:\n return;\n }\n templates.render('core/permissionmanager_role', templatedata)\n .done(function(content) {\n if (action == 'allow') {\n $(content).insertBefore(row.find('.allowmore:first'));\n } else if (action == 'prohibit') {\n $(content).insertBefore(row.find('.prohibitmore:first'));\n // Remove allowed link\n var allowedLink = row.find('.allowedroles').first().find('a[data-role-id=\"' + roleid + '\"]');\n if (allowedLink) {\n allowedLink.first().closest('.allowed').remove();\n }\n }\n panel.hide();\n })\n .fail(notification.exception);\n } catch (err) {\n notification.exception(err);\n }\n })\n .fail(function(jqXHR, status, error) {\n notification.exception(error);\n });\n };\n\n /**\n * Prompts user for selecting a role which is permitted\n *\n * @access private\n * @method handleAddRole\n * @param {event} e\n */\n var handleAddRole = function(e) {\n e.preventDefault();\n\n var link = $(e.currentTarget);\n\n // TODO: MDL-57778 Convert to core/modal.\n $('body').one('rolesloaded', function() {\n Y.use('moodle-core-notification-dialogue', function() {\n var action = link.data('action');\n var row = link.closest('tr.rolecap');\n var confirmationDetails = {\n cap: row.data('humanname'),\n context: contextname\n };\n var message = M.util.get_string('role' + action + 'info', 'core_role', confirmationDetails);\n if (panel === null) {\n panel = new M.core.dialogue({\n draggable: true,\n modal: true,\n closeButton: true,\n width: '450px'\n });\n }\n panel.set('headerContent', M.util.get_string('role' + action + 'header', 'core_role'));\n\n var i, existingrolelinks;\n\n var roles = [];\n switch (action) {\n case 'allow':\n existingrolelinks = row.find(SELECTORS.REMOVEROLE);\n break;\n case 'prohibit':\n existingrolelinks = row.find(SELECTORS.UNPROHIBIT);\n break;\n }\n for (i in overideableroles) {\n var disabled = '';\n var disable = existingrolelinks.filter(\"[data-role-id='\" + i + \"']\").length;\n if (disable) {\n disabled = 'disabled';\n }\n var roledetails = {roleid: i, rolename: overideableroles[i], disabled: disabled};\n roles.push(roledetails);\n }\n\n templates.render('core/permissionmanager_panelcontent', {message: message, roles: roles})\n .done(function(content) {\n panel.set('bodyContent', content);\n panel.show();\n $('div.role_buttons').on('click', 'input', function(e) {\n var roleid = $(e.currentTarget).data('role-id');\n changePermissions(row, roleid, action);\n });\n })\n .fail(notification.exception);\n\n });\n });\n loadOverideableRoles();\n };\n\n /**\n * Prompts user when removing permission\n *\n * @access private\n * @method handleRemoveRole\n * @param {event} e\n */\n var handleRemoveRole = function(e) {\n e.preventDefault();\n var link = $(e.currentTarget);\n $('body').one('rolesloaded', function() {\n var action = link.data('action');\n var roleid = link.data('role-id');\n var row = link.closest('tr.rolecap');\n var questionDetails = {\n role: overideableroles[roleid],\n cap: row.data('humanname'),\n context: contextname\n };\n\n notification.confirm(M.util.get_string('confirmunassigntitle', 'core_role'),\n M.util.get_string('confirmrole' + action, 'core_role', questionDetails),\n M.util.get_string('confirmunassignyes', 'core_role'),\n M.util.get_string('confirmunassignno', 'core_role'),\n function() {\n changePermissions(row, roleid, action);\n }\n );\n });\n loadOverideableRoles();\n };\n\n return /** @alias module:core/permissionmanager */ {\n /**\n * Initialize permissionmanager\n * @access public\n * @param {Object} args\n */\n initialize: function(args) {\n contextid = args.contextid;\n contextname = args.contextname;\n adminurl = args.adminurl;\n var body = $('body');\n body.on('click', SELECTORS.ADDROLE, handleAddRole);\n body.on('click', SELECTORS.REMOVEROLE, handleRemoveRole);\n }\n };\n});\n"],"file":"permissionmanager.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/permissionmanager.js"],"names":["define","$","config","notification","templates","Y","SELECTORS","ADDROLE","REMOVEROLE","UNPROHIBIT","rolesloadedevent","Event","contextid","contextname","adminurl","overideableroles","panel","loadOverideableRoles","params","getroles","sesskey","post","done","data","trigger","err","exception","fail","jqXHR","status","error","changePermissions","row","roleid","action","M","cfg","capability","templatedata","rolename","imageurl","util","image_url","spanclass","linkclass","icon","iconalt","get_string","find","first","closest","remove","render","content","insertBefore","allowedLink","hide","handleAddRole","e","preventDefault","link","currentTarget","one","use","confirmationDetails","cap","context","message","core","dialogue","draggable","modal","closeButton","width","set","i","existingrolelinks","roles","disabled","disable","filter","length","roledetails","push","show","on","handleRemoveRole","questionDetails","role","confirm","initialize","args","body"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n/*\n * @package core\n * @class permissionmanager\n * @copyright 2015 Martin Mastny \n * @since 3.0\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n /**\n * @module admin/permissionmanager\n */\ndefine(['jquery', 'core/config', 'core/notification', 'core/templates', 'core/yui'],\n function($, config, notification, templates, Y) {\n\n /**\n * Used CSS selectors\n * @access private\n */\n var SELECTORS = {\n ADDROLE: 'a.allowlink, a.prohibitlink',\n REMOVEROLE: 'a.preventlink, a.unprohibitlink',\n UNPROHIBIT: 'a.unprohibitlink'\n };\n var rolesloadedevent = $.Event('rolesloaded');\n var contextid;\n var contextname;\n var adminurl;\n var overideableroles;\n var panel = null;\n\n /**\n * Load all possible roles, which could be assigned from server\n *\n * @access private\n * @method loadOverideableRoles\n */\n var loadOverideableRoles = function() {\n var params = {\n contextid: contextid,\n getroles: 1,\n sesskey: config.sesskey\n };\n\n // Need to tell jQuery to expect JSON as the content type may not be correct (MDL-55041).\n $.post(adminurl + 'roles/ajax.php', params, null, 'json')\n .done(function(data) {\n try {\n overideableroles = data;\n loadOverideableRoles = function() {\n $('body').trigger(rolesloadedevent);\n };\n loadOverideableRoles();\n } catch (err) {\n notification.exception(err);\n }\n })\n .fail(function(jqXHR, status, error) {\n notification.exception(error);\n });\n };\n\n /**\n * Perform the UI changes after server change\n *\n * @access private\n * @method changePermissions\n * @param {JQuery} row\n * @param {int} roleid\n * @param {string} action\n */\n var changePermissions = function(row, roleid, action) {\n var params = {\n contextid: contextid,\n roleid: roleid,\n sesskey: M.cfg.sesskey,\n action: action,\n capability: row.data('name')\n };\n $.post(adminurl + 'roles/ajax.php', params, null, 'json')\n .done(function(data) {\n var action = data;\n try {\n var templatedata = {rolename: overideableroles[roleid],\n roleid: roleid,\n adminurl: adminurl,\n imageurl: M.util.image_url('t/delete', 'moodle')\n };\n switch (action) {\n case 'allow':\n templatedata.spanclass = 'allowed';\n templatedata.linkclass = 'preventlink';\n templatedata.action = 'prevent';\n templatedata.icon = 't/delete';\n templatedata.iconalt = M.util.get_string('deletexrole', 'core_role', overideableroles[roleid]);\n break;\n case 'prohibit':\n templatedata.spanclass = 'forbidden';\n templatedata.linkclass = 'unprohibitlink';\n templatedata.action = 'unprohibit';\n templatedata.icon = 't/delete';\n templatedata.iconalt = M.util.get_string('deletexrole', 'core_role', overideableroles[roleid]);\n break;\n case 'prevent':\n row.find('a[data-role-id=\"' + roleid + '\"]').first().closest('.allowed').remove();\n return;\n case 'unprohibit':\n row.find('a[data-role-id=\"' + roleid + '\"]').first().closest('.forbidden').remove();\n return;\n default:\n return;\n }\n templates.render('core/permissionmanager_role', templatedata)\n .done(function(content) {\n if (action == 'allow') {\n $(content).insertBefore(row.find('.allowmore:first'));\n } else if (action == 'prohibit') {\n $(content).insertBefore(row.find('.prohibitmore:first'));\n // Remove allowed link\n var allowedLink = row.find('.allowedroles').first().find('a[data-role-id=\"' + roleid + '\"]');\n if (allowedLink) {\n allowedLink.first().closest('.allowed').remove();\n }\n }\n panel.hide();\n })\n .fail(notification.exception);\n } catch (err) {\n notification.exception(err);\n }\n })\n .fail(function(jqXHR, status, error) {\n notification.exception(error);\n });\n };\n\n /**\n * Prompts user for selecting a role which is permitted\n *\n * @access private\n * @method handleAddRole\n * @param {event} e\n */\n var handleAddRole = function(e) {\n e.preventDefault();\n\n var link = $(e.currentTarget);\n\n // TODO: MDL-57778 Convert to core/modal.\n $('body').one('rolesloaded', function() {\n Y.use('moodle-core-notification-dialogue', function() {\n var action = link.data('action');\n var row = link.closest('tr.rolecap');\n var confirmationDetails = {\n cap: row.data('humanname'),\n context: contextname\n };\n var message = M.util.get_string('role' + action + 'info', 'core_role', confirmationDetails);\n if (panel === null) {\n panel = new M.core.dialogue({\n draggable: true,\n modal: true,\n closeButton: true,\n width: '450px'\n });\n }\n panel.set('headerContent', M.util.get_string('role' + action + 'header', 'core_role'));\n\n var i, existingrolelinks;\n\n var roles = [];\n switch (action) {\n case 'allow':\n existingrolelinks = row.find(SELECTORS.REMOVEROLE);\n break;\n case 'prohibit':\n existingrolelinks = row.find(SELECTORS.UNPROHIBIT);\n break;\n }\n for (i in overideableroles) {\n var disabled = '';\n var disable = existingrolelinks.filter(\"[data-role-id='\" + i + \"']\").length;\n if (disable) {\n disabled = 'disabled';\n }\n var roledetails = {roleid: i, rolename: overideableroles[i], disabled: disabled};\n roles.push(roledetails);\n }\n\n templates.render('core/permissionmanager_panelcontent', {message: message, roles: roles})\n .done(function(content) {\n panel.set('bodyContent', content);\n panel.show();\n $('div.role_buttons').on('click', 'button', function(e) {\n var roleid = $(e.currentTarget).data('role-id');\n changePermissions(row, roleid, action);\n });\n })\n .fail(notification.exception);\n\n });\n });\n loadOverideableRoles();\n };\n\n /**\n * Prompts user when removing permission\n *\n * @access private\n * @method handleRemoveRole\n * @param {event} e\n */\n var handleRemoveRole = function(e) {\n e.preventDefault();\n var link = $(e.currentTarget);\n $('body').one('rolesloaded', function() {\n var action = link.data('action');\n var roleid = link.data('role-id');\n var row = link.closest('tr.rolecap');\n var questionDetails = {\n role: overideableroles[roleid],\n cap: row.data('humanname'),\n context: contextname\n };\n\n notification.confirm(M.util.get_string('confirmunassigntitle', 'core_role'),\n M.util.get_string('confirmrole' + action, 'core_role', questionDetails),\n M.util.get_string('confirmunassignyes', 'core_role'),\n M.util.get_string('confirmunassignno', 'core_role'),\n function() {\n changePermissions(row, roleid, action);\n }\n );\n });\n loadOverideableRoles();\n };\n\n return /** @alias module:core/permissionmanager */ {\n /**\n * Initialize permissionmanager\n * @access public\n * @param {Object} args\n */\n initialize: function(args) {\n contextid = args.contextid;\n contextname = args.contextname;\n adminurl = args.adminurl;\n var body = $('body');\n body.on('click', SELECTORS.ADDROLE, handleAddRole);\n body.on('click', SELECTORS.REMOVEROLE, handleRemoveRole);\n }\n };\n});\n"],"file":"permissionmanager.min.js"} \ No newline at end of file diff --git a/lib/amd/src/permissionmanager.js b/lib/amd/src/permissionmanager.js index 07721043e58..3f350d30fce 100644 --- a/lib/amd/src/permissionmanager.js +++ b/lib/amd/src/permissionmanager.js @@ -204,7 +204,7 @@ define(['jquery', 'core/config', 'core/notification', 'core/templates', 'core/yu .done(function(content) { panel.set('bodyContent', content); panel.show(); - $('div.role_buttons').on('click', 'input', function(e) { + $('div.role_buttons').on('click', 'button', function(e) { var roleid = $(e.currentTarget).data('role-id'); changePermissions(row, roleid, action); }); diff --git a/lib/behat/classes/behat_session_interface.php b/lib/behat/classes/behat_session_interface.php index 2746070974c..7f2dd0c29e6 100644 --- a/lib/behat/classes/behat_session_interface.php +++ b/lib/behat/classes/behat_session_interface.php @@ -37,37 +37,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface behat_session_interface { - /** - * Small timeout. - * - * A reduced timeout for cases where self::TIMEOUT is too much - * and a simple $this->getSession()->getPage()->find() could not - * be enough. - * - * @deprecated since Moodle 3.7 MDL-64979 - please use get_reduced_timeout() instead - * @todo MDL-64982 This will be deleted in Moodle 3.11 - * @see behat_base::get_reduced_timeout() - */ - const REDUCED_TIMEOUT = 2; - - /** - * The timeout for each Behat step (load page, wait for an element to load...). - * - * @deprecated since Moodle 3.7 MDL-64979 - please use get_timeout() instead - * @todo MDL-64982 This will be deleted in Moodle 3.11 - * @see behat_base::get_timeout() - */ - const TIMEOUT = 6; - - /** - * And extended timeout for specific cases. - * - * @deprecated since Moodle 3.7 MDL-64979 - please use get_extended_timeout() instead - * @todo MDL-64982 This will be deleted in Moodle 3.11 - * @see behat_base::get_extended_timeout() - */ - const EXTENDED_TIMEOUT = 10; - /** * The JS code to check that the page is ready. * diff --git a/lib/classes/encryption.php b/lib/classes/encryption.php new file mode 100644 index 00000000000..84560f68993 --- /dev/null +++ b/lib/classes/encryption.php @@ -0,0 +1,318 @@ +. + +/** + * Class used to encrypt or decrypt data. + * + * @package core + * @copyright 2020 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core; + +/** + * Class used to encrypt or decrypt data. + * + * @package core + * @copyright 2020 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class encryption { + /** @var string Encryption method: Sodium */ + const METHOD_SODIUM = 'sodium'; + /** @var string Encryption method: hand-coded OpenSSL (less safe) */ + const METHOD_OPENSSL = 'openssl-aes-256-ctr'; + + /** @var string OpenSSL cipher method */ + const OPENSSL_CIPHER = 'AES-256-CTR'; + + /** + * Checks if Sodium is installed. + * + * @return bool True if the Sodium extension is available + */ + public static function is_sodium_installed(): bool { + return extension_loaded('sodium'); + } + + /** + * Gets the encryption method to use. We use the Sodium extension if it is installed, or + * otherwise, OpenSSL. + * + * @return string Current encryption method + */ + protected static function get_encryption_method(): string { + if (self::is_sodium_installed()) { + return self::METHOD_SODIUM; + } else { + return self::METHOD_OPENSSL; + } + } + + /** + * Creates a key for the server. + * + * @param string|null $method Encryption method (only if you want to create a non-default key) + * @param bool $chmod If true, restricts the file access of the key + * @throws \moodle_exception If the server already has a key, or there is an error + */ + public static function create_key(?string $method = null, bool $chmod = true): void { + if ($method === null) { + $method = self::get_encryption_method(); + } + + if (self::key_exists($method)) { + throw new \moodle_exception('encryption_keyalreadyexists', 'error'); + } + + // Don't make it read-only in Behat or it will fail to clear for future runs. + if (defined('BEHAT_SITE_RUNNING')) { + $chmod = false; + } + + // Generate the key. + switch ($method) { + case self::METHOD_SODIUM: + $key = sodium_crypto_secretbox_keygen(); + break; + case self::METHOD_OPENSSL: + $key = openssl_random_pseudo_bytes(32); + break; + default: + throw new \coding_exception('Unknown method: ' . $method); + } + + // Store the key, making it readable only by server. + $folder = self::get_key_folder(); + check_dir_exists($folder); + $keyfile = self::get_key_file($method); + file_put_contents($keyfile, $key); + if ($chmod) { + chmod($keyfile, 0400); + } + } + + /** + * Gets the folder used to store the secret key. + * + * @return string Folder path + */ + protected static function get_key_folder(): string { + global $CFG; + return ($CFG->secretdataroot ?? $CFG->dataroot . '/secret') . '/key'; + } + + /** + * Gets the file path used to store the secret key. The filename contains the cipher method, + * so that if necessary to transition in future it would be possible to have multiple. + * + * @param string|null $method Encryption method (only if you want to get a non-default key) + * @return string Full path to file + */ + public static function get_key_file(?string $method = null): string { + if ($method === null) { + $method = self::get_encryption_method(); + } + + return self::get_key_folder() . '/' . $method . '.key'; + } + + /** + * Checks if there is a key file. + * + * @param string|null $method Encryption method (only if you want to check a non-default key) + * @return bool True if there is a key file + */ + public static function key_exists(?string $method = null): bool { + if ($method === null) { + $method = self::get_encryption_method(); + } + + return file_exists(self::get_key_file($method)); + } + + /** + * Gets the current key, automatically creating it if there isn't one yet. + * + * @param string|null $method Encryption method (only if you want to get a non-default key) + * @return string The key (binary) + * @throws \moodle_exception If there isn't one already (and creation is disabled) + */ + protected static function get_key(?string $method = null): string { + global $CFG; + + if ($method === null) { + $method = self::get_encryption_method(); + } + + $keyfile = self::get_key_file($method); + if (!file_exists($keyfile) && empty($CFG->nokeygeneration)) { + self::create_key($method); + } + $result = @file_get_contents($keyfile); + if ($result === false) { + throw new \moodle_exception('encryption_nokey', 'error'); + } + return $result; + } + + /** + * Gets the length in bytes of the initial values data required. + * + * @param string $method Crypto method + * @return int Length in bytes + */ + protected static function get_iv_length(string $method): int { + switch ($method) { + case self::METHOD_SODIUM: + return SODIUM_CRYPTO_SECRETBOX_NONCEBYTES; + case self::METHOD_OPENSSL: + return openssl_cipher_iv_length(self::OPENSSL_CIPHER); + default: + throw new \coding_exception('Unknown method: ' . $method); + } + } + + /** + * Encrypts data using the server's key. + * + * Note there is a special case - the empty string is not encrypted. + * + * @param string $data Data to encrypt, or empty string for no data + * @param string|null $method Encryption method (only if you want to use a non-default method) + * @return string Encrypted data, or empty string for no data + * @throws \moodle_exception If the key doesn't exist, or the string is too long + */ + public static function encrypt(string $data, ?string $method = null): string { + if ($data === '') { + return ''; + } else { + if ($method === null) { + $method = self::get_encryption_method(); + } + + // Create IV. + $iv = random_bytes(self::get_iv_length($method)); + + // Encrypt data. + switch($method) { + case self::METHOD_SODIUM: + try { + $encrypted = sodium_crypto_secretbox($data, $iv, self::get_key($method)); + } catch (\SodiumException $e) { + throw new \moodle_exception('encryption_encryptfailed', 'error', '', null, $e->getMessage()); + } + break; + + case self::METHOD_OPENSSL: + // This may not be a secure authenticated encryption implementation; + // administrators should enable the Sodium extension. + $key = self::get_key($method); + if (strlen($key) !== 32) { + throw new \moodle_exception('encryption_invalidkey', 'error'); + } + $encrypted = @openssl_encrypt($data, self::OPENSSL_CIPHER, $key, OPENSSL_RAW_DATA, $iv); + if ($encrypted === false) { + throw new \moodle_exception('encryption_encryptfailed', 'error', + '', null, openssl_error_string()); + } + $hmac = hash_hmac('sha256', $iv . $encrypted, $key, true); + $encrypted .= $hmac; + break; + + default: + throw new \coding_exception('Unknown method: ' . $method); + } + + // Encrypted data is cipher method plus IV plus encrypted data. + return $method . ':' . base64_encode($iv . $encrypted); + } + } + + /** + * Decrypts data using the server's key. The decryption works with either supported method. + * + * @param string $data Data to decrypt + * @return string Decrypted data + */ + public static function decrypt(string $data): string { + if ($data === '') { + return ''; + } else { + if (preg_match('~^(' . self::METHOD_OPENSSL . '|' . self::METHOD_SODIUM . '):~', $data, $matches)) { + $method = $matches[1]; + } else { + throw new \moodle_exception('encryption_wrongmethod', 'error'); + } + $realdata = base64_decode(substr($data, strlen($method) + 1), true); + if ($realdata === false) { + throw new \moodle_exception('encryption_decryptfailed', 'error', + '', null, 'Invalid base64 data'); + } + + $ivlength = self::get_iv_length($method); + if (strlen($realdata) < $ivlength + 1) { + throw new \moodle_exception('encryption_decryptfailed', 'error', + '', null, 'Insufficient data'); + } + $iv = substr($realdata, 0, $ivlength); + $encrypted = substr($realdata, $ivlength); + + switch ($method) { + case self::METHOD_SODIUM: + try { + $decrypted = sodium_crypto_secretbox_open($encrypted, $iv, self::get_key($method)); + } catch (\SodiumException $e) { + throw new \moodle_exception('encryption_decryptfailed', 'error', + '', null, $e->getMessage()); + } + // Sodium returns false if decryption fails because data is invalid. + if ($decrypted === false) { + throw new \moodle_exception('encryption_decryptfailed', 'error', + '', null, 'Integrity check failed'); + } + break; + + case self::METHOD_OPENSSL: + if (strlen($encrypted) < 33) { + throw new \moodle_exception('encryption_decryptfailed', 'error', + '', null, 'Insufficient data'); + } + $hmac = substr($encrypted, -32); + $encrypted = substr($encrypted, 0, -32); + $key = self::get_key($method); + $expectedhmac = hash_hmac('sha256', $iv . $encrypted, $key, true); + if ($hmac !== $expectedhmac) { + throw new \moodle_exception('encryption_decryptfailed', 'error', + '', null, 'Integrity check failed'); + } + + $decrypted = @openssl_decrypt($encrypted, self::OPENSSL_CIPHER, $key, OPENSSL_RAW_DATA, $iv); + if ($decrypted === false) { + throw new \moodle_exception('encryption_decryptfailed', 'error', + '', null, openssl_error_string()); + } + break; + + default: + throw new \coding_exception('Unknown method: ' . $method); + } + + return $decrypted; + } + } +} diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index d24a5c29a82..685de9e34da 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -2786,11 +2786,12 @@ function xmldb_main_upgrade($oldversion) { if ($oldversion < 2021052500.26) { // Delete orphaned course_modules_completion rows; these were not deleted properly // by remove_course_contents function. - $DB->delete_records_subquery('course_modules_completion', 'id', 'id', - "SELECT cmc.id - FROM {course_modules_completion} cmc - LEFT JOIN {course_modules} cm ON cm.id = cmc.coursemoduleid - WHERE cm.id IS NULL"); + $DB->delete_records_select('course_modules_completion', " + NOT EXISTS ( + SELECT 1 + FROM {course_modules} cm + WHERE cm.id = {course_modules_completion}.coursemoduleid + )"); upgrade_main_savepoint(true, 2021052500.26); } diff --git a/lib/form/amd/build/encryptedpassword.min.js b/lib/form/amd/build/encryptedpassword.min.js new file mode 100644 index 00000000000..069dbc039df --- /dev/null +++ b/lib/form/amd/build/encryptedpassword.min.js @@ -0,0 +1,2 @@ +define ("core_form/encryptedpassword",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.EncryptedPassword=void 0;var b=function(a){var b=this,c=document.querySelector("div[data-encryptedpasswordid=\""+a+"\"]");this.spanOrLink=c.querySelector("span, a");this.input=c.querySelector("input");this.editButtonOrLink=c.querySelector("button[data-editbutton], a");this.cancelButton=c.querySelector("button[data-cancelbutton]");var d=function(a){a.stopImmediatePropagation();a.preventDefault();b.startEditing(!0)};this.editButtonOrLink.addEventListener("click",d);if("A"===this.editButtonOrLink.nodeName){c.parentElement.previousElementSibling.querySelector("label").addEventListener("click",d)}this.cancelButton.addEventListener("click",function(a){a.stopImmediatePropagation();a.preventDefault();b.cancelEditing()});if("y"===c.dataset.novalue){this.startEditing(!1);this.cancelButton.style.display="none"}};a.EncryptedPassword=b;b.prototype.startEditing=function(a){this.input.style.display="inline";this.input.disabled=!1;this.spanOrLink.style.display="none";this.editButtonOrLink.style.display="none";this.cancelButton.style.display="inline";var b=this.editButtonOrLink.id;this.editButtonOrLink.removeAttribute("id");this.input.id=b;if(a){this.input.focus()}};b.prototype.cancelEditing=function(){this.input.style.display="none";this.input.value="";this.input.disabled=!0;this.spanOrLink.style.display="inline";this.editButtonOrLink.style.display="inline";this.cancelButton.style.display="none";var a=this.input.id;this.input.removeAttribute("id");this.editButtonOrLink.id=a}}); +//# sourceMappingURL=encryptedpassword.min.js.map diff --git a/lib/form/amd/build/encryptedpassword.min.js.map b/lib/form/amd/build/encryptedpassword.min.js.map new file mode 100644 index 00000000000..6a62ecfa7b0 --- /dev/null +++ b/lib/form/amd/build/encryptedpassword.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/encryptedpassword.js"],"names":["EncryptedPassword","elementId","wrapper","document","querySelector","spanOrLink","input","editButtonOrLink","cancelButton","editHandler","e","stopImmediatePropagation","preventDefault","startEditing","addEventListener","nodeName","parentElement","previousElementSibling","cancelEditing","dataset","novalue","style","display","prototype","moveFocus","disabled","id","removeAttribute","focus","value"],"mappings":"uJA8BO,GAAMA,CAAAA,CAAiB,CAAG,SAASC,CAAT,CAAoB,YAC3CC,CAAO,CAAGC,QAAQ,CAACC,aAAT,CAAuB,kCAAmCH,CAAnC,CAA+C,KAAtE,CADiC,CAEjD,KAAKI,UAAL,CAAkBH,CAAO,CAACE,aAAR,CAAsB,SAAtB,CAAlB,CACA,KAAKE,KAAL,CAAaJ,CAAO,CAACE,aAAR,CAAsB,OAAtB,CAAb,CACA,KAAKG,gBAAL,CAAwBL,CAAO,CAACE,aAAR,CAAsB,4BAAtB,CAAxB,CACA,KAAKI,YAAL,CAAoBN,CAAO,CAACE,aAAR,CAAsB,2BAAtB,CAApB,CAGA,GAAIK,CAAAA,CAAW,CAAG,SAACC,CAAD,CAAO,CACrBA,CAAC,CAACC,wBAAF,GACAD,CAAC,CAACE,cAAF,GACA,CAAI,CAACC,YAAL,IACH,CAJD,CAKA,KAAKN,gBAAL,CAAsBO,gBAAtB,CAAuC,OAAvC,CAAgDL,CAAhD,EAGA,GAAuC,GAAnC,QAAKF,gBAAL,CAAsBQ,QAA1B,CAA4C,CACxCb,CAAO,CAACc,aAAR,CAAsBC,sBAAtB,CAA6Cb,aAA7C,CAA2D,OAA3D,EAAoEU,gBAApE,CAAqF,OAArF,CAA8FL,CAA9F,CACH,CAGD,KAAKD,YAAL,CAAkBM,gBAAlB,CAAmC,OAAnC,CAA4C,SAACJ,CAAD,CAAO,CAC/CA,CAAC,CAACC,wBAAF,GACAD,CAAC,CAACE,cAAF,GACA,CAAI,CAACM,aAAL,EACH,CAJD,EASA,GAAgC,GAA5B,GAAAhB,CAAO,CAACiB,OAAR,CAAgBC,OAApB,CAAqC,CACjC,KAAKP,YAAL,KACA,KAAKL,YAAL,CAAkBa,KAAlB,CAAwBC,OAAxB,CAAkC,MACrC,CACJ,CAlCM,C,sBAyCPtB,CAAiB,CAACuB,SAAlB,CAA4BV,YAA5B,CAA2C,SAASW,CAAT,CAAoB,CAC3D,KAAKlB,KAAL,CAAWe,KAAX,CAAiBC,OAAjB,CAA2B,QAA3B,CACA,KAAKhB,KAAL,CAAWmB,QAAX,IACA,KAAKpB,UAAL,CAAgBgB,KAAhB,CAAsBC,OAAtB,CAAgC,MAAhC,CACA,KAAKf,gBAAL,CAAsBc,KAAtB,CAA4BC,OAA5B,CAAsC,MAAtC,CACA,KAAKd,YAAL,CAAkBa,KAAlB,CAAwBC,OAAxB,CAAkC,QAAlC,CAGA,GAAMI,CAAAA,CAAE,CAAG,KAAKnB,gBAAL,CAAsBmB,EAAjC,CACA,KAAKnB,gBAAL,CAAsBoB,eAAtB,CAAsC,IAAtC,EACA,KAAKrB,KAAL,CAAWoB,EAAX,CAAgBA,CAAhB,CAEA,GAAIF,CAAJ,CAAe,CACX,KAAKlB,KAAL,CAAWsB,KAAX,EACH,CACJ,CAfD,CAoBA5B,CAAiB,CAACuB,SAAlB,CAA4BL,aAA5B,CAA4C,UAAW,CACnD,KAAKZ,KAAL,CAAWe,KAAX,CAAiBC,OAAjB,CAA2B,MAA3B,CACA,KAAKhB,KAAL,CAAWuB,KAAX,CAAmB,EAAnB,CACA,KAAKvB,KAAL,CAAWmB,QAAX,IACA,KAAKpB,UAAL,CAAgBgB,KAAhB,CAAsBC,OAAtB,CAAgC,QAAhC,CACA,KAAKf,gBAAL,CAAsBc,KAAtB,CAA4BC,OAA5B,CAAsC,QAAtC,CACA,KAAKd,YAAL,CAAkBa,KAAlB,CAAwBC,OAAxB,CAAkC,MAAlC,CAGA,GAAMI,CAAAA,CAAE,CAAG,KAAKpB,KAAL,CAAWoB,EAAtB,CACA,KAAKpB,KAAL,CAAWqB,eAAX,CAA2B,IAA3B,EACA,KAAKpB,gBAAL,CAAsBmB,EAAtB,CAA2BA,CAC9B,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Encrypted password functionality.\n *\n * @module core_form/encryptedpassword\n * @package core_form\n * @class encryptedpassword\n * @copyright 2019 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * Constructor for EncryptedPassword.\n *\n * @param {String} elementId The element to apply the encrypted password JS to\n */\nexport const EncryptedPassword = function(elementId) {\n const wrapper = document.querySelector('div[data-encryptedpasswordid=\"' + elementId + '\"]');\n this.spanOrLink = wrapper.querySelector('span, a');\n this.input = wrapper.querySelector('input');\n this.editButtonOrLink = wrapper.querySelector('button[data-editbutton], a');\n this.cancelButton = wrapper.querySelector('button[data-cancelbutton]');\n\n // Edit button action.\n var editHandler = (e) => {\n e.stopImmediatePropagation();\n e.preventDefault();\n this.startEditing(true);\n };\n this.editButtonOrLink.addEventListener('click', editHandler);\n\n // When it's a link, do some magic to make the label work as well.\n if (this.editButtonOrLink.nodeName === 'A') {\n wrapper.parentElement.previousElementSibling.querySelector('label').addEventListener('click', editHandler);\n }\n\n // Cancel button action.\n this.cancelButton.addEventListener('click', (e) => {\n e.stopImmediatePropagation();\n e.preventDefault();\n this.cancelEditing();\n });\n\n // If the value is not set yet, start editing and remove the cancel option - so that\n // it saves something in the config table and doesn't keep repeat showing it as a new\n // admin setting...\n if (wrapper.dataset.novalue === 'y') {\n this.startEditing(false);\n this.cancelButton.style.display = 'none';\n }\n};\n\n/**\n * Starts editing.\n *\n * @param {Boolean} moveFocus If true, sets focus to the edit box\n */\nEncryptedPassword.prototype.startEditing = function(moveFocus) {\n this.input.style.display = 'inline';\n this.input.disabled = false;\n this.spanOrLink.style.display = 'none';\n this.editButtonOrLink.style.display = 'none';\n this.cancelButton.style.display = 'inline';\n\n // Move the id around, which changes what happens when you click the label.\n const id = this.editButtonOrLink.id;\n this.editButtonOrLink.removeAttribute('id');\n this.input.id = id;\n\n if (moveFocus) {\n this.input.focus();\n }\n};\n\n/**\n * Cancels editing.\n */\nEncryptedPassword.prototype.cancelEditing = function() {\n this.input.style.display = 'none';\n this.input.value = '';\n this.input.disabled = true;\n this.spanOrLink.style.display = 'inline';\n this.editButtonOrLink.style.display = 'inline';\n this.cancelButton.style.display = 'none';\n\n // Move the id around, which changes what happens when you click the label.\n const id = this.input.id;\n this.input.removeAttribute('id');\n this.editButtonOrLink.id = id;\n};\n"],"file":"encryptedpassword.min.js"} \ No newline at end of file diff --git a/lib/form/amd/src/encryptedpassword.js b/lib/form/amd/src/encryptedpassword.js new file mode 100644 index 00000000000..2641ff68ff0 --- /dev/null +++ b/lib/form/amd/src/encryptedpassword.js @@ -0,0 +1,104 @@ +// 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 . + +/** + * Encrypted password functionality. + * + * @module core_form/encryptedpassword + * @package core_form + * @class encryptedpassword + * @copyright 2019 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Constructor for EncryptedPassword. + * + * @param {String} elementId The element to apply the encrypted password JS to + */ +export const EncryptedPassword = function(elementId) { + const wrapper = document.querySelector('div[data-encryptedpasswordid="' + elementId + '"]'); + this.spanOrLink = wrapper.querySelector('span, a'); + this.input = wrapper.querySelector('input'); + this.editButtonOrLink = wrapper.querySelector('button[data-editbutton], a'); + this.cancelButton = wrapper.querySelector('button[data-cancelbutton]'); + + // Edit button action. + var editHandler = (e) => { + e.stopImmediatePropagation(); + e.preventDefault(); + this.startEditing(true); + }; + this.editButtonOrLink.addEventListener('click', editHandler); + + // When it's a link, do some magic to make the label work as well. + if (this.editButtonOrLink.nodeName === 'A') { + wrapper.parentElement.previousElementSibling.querySelector('label').addEventListener('click', editHandler); + } + + // Cancel button action. + this.cancelButton.addEventListener('click', (e) => { + e.stopImmediatePropagation(); + e.preventDefault(); + this.cancelEditing(); + }); + + // If the value is not set yet, start editing and remove the cancel option - so that + // it saves something in the config table and doesn't keep repeat showing it as a new + // admin setting... + if (wrapper.dataset.novalue === 'y') { + this.startEditing(false); + this.cancelButton.style.display = 'none'; + } +}; + +/** + * Starts editing. + * + * @param {Boolean} moveFocus If true, sets focus to the edit box + */ +EncryptedPassword.prototype.startEditing = function(moveFocus) { + this.input.style.display = 'inline'; + this.input.disabled = false; + this.spanOrLink.style.display = 'none'; + this.editButtonOrLink.style.display = 'none'; + this.cancelButton.style.display = 'inline'; + + // Move the id around, which changes what happens when you click the label. + const id = this.editButtonOrLink.id; + this.editButtonOrLink.removeAttribute('id'); + this.input.id = id; + + if (moveFocus) { + this.input.focus(); + } +}; + +/** + * Cancels editing. + */ +EncryptedPassword.prototype.cancelEditing = function() { + this.input.style.display = 'none'; + this.input.value = ''; + this.input.disabled = true; + this.spanOrLink.style.display = 'inline'; + this.editButtonOrLink.style.display = 'inline'; + this.cancelButton.style.display = 'none'; + + // Move the id around, which changes what happens when you click the label. + const id = this.input.id; + this.input.removeAttribute('id'); + this.editButtonOrLink.id = id; +}; diff --git a/lib/html2text/Html2Text.php b/lib/html2text/Html2Text.php index f279879d6f8..9fd91235927 100644 --- a/lib/html2text/Html2Text.php +++ b/lib/html2text/Html2Text.php @@ -28,14 +28,14 @@ class Html2Text /** * Contains the HTML content to convert. * - * @type string + * @var string $html */ protected $html; /** * Contains the converted, formatted text. * - * @type string + * @var string $text */ protected $text; @@ -43,7 +43,7 @@ class Html2Text * List of preg* regular expression patterns to search for, * used in conjunction with $replace. * - * @type array + * @var array $search * @see $replace */ protected $search = array( @@ -54,6 +54,7 @@ class Html2Text '/]*>.*?<\/style>/i', //