diff --git a/.github/workflows/onebyone.yml b/.github/workflows/onebyone.yml index c84b2b8608422..8dd894cb7e0d7 100644 --- a/.github/workflows/onebyone.yml +++ b/.github/workflows/onebyone.yml @@ -51,7 +51,7 @@ jobs: chunk=$(((($count % $chunks)) + 1)) echo "$testname $testfile" >> ./chunk_$chunk.txt done < <(grep "function test_" "${testfile}" | sed -r "s/^.*function (test_[a-zA-Z0-9_]+).*/\1/") - done < <(find . -name "*_test.php") + done < <(find . -name "*_test.php" -not -path "*/fixtures/*") # Generate the matrix to run tests. echo "matrix=$(ls -1 chunk_*.txt | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT echo "$count individual tests collected in $chunks files" diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 2a123c838ef7f..3ef84934ee659 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -56,6 +56,14 @@ jobs: image: redis ports: - 6379:6379 + postgres: + image: ${{ matrix.db == 'pgsql' && 'postgres:15' || '' }} + env: + POSTGRES_DB: test + POSTGRES_USER: test + POSTGRES_PASSWORD: test + ports: + - 5432:5432 strategy: fail-fast: false matrix: @@ -71,27 +79,20 @@ jobs: db: pgsql steps: - - name: Setting up DB mysql + - name: Run MySQL Server if: ${{ matrix.db == 'mysqli' }} - uses: moodlehq/mysql-action@v1 - with: - collation server: utf8mb4_bin - mysql version: 8.4 - mysql database: test - mysql user: test - mysql password: test - use tmpfs: true - tmpfs size: '1024M' - extra conf: --skip-log-bin - - - name: Setting up DB pgsql - if: ${{ matrix.db == 'pgsql' }} - uses: m4nu56/postgresql-action@v1 - with: - postgresql version: 15 - postgresql db: test - postgresql user: test - postgresql password: test + run: | + docker run --rm \ + -e MYSQL_DATABASE=test \ + -e MYSQL_USER=test \ + -e MYSQL_PASSWORD=test \ + -e MYSQL_ROOT_PASSWORD=test \ + -p 3306:3306 \ + -d \ + --tmpfs /var/lib/mysql:rw,noexec,nosuid,size=1024M \ + mysql:8.4 \ + --skip-log-bin \ + --collation-server=utf8mb4_bin - name: Configuring git vars uses: rlespinasse/github-slug-action@v4 diff --git a/.grunt/jsdoc/jsdoc.conf.js b/.grunt/jsdoc/jsdoc.conf.js index 29684651a1df6..bdaa01a62fcef 100644 --- a/.grunt/jsdoc/jsdoc.conf.js +++ b/.grunt/jsdoc/jsdoc.conf.js @@ -89,7 +89,7 @@ const templates = { module.exports = { opts: { - destination: "./jsdoc/", + destination: "./public/jsdoc/", template: "node_modules/docdash", }, plugins, diff --git a/.grunt/tasks/ignorefiles.js b/.grunt/tasks/ignorefiles.js index c90fae11283bd..4c3cad5a20d99 100644 --- a/.grunt/tasks/ignorefiles.js +++ b/.grunt/tasks/ignorefiles.js @@ -58,6 +58,31 @@ module.exports = grunt => { }) + "\n"); }; + /** + * Extracts ignore entries from a local ignore file. + * + * @param {string} componentPath the file path to the component, relative to the code base directory + * @param {string} ignoreFilePath the path to the ignore file + * @return {array} array of ignore paths to be included in the global ignore files + */ + const getEntriesFromLocalIgnoreFile = (componentPath, ignoreFilePath) => { + const ignorePaths = []; + if (grunt.file.exists(ignoreFilePath)) { + const ignoreFile = grunt.file.read(ignoreFilePath); + const entries = ignoreFile.split('\n'); + entries.forEach(entry => { + entry = entry.trim(); + if (entry.length > 0 && !entry.startsWith('#') && !entry.startsWith('!')) { + while (entry.startsWith('/')) { + entry = entry.substring(1); + } + ignorePaths.push(componentPath + '/' + entry); + } + }); + } + return ignorePaths; + }; + /** * Generate ignore files (utilising thirdpartylibs.xml data) */ @@ -67,6 +92,20 @@ module.exports = grunt => { // An array of paths to third party directories. const thirdPartyPaths = ComponentList.getThirdPartyPaths(); + const localStylelintIgnorePaths = []; + const localEslintIgnorePaths = []; + ComponentList.getComponentPaths(process.cwd() + '/').forEach(componentPath => { + const localEslintIgnorePath = process.cwd() + '/' + componentPath + '/.eslintignore'; + const localEslintIgnoreEntries = getEntriesFromLocalIgnoreFile(componentPath, localEslintIgnorePath); + if (localEslintIgnoreEntries.length > 0) { + localEslintIgnorePaths.push(...localEslintIgnoreEntries); + } + const localStylelintIgnorePath = process.cwd() + '/' + componentPath + '/.stylelintignore'; + const localStylelintIgnoreEntries = getEntriesFromLocalIgnoreFile(componentPath, localStylelintIgnorePath); + if (localStylelintIgnoreEntries.length > 0) { + localStylelintIgnorePaths.push(...localStylelintIgnoreEntries); + } + }); // Generate .eslintignore. const eslintIgnores = [ @@ -77,7 +116,7 @@ module.exports = grunt => { // Ignore all yui/src meta directories and build directories. '*/**/yui/src/*/meta/', '*/**/build/', - ].concat(thirdPartyPaths); + ].concat(thirdPartyPaths).concat(localEslintIgnorePaths); grunt.file.write('.eslintignore', eslintIgnores.join('\n') + '\n'); // Generate .stylelintignore. @@ -88,7 +127,7 @@ module.exports = grunt => { 'public/theme/classic/style/moodle.css', 'jsdoc/styles/*.css', 'public/admin/tool/componentlibrary/hugo/dist/css/docs.css', - ].concat(thirdPartyPaths); + ].concat(thirdPartyPaths).concat(localStylelintIgnorePaths); grunt.file.write('.stylelintignore', stylelintIgnores.join('\n') + '\n'); phpcsIgnore(thirdPartyPaths); diff --git a/.grunt/tasks/jsconfig.js b/.grunt/tasks/jsconfig.js index 33fbfb50d408a..9e32c2ebcbe95 100644 --- a/.grunt/tasks/jsconfig.js +++ b/.grunt/tasks/jsconfig.js @@ -43,8 +43,8 @@ module.exports = (grunt) => { const componentData = fetchComponentData().components; for (const [thisPath, component] of Object.entries(componentData)) { - jsconfigData.compilerOptions.paths[`${component}/*`] = [`public/${thisPath}/amd/src/*`]; - jsconfigData.include.push(`public/${thisPath}/amd/src/**/*`); + jsconfigData.compilerOptions.paths[`${component}/*`] = [`${thisPath}/amd/src/*`]; + jsconfigData.include.push(`${thisPath}/amd/src/**/*`); } grunt.file.write('jsconfig.json', JSON.stringify(jsconfigData, null, " ") + "\n"); diff --git a/.upgradenotes/MDL-85322-2026012001565471.yml b/.upgradenotes/MDL-85322-2026012001565471.yml new file mode 100644 index 0000000000000..fcd2247dd4d0a --- /dev/null +++ b/.upgradenotes/MDL-85322-2026012001565471.yml @@ -0,0 +1,8 @@ +issueNumber: MDL-85322 +notes: + block_html: + - message: >- + Treat Dashboard (pagetype 'my-index') as trusted in web services so + get_content_for_external preserves embedded HTML (e.g. iframes) on user + Dashboard. + type: changed diff --git a/.upgradenotes/MDL-86524-2025120211115999.yml b/.upgradenotes/MDL-86524-2025120211115999.yml new file mode 100644 index 0000000000000..5a72101c37fbd --- /dev/null +++ b/.upgradenotes/MDL-86524-2025120211115999.yml @@ -0,0 +1,12 @@ +issueNumber: MDL-86524 +notes: + core_question: + - message: >- + During restore of a question_set_reference, mapping of IDs in the + filtercondition is now delegated to qbank plugins. + If your qbank plugin defines a filter condition that uses database + IDs, add an override of `restore_filtercondition()` to the `condition` + class, which checks the condition's data and replaces the IDs with + mapped values if required. See + `qbank_managecategories\category_condition` for an example. + type: improved diff --git a/.upgradenotes/MDL-87993-2026022007035360.yml b/.upgradenotes/MDL-87993-2026022007035360.yml new file mode 100644 index 0000000000000..bb0f82433a2cf --- /dev/null +++ b/.upgradenotes/MDL-87993-2026022007035360.yml @@ -0,0 +1,8 @@ +issueNumber: MDL-87993 +notes: + core: + - message: >- + The `core/toast` JS module now accepts a `visuallyHidden` configuration + parameter to render visually hidden toast messages for screen reader + users. + type: improved diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000..ad3402f9af682 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,55 @@ +FROM php:8.4-fpm as php-base + +RUN apt-get update && apt-get install -y \ + $PHPIZE_DEPS \ + libcurl4-openssl-dev \ + libicu-dev \ + libzip-dev \ + libpng-dev \ + libjpeg62-turbo-dev \ + libfreetype6-dev \ + libxml2-dev \ + libonig-dev \ + libsodium-dev \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +RUN docker-php-ext-configure gd --with-freetype --with-jpeg + +RUN docker-php-ext-install \ + curl \ + intl \ + mbstring \ + zip \ + gd \ + soap \ + sodium \ + pgsql \ + pdo_pgsql \ + exif + +WORKDIR /var/www/moodle + +FROM php-base as builder + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +WORKDIR /var/www/moodle + +COPY . . + +RUN composer install --no-dev --classmap-authoritative + +FROM php-base as php-release + +WORKDIR /var/www/moodle + +COPY --from=builder /var/www/moodle /var/www/moodle + +RUN chown -R www-data:www-data /var/www/moodle + +FROM nginx:alpine as nginx-release + +WORKDIR /var/www/moodle + +COPY --from=builder /var/www/moodle /var/www/moodle diff --git a/UPGRADING.md b/UPGRADING.md index 72173206087dd..f339705091add 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -6,6 +6,138 @@ More detailed information on key changes can be found in the [Developer update n The format of this change log follows the advice given at [Keep a CHANGELOG](https://keepachangelog.com). +## 5.1.3+ + +### core + +#### Added + +- The `core/toast` JS module now accepts a `visuallyHidden` configuration parameter to render visually hidden toast messages for screen reader users. + + For more information see [MDL-87993](https://tracker.moodle.org/browse/MDL-87993) + +### core_question + +#### Added + +- During restore of a question_set_reference, mapping of IDs in the filtercondition is now delegated to qbank plugins. If your qbank plugin defines a filter condition that uses database IDs, add an override of `restore_filtercondition()` to the `condition` class, which checks the condition's data and replaces the IDs with mapped values if required. See `qbank_managecategories\category_condition` for an example. + + For more information see [MDL-86524](https://tracker.moodle.org/browse/MDL-86524) + +### block_html + +#### Changed + +- Treat Dashboard (pagetype 'my-index') as trusted in web services so get_content_for_external preserves embedded HTML (e.g. iframes) on user Dashboard. + + For more information see [MDL-85322](https://tracker.moodle.org/browse/MDL-85322) + +## 5.1.2 + +### core + +#### Added + +- There is a new Behat `toast_message` named selector to more easily assert the presence of Toast messages on the page + + For more information see [MDL-87443](https://tracker.moodle.org/browse/MDL-87443) + +#### Changed + +- `\core\output\core_renderer::confirm()`'s `$displayoptions` parameter now also accepts a `headinglevel` option that developers can use to specify the heading level of the confirmation's heading. If not specified, the confirmation heading will be rendered in an `h4` tag. + + For more information see [MDL-87694](https://tracker.moodle.org/browse/MDL-87694) + +### core_customfield + +#### Added + +- Added new `\core_customfield\api::is_shortname_unique(...)` method to determine whether a shortname is available for use inside a given handler + + For more information see [MDL-87059](https://tracker.moodle.org/browse/MDL-87059) + +### core_question + +#### Fixed + +- In order to prevent re-use of question version numbers after a version is deleted, the `nextversion` column was added to `question_bank_entries`. This serves as a counter incremented each time a version is created. + Do not query this field directly. Instead use `core_question\versions::get_next_version()` to read the value, which will initialise it based on the existing versions if it is not set yet. By default, it will increment the version number automatically, unless you pass `increment: false`. Because of this, it is advisable to call it inside a transaction, that is only committed after the version number is used in a `question_versions` record. + + For more information see [MDL-86798](https://tracker.moodle.org/browse/MDL-86798) + +### customfield_number + +#### Changed + +- In order to fully support shared custom field categories, additional component/area/itemid parameters have been added to the following: + + * The `customfield_number_recalculate_value` external method + * The abstract `\customfield_number\provider_base::recalculate()` method + * The `\customfield_number\task\recalculate` helpers for queueing task instances + + For more information see [MDL-87714](https://tracker.moodle.org/browse/MDL-87714) + +## 5.1.1 + +### core + +#### Added + +- Added clean_string() that prevents double escaping in Mustache templates + + For more information see [MDL-87066](https://tracker.moodle.org/browse/MDL-87066) + +#### Changed + +- The Hook Manager now uses localcache instead of caching via MUC. + + For more information see [MDL-87107](https://tracker.moodle.org/browse/MDL-87107) + +#### Fixed + +- `restore_qtype_plugin::unset_excluded_fields` now returns the modified questiondata structure, + in order to support structures that contain arrays. + If your qtype plugin overrides `restore_qtype_plugin::remove_excluded_question_data` without + calling the parent method, you may need to modify your overridden method to use the returned + value. + + For more information see [MDL-85975](https://tracker.moodle.org/browse/MDL-85975) +- When responding to pcntl signals, call existing signal handlers. + + For more information see [MDL-87079](https://tracker.moodle.org/browse/MDL-87079) + +### core_completion + +#### Changed + +- The `completion_info::clear_criteria` method takes an optional `$removetypecriteria` to determine whether to remove course type criteria from other courses that refer to the current course + + For more information see [MDL-86332](https://tracker.moodle.org/browse/MDL-86332) + +### core_course + +#### Added + +- The external function `core_course_get_course_contents` now includes the `candisplay` property for each returned module. If this is false, the module should not be displayed on the course page (for example, for question banks). + + For more information see [MDL-85405](https://tracker.moodle.org/browse/MDL-85405) + +### core_group + +#### Added + +- `groups_print_activity_menu()` and `groups_get_activity_group()` now include an additional `$participationonly` parameter, which is true by default. This can be set false when we want the user to be able to select a non-participation group within an activity, for example if a teacher wants to filter assignment submissions by non-participation groups. It should never be used when the menu is displayed to students, as this may allow them to participate using non-participation groups. Non-participation groups are labeled as such. + + For more information see [MDL-81514](https://tracker.moodle.org/browse/MDL-81514) + +### mod_glossary + +#### Added + +- Function mod_glossary_rating_can_see_item_ratings is now implemented for checking permissions to view ratings. + + For more information see [MDL-86960](https://tracker.moodle.org/browse/MDL-86960) + ## 5.1 ### core diff --git a/admin/cli/scheduled_task.php b/admin/cli/scheduled_task.php index d75b4f8e0fbdb..940f6df22bd38 100644 --- a/admin/cli/scheduled_task.php +++ b/admin/cli/scheduled_task.php @@ -125,11 +125,6 @@ exit(0); } -if (moodle_needs_upgrading()) { - mtrace("Moodle upgrade pending, cannot manage tasks."); - exit(1); -} - if ($disable = $options['disable']) { if (!$task = \core\task\manager::get_scheduled_task($disable)) { mtrace("Task '$disable' not found"); @@ -157,6 +152,11 @@ exit(1); } } else if ($execute = $options['execute']) { + if (moodle_needs_upgrading()) { + mtrace("Moodle upgrade pending, cannot execute tasks."); + exit(1); + } + if (!$task = \core\task\manager::get_scheduled_task($execute)) { mtrace("Task '$execute' not found"); exit(1); diff --git a/composer.json b/composer.json index d2fe183f6d4d4..336f419d9d108 100644 --- a/composer.json +++ b/composer.json @@ -2,8 +2,11 @@ "name": "moodle/moodle", "license": "GPL-3.0-or-later", "description": "Moodle - the world's open source learning platform", - "type": "project", + "type": "moodle-core", "homepage": "https://moodle.org", + "provide": { + "moodle/lms": "5.1" + }, "require-dev": { "phpunit/phpunit": "^11", "mikey179/vfsstream": "1.6.*", @@ -26,6 +29,9 @@ "core_phpunit\\": "public/lib/phpunit/classes/" } }, + "extra": { + "haspublicdir": true + }, "minimum-stability": "dev", "prefer-stable": true, "require": { diff --git a/composer.lock b/composer.lock index ea964a4fd9783..8f49b589e945c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,30 +4,30 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6dea3d23fae938df252056489715c946", + "content-hash": "dbc988c9feb49514ee1ac7b206929260", "packages": [], "packages-dev": [ { "name": "behat/behat", - "version": "v3.19.0", + "version": "v3.29.0", "source": { "type": "git", "url": "https://github.com/Behat/Behat.git", - "reference": "6cf82375a88145e33e10a34b211a3f914cbd02ee" + "reference": "51bdf81639a14645c5d2c06926f4aa37d204921b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Behat/zipball/6cf82375a88145e33e10a34b211a3f914cbd02ee", - "reference": "6cf82375a88145e33e10a34b211a3f914cbd02ee", + "url": "https://api.github.com/repos/Behat/Behat/zipball/51bdf81639a14645c5d2c06926f4aa37d204921b", + "reference": "51bdf81639a14645c5d2c06926f4aa37d204921b", "shasum": "" }, "require": { - "behat/gherkin": "^4.10.0", - "behat/transliterator": "^1.5", + "behat/gherkin": "^4.12.0", "composer-runtime-api": "^2.2", - "composer/xdebug-handler": "^3.0", + "composer/xdebug-handler": "^1.4 || ^2.0 || ^3.0", "ext-mbstring": "*", - "php": "8.1.* || 8.2.* || 8.3.* || 8.4.* ", + "nikic/php-parser": "^4.19.2 || ^5.2", + "php": ">=8.1 <8.6", "psr/container": "^1.0 || ^2.0", "symfony/config": "^5.4 || ^6.4 || ^7.0", "symfony/console": "^5.4 || ^6.4 || ^7.0", @@ -37,10 +37,13 @@ "symfony/yaml": "^5.4 || ^6.4 || ^7.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.68", + "opis/json-schema": "^2.5", + "php-cs-fixer/shim": "^3.89", "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^9.6", + "rector/rector": "2.1.7", "sebastian/diff": "^4.0", + "symfony/filesystem": "^5.4 || ^6.4 || ^7.0", "symfony/polyfill-php84": "^1.31", "symfony/process": "^5.4 || ^6.4 || ^7.0" }, @@ -51,11 +54,6 @@ "bin/behat" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, "autoload": { "psr-4": { "Behat\\Hook\\": "src/Behat/Hook/", @@ -95,36 +93,50 @@ ], "support": { "issues": "https://github.com/Behat/Behat/issues", - "source": "https://github.com/Behat/Behat/tree/v3.19.0" + "source": "https://github.com/Behat/Behat/tree/v3.29.0" }, - "time": "2025-02-13T09:07:11+00:00" + "funding": [ + { + "url": "https://github.com/acoulton", + "type": "github" + }, + { + "url": "https://github.com/carlos-granados", + "type": "github" + }, + { + "url": "https://github.com/stof", + "type": "github" + } + ], + "time": "2025-12-11T09:51:30+00:00" }, { "name": "behat/gherkin", - "version": "v4.12.0", + "version": "v4.16.1", "source": { "type": "git", "url": "https://github.com/Behat/Gherkin.git", - "reference": "cc3a7e224b36373be382b53ef02ede0f1807bb58" + "reference": "e26037937dfd48528746764dd870bc5d0836665f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Gherkin/zipball/cc3a7e224b36373be382b53ef02ede0f1807bb58", - "reference": "cc3a7e224b36373be382b53ef02ede0f1807bb58", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/e26037937dfd48528746764dd870bc5d0836665f", + "reference": "e26037937dfd48528746764dd870bc5d0836665f", "shasum": "" }, "require": { "composer-runtime-api": "^2.2", - "php": "8.1.* || 8.2.* || 8.3.* || 8.4.*" + "php": ">=8.1 <8.6" }, "require-dev": { - "cucumber/cucumber": "dev-gherkin-24.1.0", - "friendsofphp/php-cs-fixer": "^3.65", + "cucumber/gherkin-monorepo": "dev-gherkin-v37.0.0", + "friendsofphp/php-cs-fixer": "^3.77", + "mikey179/vfsstream": "^1.6", "phpstan/extension-installer": "^1", "phpstan/phpstan": "^2", "phpstan/phpstan-phpunit": "^2", "phpunit/phpunit": "^10.5", - "symfony/filesystem": "^5.4 || ^6.4 || ^7.0", "symfony/yaml": "^5.4 || ^6.4 || ^7.0" }, "suggest": { @@ -137,8 +149,8 @@ } }, "autoload": { - "psr-0": { - "Behat\\Gherkin": "src/" + "psr-4": { + "Behat\\Gherkin\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -164,34 +176,49 @@ ], "support": { "issues": "https://github.com/Behat/Gherkin/issues", - "source": "https://github.com/Behat/Gherkin/tree/v4.12.0" + "source": "https://github.com/Behat/Gherkin/tree/v4.16.1" }, - "time": "2025-02-26T14:28:23+00:00" + "funding": [ + { + "url": "https://github.com/acoulton", + "type": "github" + }, + { + "url": "https://github.com/carlos-granados", + "type": "github" + }, + { + "url": "https://github.com/stof", + "type": "github" + } + ], + "time": "2025-12-08T16:12:58+00:00" }, { "name": "behat/mink", - "version": "v1.12.0", + "version": "v1.13.0", "source": { "type": "git", "url": "https://github.com/minkphp/Mink.git", - "reference": "7e4edec6c335937029cb3569ce7ef81182804d0a" + "reference": "9b08f62937c173affe070c04bb072d7ea1db1be5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/minkphp/Mink/zipball/7e4edec6c335937029cb3569ce7ef81182804d0a", - "reference": "7e4edec6c335937029cb3569ce7ef81182804d0a", + "url": "https://api.github.com/repos/minkphp/Mink/zipball/9b08f62937c173affe070c04bb072d7ea1db1be5", + "reference": "9b08f62937c173affe070c04bb072d7ea1db1be5", "shasum": "" }, "require": { "php": ">=7.2", - "symfony/css-selector": "^4.4 || ^5.0 || ^6.0 || ^7.0" + "symfony/css-selector": "^4.4 || ^5.0 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^1.10", + "jetbrains/phpstorm-attributes": "*", + "phpstan/phpstan": "^1.12.32", "phpstan/phpstan-phpunit": "^1.3", "phpunit/phpunit": "^8.5.22 || ^9.5.11", - "symfony/error-handler": "^4.4 || ^5.0 || ^6.0 || ^7.0", - "symfony/phpunit-bridge": "^5.4 || ^6.0 || ^7.0" + "symfony/error-handler": "^4.4 || ^5.0 || ^6.0 || ^7.0 || ^8.0", + "symfony/phpunit-bridge": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "suggest": { "behat/mink-browserkit-driver": "fast headless driver for any app without JS emulation", @@ -230,40 +257,40 @@ ], "support": { "issues": "https://github.com/minkphp/Mink/issues", - "source": "https://github.com/minkphp/Mink/tree/v1.12.0" + "source": "https://github.com/minkphp/Mink/tree/v1.13.0" }, - "time": "2024-10-30T18:48:14+00:00" + "time": "2025-11-22T12:18:15+00:00" }, { "name": "behat/mink-browserkit-driver", - "version": "v2.2.0", + "version": "v2.3.0", "source": { "type": "git", "url": "https://github.com/minkphp/MinkBrowserKitDriver.git", - "reference": "16d53476e42827ed3aafbfa4fde17a1743eafd50" + "reference": "d361516cba6e684bdc4518b9c044edc77f249e36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/minkphp/MinkBrowserKitDriver/zipball/16d53476e42827ed3aafbfa4fde17a1743eafd50", - "reference": "16d53476e42827ed3aafbfa4fde17a1743eafd50", + "url": "https://api.github.com/repos/minkphp/MinkBrowserKitDriver/zipball/d361516cba6e684bdc4518b9c044edc77f249e36", + "reference": "d361516cba6e684bdc4518b9c044edc77f249e36", "shasum": "" }, "require": { "behat/mink": "^1.11.0@dev", "ext-dom": "*", "php": ">=7.2", - "symfony/browser-kit": "^4.4 || ^5.0 || ^6.0 || ^7.0", - "symfony/dom-crawler": "^4.4 || ^5.0 || ^6.0 || ^7.0" + "symfony/browser-kit": "^4.4 || ^5.0 || ^6.0 || ^7.0 || ^8.0", + "symfony/dom-crawler": "^4.4 || ^5.0 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { "mink/driver-testsuite": "dev-master", "phpstan/phpstan": "^1.10", "phpstan/phpstan-phpunit": "^1.3", "phpunit/phpunit": "^8.5 || ^9.5", - "symfony/error-handler": "^4.4 || ^5.0 || ^6.0 || ^7.0", - "symfony/http-client": "^4.4 || ^5.0 || ^6.0 || ^7.0", - "symfony/http-kernel": "^4.4 || ^5.0 || ^6.0 || ^7.0", - "symfony/mime": "^4.4 || ^5.0 || ^6.0 || ^7.0", + "symfony/error-handler": "^4.4 || ^5.0 || ^6.0 || ^7.0 || ^8.0", + "symfony/http-client": "^4.4 || ^5.0 || ^6.0 || ^7.0 || ^8.0", + "symfony/http-kernel": "^4.4 || ^5.0 || ^6.0 || ^7.0 || ^8.0", + "symfony/mime": "^4.4 || ^5.0 || ^6.0 || ^7.0 || ^8.0", "yoast/phpunit-polyfills": "^1.0" }, "type": "mink-driver", @@ -298,58 +325,9 @@ ], "support": { "issues": "https://github.com/minkphp/MinkBrowserKitDriver/issues", - "source": "https://github.com/minkphp/MinkBrowserKitDriver/tree/v2.2.0" + "source": "https://github.com/minkphp/MinkBrowserKitDriver/tree/v2.3.0" }, - "time": "2023-12-09T11:30:50+00:00" - }, - { - "name": "behat/transliterator", - "version": "v1.5.0", - "source": { - "type": "git", - "url": "https://github.com/Behat/Transliterator.git", - "reference": "baac5873bac3749887d28ab68e2f74db3a4408af" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Behat/Transliterator/zipball/baac5873bac3749887d28ab68e2f74db3a4408af", - "reference": "baac5873bac3749887d28ab68e2f74db3a4408af", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "require-dev": { - "chuyskywalker/rolling-curl": "^3.1", - "php-yaoi/php-yaoi": "^1.0", - "phpunit/phpunit": "^8.5.25 || ^9.5.19" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Behat\\Transliterator\\": "src/Behat/Transliterator" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Artistic-1.0" - ], - "description": "String transliterator", - "keywords": [ - "i18n", - "slug", - "transliterator" - ], - "support": { - "issues": "https://github.com/Behat/Transliterator/issues", - "source": "https://github.com/Behat/Transliterator/tree/v1.5.0" - }, - "time": "2022-03-30T09:27:43+00:00" + "time": "2025-11-22T12:42:18+00:00" }, { "name": "composer/pcre", @@ -498,16 +476,16 @@ }, { "name": "filp/whoops", - "version": "2.17.0", + "version": "2.18.4", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "075bc0c26631110584175de6523ab3f1652eb28e" + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/075bc0c26631110584175de6523ab3f1652eb28e", - "reference": "075bc0c26631110584175de6523ab3f1652eb28e", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", "shasum": "" }, "require": { @@ -557,7 +535,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.17.0" + "source": "https://github.com/filp/whoops/tree/2.18.4" }, "funding": [ { @@ -565,7 +543,7 @@ "type": "github" } ], - "time": "2025-01-25T12:00:00+00:00" + "time": "2025-08-08T12:00:00+00:00" }, { "name": "friends-of-behat/mink-extension", @@ -635,16 +613,16 @@ }, { "name": "masterminds/html5", - "version": "2.9.0", + "version": "2.10.0", "source": { "type": "git", "url": "https://github.com/Masterminds/html5-php.git", - "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6" + "reference": "fcf91eb64359852f00d921887b219479b4f21251" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", - "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251", + "reference": "fcf91eb64359852f00d921887b219479b4f21251", "shasum": "" }, "require": { @@ -696,9 +674,9 @@ ], "support": { "issues": "https://github.com/Masterminds/html5-php/issues", - "source": "https://github.com/Masterminds/html5-php/tree/2.9.0" + "source": "https://github.com/Masterminds/html5-php/tree/2.10.0" }, - "time": "2024-03-31T07:05:07+00:00" + "time": "2025-07-25T09:04:22+00:00" }, { "name": "mikey179/vfsstream", @@ -754,16 +732,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.13.0", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "024473a478be9df5fdaca2c793f2232fe788e414" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414", - "reference": "024473a478be9df5fdaca2c793f2232fe788e414", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -802,7 +780,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -810,20 +788,20 @@ "type": "tidelift" } ], - "time": "2025-02-12T12:17:51+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "nikic/php-parser", - "version": "v5.4.0", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -842,7 +820,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -866,9 +844,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2024-12-30T11:07:19+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "oleg-andreyev/mink-phpwebdriver", @@ -1056,16 +1034,16 @@ }, { "name": "php-webdriver/webdriver", - "version": "1.15.2", + "version": "1.16.0", "source": { "type": "git", "url": "https://github.com/php-webdriver/php-webdriver.git", - "reference": "998e499b786805568deaf8cbf06f4044f05d91bf" + "reference": "ac0662863aa120b4f645869f584013e4c4dba46a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/998e499b786805568deaf8cbf06f4044f05d91bf", - "reference": "998e499b786805568deaf8cbf06f4044f05d91bf", + "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/ac0662863aa120b4f645869f584013e4c4dba46a", + "reference": "ac0662863aa120b4f645869f584013e4c4dba46a", "shasum": "" }, "require": { @@ -1074,7 +1052,7 @@ "ext-zip": "*", "php": "^7.3 || ^8.0", "symfony/polyfill-mbstring": "^1.12", - "symfony/process": "^5.0 || ^6.0 || ^7.0" + "symfony/process": "^5.0 || ^6.0 || ^7.0 || ^8.0" }, "replace": { "facebook/webdriver": "*" @@ -1087,10 +1065,10 @@ "php-parallel-lint/php-parallel-lint": "^1.2", "phpunit/phpunit": "^9.3", "squizlabs/php_codesniffer": "^3.5", - "symfony/var-dumper": "^5.0 || ^6.0 || ^7.0" + "symfony/var-dumper": "^5.0 || ^6.0 || ^7.0 || ^8.0" }, "suggest": { - "ext-SimpleXML": "For Firefox profile creation" + "ext-simplexml": "For Firefox profile creation" }, "type": "library", "autoload": { @@ -1116,41 +1094,41 @@ ], "support": { "issues": "https://github.com/php-webdriver/php-webdriver/issues", - "source": "https://github.com/php-webdriver/php-webdriver/tree/1.15.2" + "source": "https://github.com/php-webdriver/php-webdriver/tree/1.16.0" }, - "time": "2024-11-21T15:12:59+00:00" + "time": "2025-12-28T23:57:40+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "11.0.9", + "version": "11.0.12", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7" + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/14d63fbcca18457e49c6f8bebaa91a87e8e188d7", - "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^5.4.0", + "nikic/php-parser": "^5.7.0", "php": ">=8.2", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-text-template": "^4.0.1", "sebastian/code-unit-reverse-lookup": "^4.0.1", "sebastian/complexity": "^4.0.1", - "sebastian/environment": "^7.2.0", + "sebastian/environment": "^7.2.1", "sebastian/lines-of-code": "^3.0.1", "sebastian/version": "^5.0.2", - "theseer/tokenizer": "^1.2.3" + "theseer/tokenizer": "^1.3.1" }, "require-dev": { - "phpunit/phpunit": "^11.5.2" + "phpunit/phpunit": "^11.5.46" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -1188,40 +1166,52 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.9" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" } ], - "time": "2025-02-25T13:26:39+00:00" + "time": "2025-12-24T07:01:01+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "5.1.0", + "version": "5.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6", - "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -1249,15 +1239,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" } ], - "time": "2024-08-27T05:02:59+00:00" + "time": "2026-02-02T13:52:54+00:00" }, { "name": "phpunit/php-invoker", @@ -1445,16 +1447,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.12", + "version": "11.5.55", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "d42785840519401ed2113292263795eb4c0f95da" + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d42785840519401ed2113292263795eb4c0f95da", - "reference": "d42785840519401ed2113292263795eb4c0f95da", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", "shasum": "" }, "require": { @@ -1464,24 +1466,25 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.0", + "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.2", - "phpunit/php-code-coverage": "^11.0.9", - "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-code-coverage": "^11.0.12", + "phpunit/php-file-iterator": "^5.1.1", "phpunit/php-invoker": "^5.0.1", "phpunit/php-text-template": "^4.0.1", "phpunit/php-timer": "^7.0.1", "sebastian/cli-parser": "^3.0.2", - "sebastian/code-unit": "^3.0.2", - "sebastian/comparator": "^6.3.1", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.3", "sebastian/diff": "^6.0.2", - "sebastian/environment": "^7.2.0", - "sebastian/exporter": "^6.3.0", + "sebastian/environment": "^7.2.1", + "sebastian/exporter": "^6.3.2", "sebastian/global-state": "^7.0.2", "sebastian/object-enumerator": "^6.0.1", - "sebastian/type": "^5.1.0", + "sebastian/recursion-context": "^6.0.3", + "sebastian/type": "^5.1.3", "sebastian/version": "^5.0.2", "staabm/side-effects-detector": "^1.0.5" }, @@ -1526,7 +1529,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.12" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55" }, "funding": [ { @@ -1537,12 +1540,20 @@ "url": "https://github.com/sebastianbergmann", "type": "github" }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", "type": "tidelift" } ], - "time": "2025-03-07T07:31:03+00:00" + "time": "2026-02-18T12:37:06+00:00" }, { "name": "psr/container", @@ -1756,16 +1767,16 @@ }, { "name": "sebastian/code-unit", - "version": "3.0.2", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca" + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca", - "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", "shasum": "" }, "require": { @@ -1801,7 +1812,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/code-unit/issues", "security": "https://github.com/sebastianbergmann/code-unit/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.2" + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" }, "funding": [ { @@ -1809,7 +1820,7 @@ "type": "github" } ], - "time": "2024-12-12T09:59:06+00:00" + "time": "2025-03-19T07:56:08+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -1869,16 +1880,16 @@ }, { "name": "sebastian/comparator", - "version": "6.3.1", + "version": "6.3.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959" + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/24b8fbc2c8e201bb1308e7b05148d6ab393b6959", - "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", "shasum": "" }, "require": { @@ -1937,15 +1948,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.1" + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" } ], - "time": "2025-03-07T06:57:01+00:00" + "time": "2026-01-24T09:26:40+00:00" }, { "name": "sebastian/complexity", @@ -2074,23 +2097,23 @@ }, { "name": "sebastian/environment", - "version": "7.2.0", + "version": "7.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5" + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", - "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.3" }, "suggest": { "ext-posix": "*" @@ -2126,28 +2149,40 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/7.2.0" + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" } ], - "time": "2024-07-03T04:54:44+00:00" + "time": "2025-05-21T11:55:47+00:00" }, { "name": "sebastian/exporter", - "version": "6.3.0", + "version": "6.3.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3" + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/3473f61172093b2da7de1fb5782e1f24cc036dc3", - "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", "shasum": "" }, "require": { @@ -2161,7 +2196,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.1-dev" + "dev-main": "6.3-dev" } }, "autoload": { @@ -2204,15 +2239,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.0" + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-12-05T09:17:50+00:00" + "time": "2025-09-24T06:12:51+00:00" }, { "name": "sebastian/global-state", @@ -2450,23 +2497,23 @@ }, { "name": "sebastian/recursion-context", - "version": "6.0.2", + "version": "6.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "694d156164372abbd149a4b85ccda2e4670c0e16" + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/694d156164372abbd149a4b85ccda2e4670c0e16", - "reference": "694d156164372abbd149a4b85ccda2e4670c0e16", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.3" }, "type": "library", "extra": { @@ -2502,28 +2549,40 @@ "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.2" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2024-07-03T05:10:34+00:00" + "time": "2025-08-13T04:42:22+00:00" }, { "name": "sebastian/type", - "version": "5.1.0", + "version": "5.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac" + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/461b9c5da241511a2a0e8f240814fb23ce5c0aac", - "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", "shasum": "" }, "require": { @@ -2559,15 +2618,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/type/issues", "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/5.1.0" + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" } ], - "time": "2024-09-17T13:12:04+00:00" + "time": "2025-08-09T06:55:48+00:00" }, { "name": "sebastian/version", @@ -2677,27 +2748,28 @@ }, { "name": "symfony/browser-kit", - "version": "v7.2.4", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "8ce0ee23857d87d5be493abba2d52d1f9e49da61" + "reference": "bed167eadaaba641f51fc842c9227aa5e251309e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/8ce0ee23857d87d5be493abba2d52d1f9e49da61", - "reference": "8ce0ee23857d87d5be493abba2d52d1f9e49da61", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/bed167eadaaba641f51fc842c9227aa5e251309e", + "reference": "bed167eadaaba641f51fc842c9227aa5e251309e", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/dom-crawler": "^6.4|^7.0" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/dom-crawler": "^6.4|^7.0|^8.0" }, "require-dev": { - "symfony/css-selector": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0" + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -2725,7 +2797,7 @@ "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/browser-kit/tree/v7.2.4" + "source": "https://github.com/symfony/browser-kit/tree/v7.4.4" }, "funding": [ { @@ -2736,31 +2808,35 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-14T14:27:24+00:00" + "time": "2026-01-13T10:40:19+00:00" }, { "name": "symfony/config", - "version": "v7.2.3", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "7716594aaae91d9141be080240172a92ecca4d44" + "reference": "4275b53b8ab0cf37f48bf273dc2285c8178efdfb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/7716594aaae91d9141be080240172a92ecca4d44", - "reference": "7716594aaae91d9141be080240172a92ecca4d44", + "url": "https://api.github.com/repos/symfony/config/zipball/4275b53b8ab0cf37f48bf273dc2285c8178efdfb", + "reference": "4275b53b8ab0cf37f48bf273dc2285c8178efdfb", "shasum": "" }, "require": { "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/filesystem": "^7.1", + "symfony/filesystem": "^7.1|^8.0", "symfony/polyfill-ctype": "~1.8" }, "conflict": { @@ -2768,11 +2844,11 @@ "symfony/service-contracts": "<2.5" }, "require-dev": { - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^6.4|^7.0" + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -2800,7 +2876,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v7.2.3" + "source": "https://github.com/symfony/config/tree/v7.4.4" }, "funding": [ { @@ -2811,32 +2887,37 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-01-22T12:07:01+00:00" + "time": "2026-01-13T11:36:38+00:00" }, { "name": "symfony/console", - "version": "v7.2.1", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3" + "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/fefcc18c0f5d0efe3ab3152f15857298868dc2c3", - "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3", + "url": "https://api.github.com/repos/symfony/console/zipball/41e38717ac1dd7a46b6bda7d6a82af2d98a78894", + "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^6.4|^7.0" + "symfony/string": "^7.2|^8.0" }, "conflict": { "symfony/dependency-injection": "<6.4", @@ -2850,16 +2931,16 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -2893,7 +2974,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.2.1" + "source": "https://github.com/symfony/console/tree/v7.4.4" }, "funding": [ { @@ -2904,25 +2985,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-12-11T03:49:26+00:00" + "time": "2026-01-13T11:36:38+00:00" }, { "name": "symfony/css-selector", - "version": "v7.2.0", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2" + "reference": "ab862f478513e7ca2fe9ec117a6f01a8da6e1135" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2", - "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/ab862f478513e7ca2fe9ec117a6f01a8da6e1135", + "reference": "ab862f478513e7ca2fe9ec117a6f01a8da6e1135", "shasum": "" }, "require": { @@ -2958,7 +3043,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.2.0" + "source": "https://github.com/symfony/css-selector/tree/v7.4.0" }, "funding": [ { @@ -2969,33 +3054,37 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2025-10-30T13:39:42+00:00" }, { "name": "symfony/dependency-injection", - "version": "v7.2.4", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "f0a1614cccb4b8431a97076f9debc08ddca321ca" + "reference": "76a02cddca45a5254479ad68f9fa274ead0a7ef2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f0a1614cccb4b8431a97076f9debc08ddca321ca", - "reference": "f0a1614cccb4b8431a97076f9debc08ddca321ca", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/76a02cddca45a5254479ad68f9fa274ead0a7ef2", + "reference": "76a02cddca45a5254479ad68f9fa274ead0a7ef2", "shasum": "" }, "require": { "php": ">=8.2", "psr/container": "^1.1|^2.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/service-contracts": "^3.5", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/service-contracts": "^3.6", + "symfony/var-exporter": "^6.4.20|^7.2.5|^8.0" }, "conflict": { "ext-psr": "<1.1|>=2", @@ -3008,9 +3097,9 @@ "symfony/service-implementation": "1.1|2.0|3.0" }, "require-dev": { - "symfony/config": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -3038,7 +3127,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v7.2.4" + "source": "https://github.com/symfony/dependency-injection/tree/v7.4.5" }, "funding": [ { @@ -3049,25 +3138,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-21T09:47:16+00:00" + "time": "2026-01-27T16:16:02+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { @@ -3080,7 +3173,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -3105,7 +3198,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -3121,30 +3214,31 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/dom-crawler", - "version": "v7.2.4", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7" + "reference": "71fd6a82fc357c8b5de22f78b228acfc43dee965" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7", - "reference": "19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/71fd6a82fc357c8b5de22f78b228acfc43dee965", + "reference": "71fd6a82fc357c8b5de22f78b228acfc43dee965", "shasum": "" }, "require": { "masterminds/html5": "^2.6", "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.0" }, "require-dev": { - "symfony/css-selector": "^6.4|^7.0" + "symfony/css-selector": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -3172,7 +3266,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v7.2.4" + "source": "https://github.com/symfony/dom-crawler/tree/v7.4.4" }, "funding": [ { @@ -3183,25 +3277,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-17T15:53:07+00:00" + "time": "2026-01-05T08:47:25+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.2.0", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1" + "reference": "dc2c0eba1af673e736bb851d747d266108aea746" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/910c5db85a5356d0fea57680defec4e99eb9c8c1", - "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/dc2c0eba1af673e736bb851d747d266108aea746", + "reference": "dc2c0eba1af673e736bb851d747d266108aea746", "shasum": "" }, "require": { @@ -3218,13 +3316,14 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/error-handler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/stopwatch": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -3252,7 +3351,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.2.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.4" }, "funding": [ { @@ -3263,25 +3362,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-01-05T11:45:34+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", "shasum": "" }, "require": { @@ -3295,7 +3398,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -3328,7 +3431,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" }, "funding": [ { @@ -3344,20 +3447,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/filesystem", - "version": "v7.2.0", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb" + "reference": "d551b38811096d0be9c4691d406991b47c0c630a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb", - "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a", + "reference": "d551b38811096d0be9c4691d406991b47c0c630a", "shasum": "" }, "require": { @@ -3366,7 +3469,7 @@ "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/process": "^6.4|^7.0" + "symfony/process": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -3394,7 +3497,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.2.0" + "source": "https://github.com/symfony/filesystem/tree/v7.4.0" }, "funding": [ { @@ -3405,25 +3508,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-10-25T15:15:23+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/http-client", - "version": "v7.2.4", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6" + "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", - "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", + "url": "https://api.github.com/repos/symfony/http-client/zipball/84bb634857a893cc146cceb467e31b3f02c5fe9f", + "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f", "shasum": "" }, "require": { @@ -3431,10 +3538,12 @@ "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", "symfony/http-client-contracts": "~3.4.4|^3.5.2", + "symfony/polyfill-php83": "^1.29", "symfony/service-contracts": "^2.5|^3" }, "conflict": { "amphp/amp": "<2.5", + "amphp/socket": "<1.1", "php-http/discovery": "<1.15", "symfony/http-foundation": "<6.4" }, @@ -3447,18 +3556,18 @@ "require-dev": { "amphp/http-client": "^4.2.1|^5.0", "amphp/http-tunnel": "^1.0|^2.0", - "amphp/socket": "^1.1", "guzzlehttp/promises": "^1.4|^2.0", "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", "symfony/amphp-http-client-meta": "^1.0|^2.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -3489,7 +3598,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.2.4" + "source": "https://github.com/symfony/http-client/tree/v7.4.5" }, "funding": [ { @@ -3500,25 +3609,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-13T10:27:23+00:00" + "time": "2026-01-27T16:16:02+00:00" }, { "name": "symfony/http-client-contracts", - "version": "v3.5.2", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645" + "reference": "75d7043853a42837e68111812f4d964b01e5101c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ee8d807ab20fcb51267fdace50fbe3494c31e645", - "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c", + "reference": "75d7043853a42837e68111812f4d964b01e5101c", "shasum": "" }, "require": { @@ -3531,7 +3644,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -3567,7 +3680,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.2" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0" }, "funding": [ { @@ -3583,43 +3696,44 @@ "type": "tidelift" } ], - "time": "2024-12-07T08:49:48+00:00" + "time": "2025-04-29T11:18:49+00:00" }, { "name": "symfony/mime", - "version": "v7.2.4", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "87ca22046b78c3feaff04b337f33b38510fd686b" + "reference": "b18c7e6e9eee1e19958138df10412f3c4c316148" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/87ca22046b78c3feaff04b337f33b38510fd686b", - "reference": "87ca22046b78c3feaff04b337f33b38510fd686b", + "url": "https://api.github.com/repos/symfony/mime/zipball/b18c7e6e9eee1e19958138df10412f3c4c316148", + "reference": "b18c7e6e9eee1e19958138df10412f3c4c316148", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "egulias/email-validator": "~3.0.0", - "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0", + "phpdocumentor/reflection-docblock": "<5.2|>=6", + "phpdocumentor/type-resolver": "<1.5.1", "symfony/mailer": "<6.4", "symfony/serializer": "<6.4.3|>7.0,<7.0.3" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3.1|^4", "league/html-to-markdown": "^5.0", - "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/serializer": "^6.4.3|^7.0.3" + "phpdocumentor/reflection-docblock": "^5.2", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0" }, "type": "library", "autoload": { @@ -3651,7 +3765,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.2.4" + "source": "https://github.com/symfony/mime/tree/v7.4.5" }, "funding": [ { @@ -3662,16 +3776,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-19T08:51:20+00:00" + "time": "2026-01-27T08:59:58+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -3730,7 +3848,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" }, "funding": [ { @@ -3741,6 +3859,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -3750,16 +3872,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", "shasum": "" }, "require": { @@ -3808,7 +3930,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" }, "funding": [ { @@ -3819,25 +3941,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-06-27T09:58:17+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773" + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773", - "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", "shasum": "" }, "require": { @@ -3891,7 +4017,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" }, "funding": [ { @@ -3902,16 +4028,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-09-10T14:38:51+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -3972,7 +4102,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" }, "funding": [ { @@ -3983,6 +4113,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -3992,19 +4126,20 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -4052,7 +4187,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -4063,25 +4198,109 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-08T02:45:35+00:00" }, { "name": "symfony/process", - "version": "v7.2.4", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf" + "reference": "608476f4604102976d687c483ac63a79ba18cc97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf", - "reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf", + "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97", + "reference": "608476f4604102976d687c483ac63a79ba18cc97", "shasum": "" }, "require": { @@ -4113,7 +4332,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.2.4" + "source": "https://github.com/symfony/process/tree/v7.4.5" }, "funding": [ { @@ -4124,25 +4343,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-05T08:33:46+00:00" + "time": "2026-01-26T15:07:59+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.5.1", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { @@ -4160,7 +4383,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -4196,7 +4419,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -4207,31 +4430,36 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { "name": "symfony/string", - "version": "v7.2.0", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" + "reference": "1c4b10461bf2ec27537b5f36105337262f5f5d6f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", + "url": "https://api.github.com/repos/symfony/string/zipball/1c4b10461bf2ec27537b5f36105337262f5f5d6f", + "reference": "1c4b10461bf2ec27537b5f36105337262f5f5d6f", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-grapheme": "~1.33", "symfony/polyfill-intl-normalizer": "~1.0", "symfony/polyfill-mbstring": "~1.0" }, @@ -4239,12 +4467,11 @@ "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.1", - "symfony/error-handler": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4283,7 +4510,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.2.0" + "source": "https://github.com/symfony/string/tree/v7.4.4" }, "funding": [ { @@ -4294,34 +4521,39 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-11-13T13:31:26+00:00" + "time": "2026-01-12T10:54:30+00:00" }, { "name": "symfony/translation", - "version": "v7.2.4", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "283856e6981286cc0d800b53bd5703e8e363f05a" + "reference": "bfde13711f53f549e73b06d27b35a55207528877" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/283856e6981286cc0d800b53bd5703e8e363f05a", - "reference": "283856e6981286cc0d800b53bd5703e8e363f05a", + "url": "https://api.github.com/repos/symfony/translation/zipball/bfde13711f53f549e73b06d27b35a55207528877", + "reference": "bfde13711f53f549e73b06d27b35a55207528877", "shasum": "" }, "require": { "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/translation-contracts": "^2.5|^3.0" + "symfony/translation-contracts": "^2.5.3|^3.3" }, "conflict": { + "nikic/php-parser": "<5.0", "symfony/config": "<6.4", "symfony/console": "<6.4", "symfony/dependency-injection": "<6.4", @@ -4335,19 +4567,19 @@ "symfony/translation-implementation": "2.3|3.0" }, "require-dev": { - "nikic/php-parser": "^4.18|^5.0", + "nikic/php-parser": "^5.0", "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", "symfony/http-client-contracts": "^2.5|^3.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", "symfony/polyfill-intl-icu": "^1.21", - "symfony/routing": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^6.4|^7.0" + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4378,7 +4610,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.2.4" + "source": "https://github.com/symfony/translation/tree/v7.4.4" }, "funding": [ { @@ -4389,25 +4621,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-13T10:27:23+00:00" + "time": "2026-01-13T10:40:19+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.5.1", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "4667ff3bd513750603a09c8dedbea942487fb07c" + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/4667ff3bd513750603a09c8dedbea942487fb07c", - "reference": "4667ff3bd513750603a09c8dedbea942487fb07c", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", "shasum": "" }, "require": { @@ -4420,7 +4656,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -4456,7 +4692,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" }, "funding": [ { @@ -4467,34 +4703,39 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-07-15T13:41:35+00:00" }, { "name": "symfony/var-exporter", - "version": "v7.2.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "4ede73aa7a73d81506002d2caadbbdad1ef5b69a" + "reference": "03a60f169c79a28513a78c967316fbc8bf17816f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/4ede73aa7a73d81506002d2caadbbdad1ef5b69a", - "reference": "4ede73aa7a73d81506002d2caadbbdad1ef5b69a", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/03a60f169c79a28513a78c967316fbc8bf17816f", + "reference": "03a60f169c79a28513a78c967316fbc8bf17816f", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" }, "require-dev": { - "symfony/property-access": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4532,7 +4773,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.2.4" + "source": "https://github.com/symfony/var-exporter/tree/v7.4.0" }, "funding": [ { @@ -4543,37 +4784,41 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-13T10:27:23+00:00" + "time": "2025-09-11T10:15:23+00:00" }, { "name": "symfony/yaml", - "version": "v7.2.3", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "ac238f173df0c9c1120f862d0f599e17535a87ec" + "reference": "24dd4de28d2e3988b311751ac49e684d783e2345" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/ac238f173df0c9c1120f862d0f599e17535a87ec", - "reference": "ac238f173df0c9c1120f862d0f599e17535a87ec", + "url": "https://api.github.com/repos/symfony/yaml/zipball/24dd4de28d2e3988b311751ac49e684d783e2345", + "reference": "24dd4de28d2e3988b311751ac49e684d783e2345", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8" }, "conflict": { "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -4604,7 +4849,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.2.3" + "source": "https://github.com/symfony/yaml/tree/v7.4.1" }, "funding": [ { @@ -4615,25 +4860,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-01-07T12:55:42+00:00" + "time": "2025-12-04T18:11:45+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -4662,7 +4911,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -4670,7 +4919,7 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-11-17T20:03:58+00:00" } ], "aliases": [], @@ -4701,5 +4950,5 @@ "ext-sodium": "*" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/config-dist.php b/config-dist.php index 7a33f44748c9c..4bf70da9b3270 100644 --- a/config-dist.php +++ b/config-dist.php @@ -175,13 +175,6 @@ $CFG->wwwroot = 'http://example.com/moodle'; -// Generally it is not advisable to use a wwwroot that ends in 'public'. -// This is because the 'public' directory is used to serve web-accessible content. -// Moodle looks for any URL which ends in 'public' and assumes that it is a misconfiguration. -// In the event that there is a need to have a wwwroot that ends in 'public', the -// following setting can be used to override this check. -$CFG->wwwrootendsinpublic = false; - //========================================================================= // 3. DATA FILES LOCATION //========================================================================= diff --git a/index.php b/index.php index 9dff3039f0b40..ba0bbb120e3ad 100644 --- a/index.php +++ b/index.php @@ -15,13 +15,12 @@ // along with Moodle. If not, see . /** - * This file acts as a redirector to the public directory. - * - * Note: This file is not intended to be accessed directly. + * Throw an exception when users try to access index.php outside of the web root. * * @package core * @copyright Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -@header($_SERVER['SERVER_PROTOCOL'] . ' 308 Permanent Redirect'); -@header('Location: ./public/'); +require_once('config.php'); + +throw new \core\exception\moodle_exception('rootdirpublic', 'error'); diff --git a/public/admin/category.php b/public/admin/category.php index 803367bc772a7..0bdfbe0c11de5 100644 --- a/public/admin/category.php +++ b/public/admin/category.php @@ -119,8 +119,8 @@ $outputhtml .= html_writer::tag('div', '', array('class' => 'clearer')); $outputhtml .= $setting->output_html($data); if ($childpage->has_dependencies()) { - $opts = ['dependencies' => $childpage->get_dependencies_for_javascript()]; - $PAGE->requires->js_call_amd('core/showhidesettings', 'init', [$opts]); + $context = ['dependencies' => json_encode($childpage->get_dependencies_for_javascript())]; + echo $OUTPUT->render_from_template('core_admin/settings_showhide', $context); } } $outputhtml .= html_writer::end_tag('fieldset'); diff --git a/public/admin/classes/admin/admin_setting_notification.php b/public/admin/classes/admin/admin_setting_notification.php new file mode 100644 index 0000000000000..de4b21bf8df59 --- /dev/null +++ b/public/admin/classes/admin/admin_setting_notification.php @@ -0,0 +1,74 @@ +. + +namespace core_admin\admin; + +use admin_setting; + +/** + * Render a notification as part of other admin settings. + * + * @package core_admin + * @subpackage admin + * @copyright 2025 Matt Porritt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class admin_setting_notification extends admin_setting { + /** + * Constructor. + * + * @param string $name The name of the setting. + * @param string $notification The notification to display. + * @param string $type The type of notification. + * @param bool $cancelable Whether the notification can be canceled. + */ + public function __construct( + string $name, + /** @var string The notification to display. */ + protected string $notification, + /** @var string The type of notification. */ + protected string $type = 'info', + /** @var bool Whether the notification can be canceled. */ + protected bool $cancelable = false + ) { + $this->nosave = true; + + parent::__construct($name, '', '', ''); + } + + #[\Override] + public function get_setting(): bool { + return true; + } + + #[\Override] + public function get_defaultsetting(): bool { + return true; + } + + #[\Override] + public function write_setting($data): string { + // Do not write any setting. + return ''; + } + + #[\Override] + public function output_html($data, $query = ''): string { + global $OUTPUT; + + return $OUTPUT->notification($this->notification, $this->type, $this->cancelable); + } +} diff --git a/public/admin/environment.xml b/public/admin/environment.xml index a872ea319b007..30a56c7abe5b1 100644 --- a/public/admin/environment.xml +++ b/public/admin/environment.xml @@ -3835,6 +3835,7 @@ + @@ -4411,6 +4412,7 @@ + @@ -4607,6 +4609,7 @@ + @@ -4802,6 +4805,7 @@ + @@ -4997,6 +5001,211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/admin/filters.php b/public/admin/filters.php index 7f9cc73837d20..b58795103d658 100644 --- a/public/admin/filters.php +++ b/public/admin/filters.php @@ -121,7 +121,7 @@ $table->head = [get_string('filter'), get_string('isactive', 'filters'), get_string('order'), get_string('applyto', 'filters'), get_string('settings'), get_string('uninstallplugin', 'core_admin')]; $table->colclasses = array ('leftalign', 'leftalign', 'centeralign', 'leftalign', 'leftalign', 'leftalign'); -$table->attributes['class'] = 'admintable table generaltable'; +$table->attributes['class'] = 'admintable table generaltable table-hover'; $table->id = 'filterssetting'; $table->data = []; diff --git a/public/admin/index.php b/public/admin/index.php index d9a3a88125dc6..01eba225576f5 100644 --- a/public/admin/index.php +++ b/public/admin/index.php @@ -969,8 +969,14 @@ // Check if the site is being foced onto ssl. $overridetossl = !empty($CFG->overridetossl); -// Check if moodle campaign content setting is enabled or not. -$showcampaigncontent = !isset($CFG->showcampaigncontent) || $CFG->showcampaigncontent; +if (defined('BEHAT_SITE_RUNNING') && BEHAT_SITE_RUNNING) { + // We need to add this in order to be able to verify the showcampaigncontent setting behaviour during behat tests. + $showcampaigncontent = get_config('core', 'showcampaigncontent'); + $showcampaigncontent = $showcampaigncontent === 'true'; +} else { + // Check if moodle campaign content setting is enabled or not. + $showcampaigncontent = !isset($CFG->showcampaigncontent) || $CFG->showcampaigncontent; +} // Encourage admins to enable the user feedback feature if it is not enabled already. $showfeedbackencouragement = empty($CFG->enableuserfeedback); diff --git a/public/admin/mnet/index.php b/public/admin/mnet/index.php index 330010351e808..42d16662d70da 100644 --- a/public/admin/mnet/index.php +++ b/public/admin/mnet/index.php @@ -98,7 +98,7 @@ echo $OUTPUT->header(); ?>
- +
@@ -119,7 +119,7 @@
- +
diff --git a/public/admin/plagiarism.php b/public/admin/plagiarism.php index 7df18fa34cdf3..f19526d697422 100644 --- a/public/admin/plagiarism.php +++ b/public/admin/plagiarism.php @@ -54,7 +54,7 @@ $table->head = array($txt->name, $txt->version, $txt->uninstall, $txt->settings); $table->colclasses = array('mdl-left', 'mdl-align', 'mdl-align', 'mdl-align'); $table->data = array(); -$table->attributes['class'] = 'manageplagiarismtable table generaltable'; +$table->attributes['class'] = 'manageplagiarismtable table generaltable table-hover'; // Iterate through auth plugins and add to the display table. $authcount = count($plagiarismplugins); diff --git a/public/admin/renderer.php b/public/admin/renderer.php index 4f789e4394f8f..71437d5c9b757 100644 --- a/public/admin/renderer.php +++ b/public/admin/renderer.php @@ -282,13 +282,30 @@ public function upgrade_confirm_abort_install_page(array $abortable, moodle_url * * @return string HTML to output. */ - public function admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed, - $cronoverdue, $dbproblems, $maintenancemode, $availableupdates, $availableupdatesfetch, - $buggyiconvnomb, $registered, array $cachewarnings = array(), $eventshandlers = 0, - $themedesignermode = false, $devlibdir = false, $mobileconfigured = false, - $overridetossl = false, $invalidforgottenpasswordurl = false, $croninfrequent = false, - $showcampaigncontent = false, bool $showfeedbackencouragement = false, bool $showservicesandsupport = false, - $xmlrpcwarning = '') { + public function admin_notifications_page( + $maturity, + $insecuredataroot, + $errorsdisplayed, + $cronoverdue, + $dbproblems, + $maintenancemode, + $availableupdates, + $availableupdatesfetch, + $buggyiconvnomb, + $registered, + array $cachewarnings = [], + $eventshandlers = 0, + $themedesignermode = false, + $devlibdir = false, + $mobileconfigured = false, + $overridetossl = false, + $invalidforgottenpasswordurl = false, + $croninfrequent = false, + $showcampaigncontent = false, + bool $showfeedbackencouragement = false, + bool $showservicesandsupport = false, + $xmlrpcwarning = '' + ) { global $CFG; $output = ''; @@ -313,6 +330,7 @@ public function admin_notifications_page($maturity, $insecuredataroot, $errorsdi $output .= $this->mobile_configuration_warning($mobileconfigured); $output .= $this->forgotten_password_url_warning($invalidforgottenpasswordurl); $output .= $this->mnet_deprecation_warning($xmlrpcwarning); + $output .= $this->moodlenet_removal_warning(); $output .= $this->userfeedback_encouragement($showfeedbackencouragement); $output .= $this->services_and_support_content($showservicesandsupport); $output .= $this->campaign_content($showcampaigncontent); @@ -1809,6 +1827,7 @@ public function plugins_control_panel(core_plugin_manager $pluginman, array $opt $table = new html_table(); $table->id = 'plugins-control-panel'; + $table->attributes['class'] = 'generaltable table table-striped table-hover'; $table->head = array( get_string('displayname', 'core_plugin'), get_string('version', 'core_plugin'), @@ -2063,7 +2082,7 @@ public function environment_check_table($result, $environment_results) { get_string('status'), ); $servertable->colclasses = array('centeralign name', 'centeralign info', 'leftalign report', 'leftalign plugin', 'centeralign status'); - $servertable->attributes['class'] = 'table table-striped admintable environmenttable generaltable table-sm'; + $servertable->attributes['class'] = 'table table-striped admintable environmenttable generaltable table-sm table-hover'; $servertable->id = 'serverstatus'; $serverdata = array('ok'=>array(), 'warn'=>array(), 'error'=>array()); @@ -2076,7 +2095,7 @@ public function environment_check_table($result, $environment_results) { get_string('status'), ); $othertable->colclasses = array('aligncenter info', 'alignleft report', 'alignleft plugin', 'aligncenter status'); - $othertable->attributes['class'] = 'table table-striped admintable environmenttable generaltable table-sm'; + $othertable->attributes['class'] = 'table table-striped admintable environmenttable generaltable table-sm table-hover'; $othertable->id = 'otherserverstatus'; $otherdata = array('ok'=>array(), 'warn'=>array(), 'error'=>array()); @@ -2325,6 +2344,21 @@ protected function mnet_deprecation_warning($xmlrpcwarning) { return $this->warning($xmlrpcwarning); } + /** + * Display a warning about the removal of MoodleNet integration. + * + * @return string HTML to output. + */ + protected function moodlenet_removal_warning(): string { + $moodlenetenabled = get_config('tool_moodlenet', 'enablemoodlenet'); + if (!empty($moodlenetenabled)) { + $moodlenetwarning = get_string('moodlenetremovalwarning', 'admin'); + return $this->warning($moodlenetwarning); + } + + return ''; + } + /** * Renders the theme selector list. * diff --git a/public/admin/repository.php b/public/admin/repository.php index 280856997228c..bca4d56219f92 100644 --- a/public/admin/repository.php +++ b/public/admin/repository.php @@ -263,7 +263,7 @@ $table->colclasses = array('leftalign', 'centeralign', 'centeralign', 'centeralign', 'centeralign', 'centeralign'); $table->id = 'repositoriessetting'; $table->data = array(); - $table->attributes['class'] = 'admintable table generaltable'; + $table->attributes['class'] = 'admintable table generaltable table-hover'; // Get list of used plug-ins $repositorytypes = repository::get_types(); diff --git a/public/admin/roles/admins.php b/public/admin/roles/admins.php index f715fa549965b..2f40b9cbf2fd0 100644 --- a/public/admin/roles/admins.php +++ b/public/admin/roles/admins.php @@ -179,7 +179,7 @@
-
+

diff --git a/public/admin/roles/assign.php b/public/admin/roles/assign.php index 492e4440d8284..90431f5a269c2 100644 --- a/public/admin/roles/assign.php +++ b/public/admin/roles/assign.php @@ -228,7 +228,7 @@

- +

@@ -322,7 +322,7 @@ $table->id = 'assignrole'; $table->head = array(get_string('role'), get_string('description'), get_string('userswiththisrole', 'core_role')); $table->colclasses = array('leftalign role', 'leftalign', 'centeralign userrole'); - $table->attributes['class'] = 'admintable table generaltable'; + $table->attributes['class'] = 'admintable table generaltable table-hover'; if ($showroleholders) { $table->headspan = array(1, 1, 2); $table->colclasses[] = 'leftalign roleholder'; diff --git a/public/admin/roles/manage.php b/public/admin/roles/manage.php index 8b125a1007b2e..69a74602fd2a9 100644 --- a/public/admin/roles/manage.php +++ b/public/admin/roles/manage.php @@ -153,7 +153,7 @@ $table = new html_table(); $table->colclasses = array('leftalign', 'leftalign', 'leftalign', 'leftalign'); $table->id = 'roles'; -$table->attributes['class'] = 'admintable table generaltable'; +$table->attributes['class'] = 'admintable table generaltable table-hover'; $table->head = array( get_string('role') . ' ' . $OUTPUT->help_icon('roles', 'core_role'), get_string('description'), diff --git a/public/admin/settings.php b/public/admin/settings.php index 48171745b3a14..25ba9a556acab 100644 --- a/public/admin/settings.php +++ b/public/admin/settings.php @@ -165,10 +165,8 @@ $PAGE->requires->js_call_amd('core_form/changechecker', 'watchFormById', ['adminsettings']); if ($settingspage->has_dependencies()) { - $opts = [ - 'dependencies' => $settingspage->get_dependencies_for_javascript() - ]; - $PAGE->requires->js_call_amd('core/showhidesettings', 'init', [$opts]); + $context = ['dependencies' => json_encode($settingspage->get_dependencies_for_javascript())]; + echo $OUTPUT->render_from_template('core_admin/settings_showhide', $context); } echo $OUTPUT->footer(); diff --git a/public/admin/settings/ai.php b/public/admin/settings/ai.php index ecb00c8c09328..5a27bab52de83 100644 --- a/public/admin/settings/ai.php +++ b/public/admin/settings/ai.php @@ -32,19 +32,29 @@ $providers->add(new admin_setting_heading('availableproviders', get_string('availableproviders', 'core_ai'), get_string('availableproviders_desc', 'core_ai'))); - // Add call to action to add a new provider. - $providers->add(new \core_admin\admin\admin_setting_template_render( - name: 'addnewprovider', - templatename: 'core_ai/admin_add_provider', - context: ['addnewproviderurl' => new moodle_url('/ai/configure.php')] - )); - $providers->add(new \core_ai\admin\admin_setting_provider_manager( + if (!empty(core_plugin_manager::instance()->get_plugins_of_type("aiprovider"))) { + // Add call to action to add a new provider. + $providers->add(new \core_admin\admin\admin_setting_template_render( + name: 'addnewprovider', + templatename: 'core_ai/admin_add_provider', + context: ['addnewproviderurl' => new moodle_url('/ai/configure.php')] + )); + + $providers->add(new \core_ai\admin\admin_setting_provider_manager( 'aiprovider', \core_ai\table\aiprovider_management_table::class, 'manageaiproviders', new lang_string('manageaiproviders', 'core_ai'), - )); + )); + } else { + $providers->add(new \core_admin\admin\admin_setting_notification( + name:'noproviderplugins', + notification: get_string('noproviderplugins', 'core_ai'), + type: 'danger' + )); + } + $ADMIN->add('ai', $providers); // Add settings page for AI placement settings. diff --git a/public/admin/templates/settings_showhide.mustache b/public/admin/templates/settings_showhide.mustache new file mode 100644 index 0000000000000..e02d5b14ad46e --- /dev/null +++ b/public/admin/templates/settings_showhide.mustache @@ -0,0 +1,51 @@ +{{! + 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/settings_showhide + + Passes the necessary data to show/hide dependant settings. + + Context variables required for this template: + * dependencies - JSON-serialized data structure describing the dependencies between settings + + Example context (json): + { + "dependencies": { + "s__setting1":{ + "eq":[ + [ + "s__setting0" + ] + ] + }, + "s__setting2":{ + "eq":[ + [ + "s__setting1" + ] + ] + } + } + } +}} + + +{{#js}} + require(['core/showhidesettings'], function(ShowHideSettings) { + ShowHideSettings.init("settings-showhide-{{uniqid}}"); + }); +{{/js}} diff --git a/public/admin/tests/behat/browse_users.feature b/public/admin/tests/behat/browse_users.feature index cdbcfbeef585e..2c7d3de127500 100644 --- a/public/admin/tests/behat/browse_users.feature +++ b/public/admin/tests/behat/browse_users.feature @@ -90,6 +90,7 @@ Feature: An administrator can browse user accounts | username | firstname | lastname | email | confirmed | | user3 | User | Three | three@example.com | 0 | And I navigate to "Users > Accounts > Browse list of users" in site administration + And I change window size to "large" Then I should see "Confirmation pending" in the "User Three" "table_row" And I press "Resend confirmation email" action in the "User Three" report row And I should see "Confirmation email sent successfully" diff --git a/public/admin/tests/behat/toggle_campaign_display.feature b/public/admin/tests/behat/toggle_campaign_display.feature new file mode 100644 index 0000000000000..c0cbb84def46a --- /dev/null +++ b/public/admin/tests/behat/toggle_campaign_display.feature @@ -0,0 +1,17 @@ +@core @core_admin +Feature: Toggle campaign banner visibility + In order to control the visibility of the campaign banner content + As an admin + I need to be able to disable the campaign banner display + + Scenario Outline: Admin can disable the campaign banner display + Given the following config values are set as admin: + | showcampaigncontent | | + And I log in as "admin" + When I navigate to "Notifications" in site administration + Then "//iframe[@id='campaign-content']" "xpath_element" exist + + Examples: + | showcampaigncontent | display | + | true | should | + | false | should not | diff --git a/public/admin/thirdpartylibs.php b/public/admin/thirdpartylibs.php index 42bac41763377..9a35f25705055 100644 --- a/public/admin/thirdpartylibs.php +++ b/public/admin/thirdpartylibs.php @@ -47,7 +47,7 @@ get_string('thirdpartylibrarylocation', 'core_admin'), get_string('license')); $table->align = array('left', 'left', 'left', 'left'); $table->id = 'thirdpartylibs'; -$table->attributes['class'] = 'admintable table generaltable'; +$table->attributes['class'] = 'admintable table generaltable table-hover'; $table->data = array(); foreach ($files as $component => $xmlpath) { diff --git a/public/admin/tool/behat/cli/init.php b/public/admin/tool/behat/cli/init.php index a0f4f2cf75eed..d5d72776a1449 100644 --- a/public/admin/tool/behat/cli/init.php +++ b/public/admin/tool/behat/cli/init.php @@ -109,7 +109,7 @@ -h, --help Print out this help Example from Moodle root directory: -\$ php admin/tool/behat/cli/init.php --parallel=2 +\$ php public/admin/tool/behat/cli/init.php --parallel=2 More info in https://moodledev.io/general/development/tools/behat/running "; diff --git a/public/admin/tool/behat/cli/run.php b/public/admin/tool/behat/cli/run.php index eaa5bb953f41a..b393f2102a9c1 100644 --- a/public/admin/tool/behat/cli/run.php +++ b/public/admin/tool/behat/cli/run.php @@ -89,7 +89,7 @@ -h, --help Print out this help Example from Moodle root directory: -\$ php admin/tool/behat/cli/run.php --tags=\"@javascript\" +\$ php public/admin/tool/behat/cli/run.php --tags=\"@javascript\" More info in https://moodledev.io/general/development/tools/behat/running "; @@ -224,6 +224,11 @@ $cmds['singlerun'] = $runtestscommand; echo "Running single behat site:" . PHP_EOL; + // The inner PHP process is not marked as having an interactive terminal even if it's passed + // through from this one which does, so we need to pass it through as an environment variable. + if (function_exists('posix_isatty') && posix_isatty(STDOUT)) { + putenv('MOODLE_BEHAT_RUNNING_IN_TTY=1'); + } passthru("php $runtestscommand", $status); $exitcodes['singlerun'] = $status; chdir($cwd); diff --git a/public/admin/tool/behat/cli/util.php b/public/admin/tool/behat/cli/util.php index 4fbf65cf939e0..987a1489d0263 100644 --- a/public/admin/tool/behat/cli/util.php +++ b/public/admin/tool/behat/cli/util.php @@ -102,7 +102,7 @@ -h, --help Print out this help Example from Moodle root directory: -\$ php admin/tool/behat/cli/util.php --enable --parallel=4 +\$ php public/admin/tool/behat/cli/util.php --enable --parallel=4 More info in https://moodledev.io/general/development/tools/behat/running "; diff --git a/public/admin/tool/behat/cli/util_single_run.php b/public/admin/tool/behat/cli/util_single_run.php index d7a0479f991dc..79f3782f27814 100644 --- a/public/admin/tool/behat/cli/util_single_run.php +++ b/public/admin/tool/behat/cli/util_single_run.php @@ -95,7 +95,7 @@ -h, --help Print out this help Example from Moodle root directory: -\$ php admin/tool/behat/cli/util_single_run.php --enable +\$ php public/admin/tool/behat/cli/util_single_run.php --enable More info in https://moodledev.io/general/development/tools/behat/running "; diff --git a/public/admin/tool/capability/index.php b/public/admin/tool/capability/index.php index a35b344e7d4c2..7a3660130b915 100644 --- a/public/admin/tool/capability/index.php +++ b/public/admin/tool/capability/index.php @@ -140,7 +140,7 @@ function print_report_tree($contextid, $contexts, $allroles) { // If there are any role overrides here, print them. if (!empty($contexts[$contextid]->rolecapabilities)) { $rowcounter = 0; - echo ''; + echo '
'; foreach ($allroles as $role) { if (isset($contexts[$contextid]->rolecapabilities[$role->id])) { $permission = $contexts[$contextid]->rolecapabilities[$role->id]; diff --git a/public/admin/tool/cohortroles/classes/privacy/provider.php b/public/admin/tool/cohortroles/classes/privacy/provider.php index 9896a620610af..e55794c755520 100644 --- a/public/admin/tool/cohortroles/classes/privacy/provider.php +++ b/public/admin/tool/cohortroles/classes/privacy/provider.php @@ -166,10 +166,12 @@ public static function export_user_data(approved_contextlist $contextlist) { // Retrieve the tool_cohortroles records created for the user. $sql = "SELECT cr.id as cohortroleid, + cr.cohortid, c.name as cohortname, c.idnumber as cohortidnumber, - c.description as cohortdescription, - c.contextid as contextid, + c.description, + c.descriptionformat, + c.contextid, r.shortname as roleshortname, cr.userid as userid, cr.timecreated as timecreated, @@ -185,27 +187,39 @@ public static function export_user_data(approved_contextlist $contextlist) { $cohortroles = $DB->get_records_sql($sql, $params); foreach ($cohortroles as $cohortrole) { + $context = \context::instance_by_id($cohortrole->contextid); + // The tool_cohortroles data export is organised in: // {User Context}/Cohort roles management/{cohort name}/{role shortname}/data.json. $subcontext = [ get_string('pluginname', 'tool_cohortroles'), - $cohortrole->cohortname, + format_string($cohortrole->cohortname, options: ['context' => $context]), $cohortrole->roleshortname ]; $data = (object) [ - 'cohortname' => $cohortrole->cohortname, + 'cohortname' => format_string($cohortrole->cohortname, options: ['context' => $context]), 'cohortidnumber' => $cohortrole->cohortidnumber, - 'cohortdescription' => $cohortrole->cohortdescription, + 'cohortdescription' => format_text( + writer::with_context($context)->rewrite_pluginfile_urls( + $subcontext, + 'cohort', + 'description', + $cohortrole->cohortid, + $cohortrole->description, + ), + $cohortrole->descriptionformat, + ['context' => $context], + ), 'roleshortname' => $cohortrole->roleshortname, 'userid' => transform::user($cohortrole->userid), 'timecreated' => transform::datetime($cohortrole->timecreated), 'timemodified' => transform::datetime($cohortrole->timemodified) ]; - $context = \context::instance_by_id($cohortrole->contextid); - - writer::with_context($context)->export_data($subcontext, $data); + writer::with_context($context) + ->export_area_files($subcontext, 'cohort', 'description', $cohortrole->cohortid) + ->export_data($subcontext, $data); } } diff --git a/public/admin/tool/cohortroles/db/upgrade.php b/public/admin/tool/cohortroles/db/upgrade.php index a06e6855d93c8..93ab547e8a906 100644 --- a/public/admin/tool/cohortroles/db/upgrade.php +++ b/public/admin/tool/cohortroles/db/upgrade.php @@ -54,5 +54,8 @@ function xmldb_tool_cohortroles_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/customlang/db/upgrade.php b/public/admin/tool/customlang/db/upgrade.php index 44cf4ac15c048..465980a1bfd55 100644 --- a/public/admin/tool/customlang/db/upgrade.php +++ b/public/admin/tool/customlang/db/upgrade.php @@ -39,5 +39,8 @@ function xmldb_tool_customlang_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/dataprivacy/classes/output/data_requests_table.php b/public/admin/tool/dataprivacy/classes/output/data_requests_table.php index 0ea44f4738d63..d3dce96401f42 100644 --- a/public/admin/tool/dataprivacy/classes/output/data_requests_table.php +++ b/public/admin/tool/dataprivacy/classes/output/data_requests_table.php @@ -162,7 +162,7 @@ public function col_type($data) { */ public function col_userid($data) { $user = $data->foruser; - return html_writer::link($user->profileurl, $user->fullname, ['title' => get_string('viewprofile')]); + return html_writer::link($user->profileurl, $user->fullname); } /** @@ -183,7 +183,7 @@ public function col_timecreated($data) { */ public function col_requestedby($data) { $user = $data->requestedbyuser; - return html_writer::link($user->profileurl, $user->fullname, ['title' => get_string('viewprofile')]); + return html_writer::link($user->profileurl, $user->fullname); } /** diff --git a/public/admin/tool/dataprivacy/db/upgrade.php b/public/admin/tool/dataprivacy/db/upgrade.php index e92a5ebeb7c5d..83fb04f91cf76 100644 --- a/public/admin/tool/dataprivacy/db/upgrade.php +++ b/public/admin/tool/dataprivacy/db/upgrade.php @@ -109,5 +109,8 @@ function xmldb_tool_dataprivacy_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/dataprivacy/templates/my_data_requests.mustache b/public/admin/tool/dataprivacy/templates/my_data_requests.mustache index 0d1e5f35ef24d..6664eba2737da 100644 --- a/public/admin/tool/dataprivacy/templates/my_data_requests.mustache +++ b/public/admin/tool/dataprivacy/templates/my_data_requests.mustache @@ -138,7 +138,7 @@ }}> - + diff --git a/public/admin/tool/dataprivacy/templates/request_details.mustache b/public/admin/tool/dataprivacy/templates/request_details.mustache index f4709bca52a5e..36f939ea58da1 100644 --- a/public/admin/tool/dataprivacy/templates/request_details.mustache +++ b/public/admin/tool/dataprivacy/templates/request_details.mustache @@ -62,7 +62,7 @@

- {{foruser.fullname}} + {{foruser.fullname}}

{{foruser.email}}
@@ -76,7 +76,7 @@ {{#str}}requestbydetail, tool_dataprivacy{{/str}} - {{requestedbyuser.fullname}} + {{requestedbyuser.fullname}}
{{#canreview}} diff --git a/public/admin/tool/dataprivacy/tests/behat/user_data_request.feature b/public/admin/tool/dataprivacy/tests/behat/user_data_request.feature new file mode 100644 index 0000000000000..a39e34a21816d --- /dev/null +++ b/public/admin/tool/dataprivacy/tests/behat/user_data_request.feature @@ -0,0 +1,100 @@ +@tool @tool_dataprivacy +Feature: Authorized users can request others personal data + In order to export or access another users data + As a designated role + I need the correct permissions + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | user1 | User1 | One | user1@example.com | + | user2 | User2 | Two | user2@example.com | + | officer1 | Officer1 | One | officer1@example.com | + # Create Privacy Officer Role. + And the following "role" exists: + | shortname | privacyofficer | + | name | Privacy Officer | + | context_system | 1 | + | tool/dataprivacy:managedataregistry | allow | + | tool/dataprivacy:managedatarequests | allow | + | tool/dataprivacy:makedatarequestsforchildren | allow | + | moodle/site:configview | allow | + | moodle/category:viewhiddencategories | allow | + | moodle/course:viewhiddencourses | allow | + | moodle/course:viewhiddenactivities | allow | + | moodle/course:view | allow | + # Create Parent Role. + And the following "role" exists: + | shortname | parentrole | + | name | Parent Role | + | context_user | 1 | + | moodle/user:viewdetails | allow | + | moodle/user:viewalldetails | allow | + | moodle/user:readuserblogs | allow | + | moodle/user:readuserposts | allow | + | moodle/user:viewuseractivitiesreport | allow | + | moodle/user:editprofile | allow | + | tool/policy:acceptbehalf | allow | + | tool/dataprivacy:makedatarequestsforchildren | allow | + # Add permission to allow parent to make requests on behalf of child user. + And the following config values are set as admin: + | contactdataprotectionofficer | 1 | tool_dataprivacy | + And I log in as "admin" + + @javascript + Scenario: Privacy officer can request for other user's personal data + Given I navigate to "Users > Permissions > Assign system roles" in site administration + # Assign Privacy Officer role to officer1. + And I follow "Privacy Officer" + And I set the field "addselect_searchtext" to "Officer1" + And I set the field "addselect" to "Officer1 One (officer1@example.com)" + And I press "Add" + # Navigate to home in order to navigate properly to Privacy settings. + And I am on site homepage + # Select Privacy officer in the Orivacy officer role mapping setting. + And I navigate to "Users > Privacy and policies > Privacy settings" in site administration + And I click on "Privacy Officer" "checkbox" + And I press "Save changes" + And I log in as "officer1" + And I navigate to "Users > Privacy and policies > Data requests" in site administration + # Create a new request as the designated privacy officer. + When I follow "New request" + And I set the field "User" to "User1 One" + And I set the field "Comment" to "User One data" + And I press "Save changes" + # Confirm that the new data request is successfully created for selected user with status "Awaiting approval". + Then the following should exist in the "generaltable" table: + | Type | User | Requested by | Status | Message | + | Export | User1 One | Officer1 One | Awaiting approval | User One data | + + @javascript + Scenario: Parent user can request data on behalf of child user + Given I navigate to "Users > Accounts > Browse list of users" in site administration + And I follow "User1 One" + And I click on "Preferences" "link" in the ".profile_tree" "css_element" + # Assign user2 as parent for user1. + And I follow "Assign roles relative to this user" + And I follow "Parent" + And I set the field "Potential users" to "User2 Two (user2@example.com)" + And I click on "Add" "button" in the "#page-content" "css_element" + And I log in as "user2" + And I follow "Profile" in the user menu + And I follow "Data requests" + # As parent, create a data request for a child user. + And I follow "New request" + And I click on "User" "field" + When I type "User1 One" + # Confirm that only the parent's child users can be searched and selected. + Then I should see "User1 One" + And I type "User2 Two" + And I should see "No suggestions" + And I type "Officer1 One" + And I should see "No suggestions" + And I set the field "Search" to "User1" + And I set the field "Comment" to "This is a comment" + And I press "Save changes" + # Confirm that data request was successfully made by parent on behalf of child user. + And I should see "Your request has been submitted to the privacy officer" + And the following should exist in the "generaltable" table: + | Type | Requested by | Status | Message | + | Export all of my personal data (User1 One) | User2 Two | Awaiting approval | This is a comment | diff --git a/public/admin/tool/generator/cli/runtestscenario.php b/public/admin/tool/generator/cli/runtestscenario.php index 3e22eec1f6145..a1c654a9d4520 100644 --- a/public/admin/tool/generator/cli/runtestscenario.php +++ b/public/admin/tool/generator/cli/runtestscenario.php @@ -132,7 +132,7 @@ $runner->init(); } catch (Exception $e) { echo "Something is wrong with the behat setup.\n"; - echo " Please,try running \"php admin/tool/behat/cli/init.php\" from your Moodle root directory.\n"; + echo " Please,try running \"php public/admin/tool/behat/cli/init.php\" from your Moodle root directory.\n"; exit(0); } diff --git a/public/admin/tool/licensemanager/classes/output/table.php b/public/admin/tool/licensemanager/classes/output/table.php index 51b7e66d9f27e..65c53386ee9e9 100644 --- a/public/admin/tool/licensemanager/classes/output/table.php +++ b/public/admin/tool/licensemanager/classes/output/table.php @@ -80,7 +80,7 @@ public function create_license_manager_table(array $licenses, \renderer_base $ou 'text-center', ]; $table->id = 'manage-licenses'; - $table->attributes['class'] = 'admintable table generaltable'; + $table->attributes['class'] = 'admintable table generaltable table-hover'; $table->data = []; $rownumber = 0; diff --git a/public/admin/tool/log/classes/setting_managestores.php b/public/admin/tool/log/classes/setting_managestores.php index d51b8f1678500..2575cb9e28872 100644 --- a/public/admin/tool/log/classes/setting_managestores.php +++ b/public/admin/tool/log/classes/setting_managestores.php @@ -141,7 +141,7 @@ public function output_html($data, $query = '') { $table->colclasses = array('leftalign', 'centeralign', 'centeralign', 'centeralign', 'centeralign', 'centeralign', 'centeralign'); $table->id = 'logstoreplugins'; - $table->attributes['class'] = 'admintable table generaltable'; + $table->attributes['class'] = 'admintable table generaltable table-hover'; $table->data = array(); // Iterate through store plugins and add to the display table. diff --git a/public/admin/tool/log/db/upgrade.php b/public/admin/tool/log/db/upgrade.php index 7a4ef369a5a78..c300988e9b854 100644 --- a/public/admin/tool/log/db/upgrade.php +++ b/public/admin/tool/log/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_tool_log_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/log/store/database/db/upgrade.php b/public/admin/tool/log/store/database/db/upgrade.php index 8490b8d87c7bd..5ab4f80d958a7 100644 --- a/public/admin/tool/log/store/database/db/upgrade.php +++ b/public/admin/tool/log/store/database/db/upgrade.php @@ -38,5 +38,8 @@ function xmldb_logstore_database_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/log/store/standard/db/upgrade.php b/public/admin/tool/log/store/standard/db/upgrade.php index ba4a84ba32617..32a2a2309d2a1 100644 --- a/public/admin/tool/log/store/standard/db/upgrade.php +++ b/public/admin/tool/log/store/standard/db/upgrade.php @@ -38,5 +38,8 @@ function xmldb_logstore_standard_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/lp/templates/user_summary.mustache b/public/admin/tool/lp/templates/user_summary.mustache index 598f23f9530e9..32024031f7e09 100644 --- a/public/admin/tool/lp/templates/user_summary.mustache +++ b/public/admin/tool/lp/templates/user_summary.mustache @@ -49,7 +49,7 @@ } }} - + {{fullname}} {{#hasidentity}} diff --git a/public/admin/tool/lp/tests/behat/competency_crud.feature b/public/admin/tool/lp/tests/behat/competency_crud.feature new file mode 100644 index 0000000000000..002ccc211f6ed --- /dev/null +++ b/public/admin/tool/lp/tests/behat/competency_crud.feature @@ -0,0 +1,77 @@ +@tool @tool_lp @tool_lp_framework +Feature: Manage CRUD operations for competencies + In order to perform CRUD operations on competencies + As a manager + I need to be able to create, read, update and delete competencies + + Background: + Given the following "core_competency > frameworks" exist: + | shortname | idnumber | + | CF1 | CF1 | + And the following "core_competency > competencies" exist: + | shortname | competencyframework | idnumber | description | + | C1 | CF1 | C1ID | C1 description | + And I log in as "admin" + And I navigate to "Competencies > Competency frameworks" in site administration + And I click on "CF1 (CF1)" "link" + + @javascript + Scenario: Create a new competency + # Targets the `CF1` element to make `Add competency` button visible and accessible to avoid ambiguity with the other 'CF1' element on the screen. + Given I click on "//span[text()='CF1']" "xpath_element" + When I press "Add competency" + And I set the field "Name" to "C2" + And I set the field "Description" to "C2 description" + And I set the field "ID number" to "C2ID" + And I press "Save changes" + # Access the newly created competencies to ensure that correct information was registered. + Then "Competency created" "text" should exist + And "C2" "text" should appear after "C1" "text" + And I select "C2" of the competency tree + And "C2ID" "text" should exist + And "C2 description" "text" should exist + + @javascript + Scenario: Read a competency + When I select "C1" of the competency tree + # Confirm that selected competency info displayed matches registered info. + Then "C1ID" "text" should exist + And "C1 description" "text" should exist + # Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link. + And I click on "//a[@href='#' and text()='Edit']" "xpath_element" + # Similar to the previous step, to avoid ambiguity with the competency framework "Edit", target css element with data-action=edit. + And I click on "[data-action=edit]" "css_element" + # Confirm that the details displayed when accessing edit screen values match registered data. + And the field "Name" matches value "C1" + And the field "Description" matches value "C1 description" + And the field "ID number" matches value "C1ID" + + @javascript + Scenario: Update a competency + Given I select "C1" of the competency tree + # Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link. + And I click on "//a[@href='#' and text()='Edit']" "xpath_element" + # Similar to the previous step, to avoid ambiguity with the competency framework "Edit", target css element with data-action=edit. + When I click on "[data-action=edit]" "css_element" + And I set the field "Name" to "C2" + And I set the field "Description" to "C2 description" + And I set the field "ID number" to "C2ID" + And I press "Save changes" + Then "Competency updated" "text" should exist + And "C1" "text" should not exist + And "C2" "text" should exist + And I select "C2" of the competency tree + And "C2ID" "text" should exist + And "C1ID" "text" should not exist + And "C2 description" "text" should exist + And "C1 description" "text" should not exist + + @javascript + Scenario: Delete a competency + Given I select "C1" of the competency tree + # Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link. + And I click on "//a[@href='#' and text()='Edit']" "xpath_element" + When I click on "Delete" "link" + And I click on "Delete" "button" in the "Confirm" "dialogue" + # Confirm that "C1" competency was successfully deleted. + Then "C1" "text" should not exist diff --git a/public/admin/tool/lp/tests/behat/competency_management.feature b/public/admin/tool/lp/tests/behat/competency_management.feature new file mode 100644 index 0000000000000..0f89bcc15b4d0 --- /dev/null +++ b/public/admin/tool/lp/tests/behat/competency_management.feature @@ -0,0 +1,63 @@ +@tool @tool_lp @tool_lp_framework +Feature: Move and cross-reference competencies + In order to move and cross-reference competencies + As a manager + I need to be open the competency's menu items. + + Background: + Given the following "core_competency > frameworks" exist: + | shortname | idnumber | + | CF1 | CF1 | + And the following "core_competency > competencies" exist: + | shortname | competencyframework | + | C1 | CF1 | + | C2 | CF1 | + | C3 | CF1 | + | C4 | CF1 | + And I log in as "admin" + And I navigate to "Competencies > Competency frameworks" in site administration + And I click on "CF1 (CF1)" "link" + + @javascript + Scenario: Move a competency using Move up/Move down menu items + Given I select "C1" of the competency tree + # Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link. + And I click on "//a[@href='#' and text()='Edit']" "xpath_element" + When I click on "Move down" "link" + Then "C1" "text" should appear after "C2" "text" + # Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link. + And I click on "//a[@href='#' and text()='Edit']" "xpath_element" + And I click on "Move up" "link" + And "C1" "text" should appear before "C2" "text" + + @javascript + Scenario: Move a competency using Relocate menu item + Given I select "C3" of the competency tree + # Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link. + And I click on "//a[@href='#' and text()='Edit']" "xpath_element" + When I click on "Relocate" "link" + And I click on "C1" "text" in the "Move competency" "dialogue" + And I click on "Move" "button" in the "Move competency" "dialogue" + Then "C1" "text" should appear before "C3" "text" + And "C3" "text" should appear before "C2" "text" + And I select "C3" of the competency tree + # Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link. + And I click on "//a[@href='#' and text()='Edit']" "xpath_element" + # Similar to the previous step, to avoid ambiguity with the competency framework "Edit", target css element with data-action=edit. + And I click on "[data-action=edit]" "css_element" + And "C1" "text" should exist + And "No parent (top-level competency)" "text" should not exist + And I press "Cancel" + And "C1" "text" should appear before "C3" "text" + And "C3" "text" should appear before "C2" "text" + + @javascript + Scenario: Cross-reference a competency + Given I select "C1" of the competency tree + # Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link. + And I click on "//a[@href='#' and text()='Edit']" "xpath_element" + When I click on "Add cross-referenced competency" "link" + And I click on "C2" "text" in the "Competency picker" "dialogue" + And I click on "Add" "button" in the "Competency picker" "dialogue" + Then "Cross-referenced competencies:" "text" should exist + And I should see "C2 cmp2" diff --git a/public/admin/tool/lp/tests/behat/plan_workflow.feature b/public/admin/tool/lp/tests/behat/plan_workflow.feature index f9b811df6d168..0206185f25732 100644 --- a/public/admin/tool/lp/tests/behat/plan_workflow.feature +++ b/public/admin/tool/lp/tests/behat/plan_workflow.feature @@ -16,16 +16,10 @@ Feature: Manage plan workflow | usermanageownplan | User manage own plan role | user | | manageplan | Manager all plans role | manager | And the following "role capabilities" exist: - | role | moodle/competency:planmanageowndraft | moodle/competency:planmanageown | - | usermanageowndraftplan | allow | | - | usermanageownplan | allow | allow | - | manageplan | allow | allow | - And the following "role capability" exists: - | role | manageplan | - | moodle/competency:planmanage | allow | - | moodle/competency:planview | allow | - | moodle/competency:planreview | allow | - | moodle/competency:planrequestreview | allow | + | role | moodle/competency:planmanageowndraft | moodle/competency:planmanageown | moodle/competency:planmanage | moodle/competency:planview | moodle/competency:planreview | moodle/competency:planrequestreview | + | usermanageowndraftplan | allow | | | | | | + | usermanageownplan | allow | allow | | | | | + | manageplan | allow | allow | allow | allow | allow | allow | And the following "role assigns" exist: | user | role | contextlevel | reference | | user1 | usermanageowndraftplan | System | | @@ -53,8 +47,7 @@ Feature: Manage plan workflow | lp | System | 1 | my-index | content | Scenario: User can manages his own plan draft - Given I log in as "user1" - And I follow "Profile" in the user menu + Given I am on the "user1" "user > profile" page logged in as user1 When I follow "Learning plans" Then I should see "List of learning plans" And I should see "Test-Plan1" @@ -63,11 +56,9 @@ Feature: Manage plan workflow And I should see "Waiting for review" And I click on "Cancel review" of edit menu in the "Test-Plan1" row And I should see "Draft" - And I log out Scenario: User can manages his own plan - Given I log in as "user2" - And I follow "Profile" in the user menu + Given I am on the "user2" "user > profile" page logged in as user2 When I follow "Learning plans" Then I should see "List of learning plans" And I should see "Test-Plan2" @@ -82,11 +73,11 @@ Feature: Manage plan workflow And I should see "Active" And I click on "Complete this learning plan" of edit menu in the "Test-Plan2" row And I click on "Complete this learning plan" "button" in the "Confirm" "dialogue" + And I wait until the page is ready And I should see "Complete" And I click on "Reopen this learning plan" of edit menu in the "Test-Plan2" row And I click on "Reopen this learning plan" "button" in the "Confirm" "dialogue" And I should see "Active" - And I log out Scenario: Manager can see learning plan with status waiting for review Given the following "core_competency > plans" exist: @@ -96,7 +87,6 @@ Feature: Manage plan workflow When I log in as "manager1" Then I should see "Test-Plan3" And I should not see "Test-Plan4" - And I log out Scenario: Manager can start review of learning plan with status waiting for review Given the following "core_competency > plans" exist: @@ -108,7 +98,6 @@ Feature: Manage plan workflow And I should see "Test-Plan3" When I follow "Start review" Then I should see "In review" - And I log out Scenario: Manager can reject a learning plan with status in review Given the following "core_competency > plans" exist: @@ -121,7 +110,6 @@ Feature: Manage plan workflow And I should see "In review" When I follow "Finish review" Then I should see "Draft" - And I log out Scenario: Manager can accept a learning plan with status in review Given the following "core_competency > plans" exist: @@ -134,7 +122,6 @@ Feature: Manage plan workflow And I should see "In review" When I follow "Make active" Then I should see "Active" - And I log out Scenario: Manager send back to draft an active learning plan Given the following "core_competency > plans" exist: @@ -150,7 +137,6 @@ Feature: Manage plan workflow And I follow "Learning plans" Then I should see "Draft" And I should not see "Active" - And I log out Scenario: Manager change an active learning plan to completed Given the following "core_competency > plans" exist: @@ -169,7 +155,6 @@ Feature: Manage plan workflow And I follow "Learning plans" Then I should see "Complete" And I should not see "Active" - And I log out Scenario: Manager reopen a complete learning plan Given the following "core_competency > plans" exist: @@ -187,4 +172,67 @@ Feature: Manage plan workflow And I follow "Learning plans" Then I should see "Active" And I should not see "Complete" - And I log out + + Scenario: Student learning plan derived from templates can be completed + Given the following "core_competency > templates" exist: + | shortname | + | LPT1 | + And the following "core_competency > template_competencies" exist: + | template | competency | + | LPT1 | Test-Comp1 | + And I log in as "admin" + And I navigate to "Competencies > Learning plan templates" in site administration + # Select 1 user to assign to create learning plans for using template. + And I click on ".template-userplans" "css_element" in the "LPT1" "table_row" + And I set the field "Select users" to "user1" + And I press "Create learning plans" + And I click on "LPT1" "link" in the "LPT1" "table_row" + When I click on "Complete this learning plan" "link" + And I click on "Complete this learning plan" "button" in the "Confirm" "dialogue" + # Add a short wait to ensure the page has loaded before checking that "Complete" "text" exists. + And I wait until the page is ready + # Confirm that student's learning plan template is marked as Completed. + Then "Complete" "text" should exist + And "Reopen this learning plan" "link" should exist + + Scenario: Learning plan template updates are not reflected on plans already completed + Given the following "core_competency > templates" exist: + | shortname | + | LPT1 | + And the following "core_competency > template_competencies" exist: + | template | competency | + | LPT1 | Test-Comp1 | + And I log in as "admin" + And I navigate to "Competencies > Learning plan templates" in site administration + # Select 2 users to assign to create learning plans for using template. + And I click on ".template-userplans" "css_element" in the "LPT1" "table_row" + And I set the field "Select users" to "user1" + And I press "Create learning plans" + And I set the field "Select users" to "user2" + And I press "Create learning plans" + # Complete the learning plan for User 1. + And I click on "LPT1" "link" in the "User 1" "table_row" + And I click on "Complete this learning plan" "link" + And I click on "Complete this learning plan" "button" in the "Confirm" "dialogue" + # Add another competency to the learning plan template. + And the following "core_competency > template_competencies" exist: + | template | competency | + | LPT1 | Test-Comp2 | + # Navigate back to the list of Learning plan templates in order to access User 1's learning plan. + And I navigate to "Competencies > Learning plan templates" in site administration + And I click on ".template-userplans" "css_element" in the "LPT1" "table_row" + # Confirm that only the first competency is reflected on User 1's learning plan since it's already completed. + When I click on "LPT1" "link" in the "User 1" "table_row" + Then "Test-Comp2" "link" should not exist + And "Test-Comp2" "text" should not exist + And "Test-Comp1" "link" should exist + And "Test-Comp1" "text" should exist + # Navigate back to list of Learning plan templates in order to access User 2's learning plan. + And I navigate to "Competencies > Learning plan templates" in site administration + And I click on ".template-userplans" "css_element" in the "LPT1" "table_row" + # Confirm that both competencies are reflected on User 2's learning plan since it's not yet completed. + And I click on "LPT1" "link" in the "User 2" "table_row" + And "Test-Comp2" "link" should exist + And "Test-Comp2" "text" should exist + And "Test-Comp1" "link" should exist + And "Test-Comp1" "text" should exist diff --git a/public/admin/tool/lp/tests/behat/synchronize_cohorts_lp.feature b/public/admin/tool/lp/tests/behat/synchronize_cohorts_lp.feature new file mode 100644 index 0000000000000..70c2d38f936da --- /dev/null +++ b/public/admin/tool/lp/tests/behat/synchronize_cohorts_lp.feature @@ -0,0 +1,60 @@ +@tool @tool_lp @core_cohort +Feature: Cohorts can be synchronized with learning plans + In order to create learning plans for cohort members + As an admin + I need to be able to synchronise cohorts with learning plans + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | user1 | User | One | user1@example.com | + | user2 | User | Two | user2@example.com | + And the following "cohorts" exist: + | name | idnumber | + | Cohort 1 | CH1 | + And the following "cohort members" exist: + | user | cohort | + | user1 | CH1 | + | user2 | CH1 | + And the following "core_competency > frameworks" exist: + | shortname | idnumber | + | CF1 | CF1 | + And the following "core_competency > competencies" exist: + | shortname | competencyframework | idnumber | + | C1 | CF1 | C1 | + And the following "core_competency > templates" exist: + | shortname | + | LPT1 | + And the following "core_competency > template_competencies" exist: + | template | competency | + | LPT1 | C1 | + + @javascript + Scenario: Cohorts can be synchronised with learning plans + Given I log in as "admin" + # Navigate to the list of learning plan templates in order to add cohorts. + And I navigate to "Competencies > Learning plan templates" in site administration + When I click on "Add cohorts to sync" of edit menu in the "LPT1" row + And I set the field "Select cohorts to sync" to "Cohort 1" + And I press "Add cohorts" + And I wait until the page is ready + # Confirm that 2 learning plans were created for members of the cohort. + Then "2 learning plans were created" "text" should exist + # Confirm current screen is still "Cohorts synced to this learning plan template screen" + And "Cohorts synced to this learning plan template" "text" should exist + # Confirm that the cohort is now added to the learning plan template. + And the following should exist in the "generaltable" table: + | Name | Cohort ID | + | Cohort 1 | CH1 | + # Navigate back to the list of learning plan templates to view updated list. + And I navigate to "Competencies > Learning plan templates" in site administration + # Confirm that the added cohort and learning plans are now reflected on the list of Learning plan templates. + And the following should exist in the "generaltable" table: + | Name | Category | Cohorts | Learning plans | + | LPT1 | System | 1 | 2 | + And I click on ".template-userplans" "css_element" in the "LPT1" "table_row" + # Confirm that learning plans were created for all cohort members. + And the following should exist in the "generaltable" table: + | Name | First name / Last name | Email address | + | LPT1 | User One | user1@example.com | + | LPT1 | User Two | user2@example.com | diff --git a/public/admin/tool/lp/tests/externallib_test.php b/public/admin/tool/lp/tests/externallib_test.php index e01a25fe0656a..59f5a01a53c43 100644 --- a/public/admin/tool/lp/tests/externallib_test.php +++ b/public/admin/tool/lp/tests/externallib_test.php @@ -17,6 +17,7 @@ namespace tool_lp; use core_competency\api; +use core_competency\competency; use core_external\external_api; /** @@ -424,6 +425,80 @@ public function test_data_for_user_competency_summary_in_plan(): void { $this->assertEquals('A', $summary->usercompetencysummary->evidence[1]->gradename); } + /** + * Evidence stored against a course module competency is deleted if the CM is deleted. + * @covers \tool_lp\external::data_for_user_competency_summary_in_plan + */ + public function test_data_for_user_competency_summary_in_plan_deleted_cm(): void { + $this->setUser($this->creator); + + $dg = $this->getDataGenerator(); + $lpg = $dg->get_plugin_generator('core_competency'); + + $f1 = $lpg->create_framework(); + + $c1 = $lpg->create_competency(['competencyframeworkid' => $f1->get('id')]); + $c2 = $lpg->create_competency(['competencyframeworkid' => $f1->get('id')]); + + $tpl = $lpg->create_template(); + $lpg->create_template_competency(['templateid' => $tpl->get('id'), 'competencyid' => $c1->get('id')]); + $lpg->create_template_competency(['templateid' => $tpl->get('id'), 'competencyid' => $c2->get('id')]); + + $plan = $lpg->create_plan(['userid' => $this->user->id, 'templateid' => $tpl->get('id'), 'name' => 'Evil']); + + $course = $dg->create_course(); + + $assign1 = $dg->create_module('assign', ['course' => $course->id]); + $assign2 = $dg->create_module('assign', ['course' => $course->id]); + $lpg->create_course_module_competency(['competencyid' => $c1->get('id'), 'cmid' => $assign1->cmid]); + $lpg->create_course_module_competency(['competencyid' => $c2->get('id'), 'cmid' => $assign1->cmid]); + + api::add_evidence( + $this->user->id, + $c1->get('id'), + \core\context\module::instance($assign1->cmid), + competency::OUTCOME_COMPLETE, + 'evidence_competencyrule', + 'core_competency', + ); + api::add_evidence( + $this->user->id, + $c2->get('id'), + \core\context\module::instance($assign2->cmid), + competency::OUTCOME_COMPLETE, + 'evidence_competencyrule', + 'core_competency', + ); + + // Confirm we have evidence for 2 course module competencies. + $summary1 = external::data_for_user_competency_summary_in_plan($c1->get('id'), $plan->get('id')); + $this->assertEquals('Evil', $summary1->plan->name); + $this->assertEquals( + get_string('evidence_competencyrule', 'core_competency'), + $summary1->usercompetencysummary->evidence[0]->description + ); + $summary2 = external::data_for_user_competency_summary_in_plan($c2->get('id'), $plan->get('id')); + $this->assertEquals('Evil', $summary2->plan->name); + $this->assertEquals( + get_string('evidence_competencyrule', 'core_competency'), + $summary2->usercompetencysummary->evidence[0]->description + ); + + // Delete one course module. + course_delete_module($assign1->cmid); + + // The evidence for the deleted course module should have been deleted. Other evidence should remain. + $summary1 = external::data_for_user_competency_summary_in_plan($c1->get('id'), $plan->get('id')); + $this->assertEquals('Evil', $summary1->plan->name); + $this->assertFalse(array_key_exists(0, $summary1->usercompetencysummary->evidence)); + $summary2 = external::data_for_user_competency_summary_in_plan($c2->get('id'), $plan->get('id')); + $this->assertEquals('Evil', $summary2->plan->name); + $this->assertEquals( + get_string('evidence_competencyrule', 'core_competency'), + $summary2->usercompetencysummary->evidence[0]->description + ); + } + public function test_data_for_user_competency_summary(): void { $this->setUser($this->creator); diff --git a/public/admin/tool/messageinbound/renderer.php b/public/admin/tool/messageinbound/renderer.php index 9bf949e66f67a..d5bf003dfcdf8 100644 --- a/public/admin/tool/messageinbound/renderer.php +++ b/public/admin/tool/messageinbound/renderer.php @@ -56,7 +56,7 @@ public function messageinbound_handlers_table(array $handlers) { $enabled, $edit, ); - $table->attributes['class'] = 'admintable table generaltable messageinboundhandlers'; + $table->attributes['class'] = 'admintable table generaltable messageinboundhandlers table-hover'; $yes = get_string('yes'); $no = get_string('no'); diff --git a/public/admin/tool/mfa/classes/manager.php b/public/admin/tool/mfa/classes/manager.php index 9b7bbdc1e606a..74e9ca7216a02 100644 --- a/public/admin/tool/mfa/classes/manager.php +++ b/public/admin/tool/mfa/classes/manager.php @@ -74,7 +74,7 @@ public static function display_debug_notification(): void { get_string('achievedweight', 'tool_mfa'), get_string('status'), ]; - $table->attributes['class'] = 'admintable generaltable table table-bordered'; + $table->attributes['class'] = 'admintable generaltable table table-bordered table-hover'; $table->colclasses = [ 'text-end', '', diff --git a/public/admin/tool/mfa/classes/output/renderer.php b/public/admin/tool/mfa/classes/output/renderer.php index 2ea5b63949829..93cd9c074a595 100644 --- a/public/admin/tool/mfa/classes/output/renderer.php +++ b/public/admin/tool/mfa/classes/output/renderer.php @@ -174,7 +174,7 @@ public function active_factors(?string $filterfactor = null): string { $table = new \html_table(); $table->id = 'active_factors'; - $table->attributes['class'] = 'generaltable table table-bordered'; + $table->attributes['class'] = 'generaltable table table-bordered table-hover'; $table->head = [ $headers->devicename, $headers->added, @@ -348,7 +348,7 @@ public function factors_in_use_table(int $lookback): string { $table = new \html_table(); $table->head = $displaynames; $table->align = $colclasses; - $table->attributes['class'] = 'generaltable table table-bordered w-auto'; + $table->attributes['class'] = 'generaltable table table-bordered w-auto table-hover'; $table->attributes['style'] = 'width: auto; min-width: 50%; margin-bottom: 0;'; // Manually handle Total users and MFA users. @@ -461,7 +461,7 @@ public function factors_locked_table(): string { $table = new \html_table(); - $table->attributes['class'] = 'generaltable table table-bordered w-auto'; + $table->attributes['class'] = 'generaltable table table-bordered w-auto table-hover'; $table->attributes['style'] = 'width: auto; min-width: 50%'; $table->head = [ @@ -513,7 +513,7 @@ public function factor_locked_users_table(object_factor $factor): string { global $DB; $table = new \html_table(); - $table->attributes['class'] = 'generaltable table table-bordered w-auto'; + $table->attributes['class'] = 'generaltable table table-bordered w-auto table-hover'; $table->attributes['style'] = 'width: auto; min-width: 50%'; $table->head = [ 'userid' => get_string('userid', 'grades'), diff --git a/public/admin/tool/mfa/classes/table/admin_setting_managemfa.php b/public/admin/tool/mfa/classes/table/admin_setting_managemfa.php index 418cb90e7dca8..a91b12b86786d 100644 --- a/public/admin/tool/mfa/classes/table/admin_setting_managemfa.php +++ b/public/admin/tool/mfa/classes/table/admin_setting_managemfa.php @@ -131,7 +131,7 @@ public function output_factor_combinations_table(): void { $txt = get_strings(['combination', 'totalweight'], 'tool_mfa'); $table = new \html_table(); $table->id = 'mfacombinations'; - $table->attributes['class'] = 'admintable generaltable table table-bordered'; + $table->attributes['class'] = 'admintable generaltable table table-bordered table-hover'; $table->head = [$txt->combination, $txt->totalweight]; $table->data = []; diff --git a/public/admin/tool/mfa/factor/auth/db/upgrade.php b/public/admin/tool/mfa/factor/auth/db/upgrade.php index a18038506c830..4e443b4bf516e 100644 --- a/public/admin/tool/mfa/factor/auth/db/upgrade.php +++ b/public/admin/tool/mfa/factor/auth/db/upgrade.php @@ -57,5 +57,8 @@ function xmldb_factor_auth_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/mfa/factor/auth/lang/en/factor_auth.php b/public/admin/tool/mfa/factor/auth/lang/en/factor_auth.php index f1149e9eeda68..604ebe7ec2c0d 100644 --- a/public/admin/tool/mfa/factor/auth/lang/en/factor_auth.php +++ b/public/admin/tool/mfa/factor/auth/lang/en/factor_auth.php @@ -28,6 +28,6 @@ $string['privacy:metadata'] = 'The Authentication type factor plugin does not store any personal data.'; $string['settings:description'] = 'Automatically verify users based on their authentication type.'; $string['settings:goodauth'] = 'Factor authentication types'; -$string['settings:goodauth_help'] = 'Select all authentication types to use as a factor for MFA. Any types not selected will not be treated as a FAIL in MFA.'; +$string['settings:goodauth_help'] = 'Select all authentication types that will gain the points from this factor. Other authentication types will not be treated as a fail in MFA, but will not gain this factor\'s weight points.'; $string['settings:shortdescription'] = 'Allow users to bypass extra authentication steps based on their authentication type.'; $string['summarycondition'] = 'has an authentication type of {$a}'; diff --git a/public/admin/tool/mfa/factor/email/db/upgrade.php b/public/admin/tool/mfa/factor/email/db/upgrade.php index a54b0b7b3921f..72d32bd2ab789 100644 --- a/public/admin/tool/mfa/factor/email/db/upgrade.php +++ b/public/admin/tool/mfa/factor/email/db/upgrade.php @@ -56,5 +56,8 @@ function xmldb_factor_email_upgrade($oldversion): bool { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/mfa/factor/sms/db/upgrade.php b/public/admin/tool/mfa/factor/sms/db/upgrade.php index e391f53253942..5464499583553 100644 --- a/public/admin/tool/mfa/factor/sms/db/upgrade.php +++ b/public/admin/tool/mfa/factor/sms/db/upgrade.php @@ -99,5 +99,8 @@ classname: \smsgateway_aws\gateway::class, // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/mfa/factor/totp/classes/factor.php b/public/admin/tool/mfa/factor/totp/classes/factor.php index c669389e3caa8..affcef874b77c 100644 --- a/public/admin/tool/mfa/factor/totp/classes/factor.php +++ b/public/admin/tool/mfa/factor/totp/classes/factor.php @@ -195,7 +195,7 @@ public function setup_factor_form_definition_after_data(MoodleQuickForm $mform): $manualtable = new \html_table(); $manualtable->id = 'manualattributes'; - $manualtable->attributes['class'] = 'generaltable table table-bordered table-sm w-auto'; + $manualtable->attributes['class'] = 'generaltable table table-bordered table-sm w-auto table-hover'; $manualtable->attributes['style'] = 'width: auto;'; $manualtable->data = [ [get_string('setupfactor:key', 'factor_totp'), $secret], diff --git a/public/admin/tool/mfa/factor/totp/db/upgrade.php b/public/admin/tool/mfa/factor/totp/db/upgrade.php index a3944548e5c5f..c3c5839650210 100644 --- a/public/admin/tool/mfa/factor/totp/db/upgrade.php +++ b/public/admin/tool/mfa/factor/totp/db/upgrade.php @@ -45,5 +45,8 @@ function xmldb_factor_totp_upgrade($oldversion): bool { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/mobile/classes/api.php b/public/admin/tool/mobile/classes/api.php index 71f6e9abd3916..448d60042446a 100644 --- a/public/admin/tool/mobile/classes/api.php +++ b/public/admin/tool/mobile/classes/api.php @@ -817,7 +817,7 @@ public static function get_subscription_information(): ?array { $credentials[] = ['type' => 'airnotifieraccesskey', 'value' => $CFG->airnotifieraccesskey]; } if (\core\hub\registration::is_registered()) { - $credentials[] = ['type' => 'siteid', 'value' => $CFG->siteidentifier]; + $credentials[] = ['type' => 'siteid', 'value' => \core\hub\registration::get_secret()]; } // Generate a hash key for validating that the request is coming from this site via WS. $key = complex_random_string(32); diff --git a/public/admin/tool/mobile/classes/hook_callbacks.php b/public/admin/tool/mobile/classes/hook_callbacks.php index 80f164b7beb7e..52de464c31714 100644 --- a/public/admin/tool/mobile/classes/hook_callbacks.php +++ b/public/admin/tool/mobile/classes/hook_callbacks.php @@ -16,7 +16,10 @@ namespace tool_mobile; +use core\hook\output\extend_url; use html_writer; +use moodle_url; +use tool_mobile\local\hooks\before_extend_ios_app_banner; /** * Allows plugins to add any elements to the footer. @@ -35,22 +38,39 @@ public static function before_standard_head_html_generation( \core\hook\output\before_standard_head_html_generation $hook, ): void { global $CFG, $PAGE; - // Smart App Banners meta tag is only displayed if mobile services are enabled and configured. - if (!empty($CFG->enablemobilewebservice)) { - $mobilesettings = get_config('tool_mobile'); - if (!empty($mobilesettings->enablesmartappbanners)) { - if (!empty($mobilesettings->iosappid)) { - $hook->add_html( - '' - ); - } + // Only emit mobile app metadata when mobile services are enabled + configured. + if (empty($CFG->enablemobilewebservice)) { + return; + } + $mobilesettings = get_config('tool_mobile'); + if (empty($mobilesettings->enablesmartappbanners)) { + return; + } + // IOS with hook-based app id and argument augmentation. + if (!empty($mobilesettings->iosappid)) { + $appid = (string)$mobilesettings->iosappid; + $appargument = $PAGE->url->out(); + // Hook to allow modification of ios smart app banner fields. + $ioshook = new before_extend_ios_app_banner($appid, $appargument); + \core\di::get(\core\hook\manager::class)->dispatch($ioshook); + $appid = $ioshook->get_appid(); + $appargument = $ioshook->get_appargument(); + // Add the meta tag. + $hook->add_html( + '' + ); + } - if (!empty($mobilesettings->androidappid)) { - $mobilemanifesturl = "$CFG->wwwroot/$CFG->admin/tool/mobile/mobile.webmanifest.php"; - $hook->add_html(''); - } - } + // Android with hook-based URL augmentation. + if (!empty($mobilesettings->androidappid)) { + $url = new moodle_url('/admin/tool/mobile/mobile.webmanifest.php'); + $urlhook = new extend_url($url); + \core\di::get(\core\hook\manager::class)->dispatch($urlhook); + $url = $urlhook->get_url(); + + // Add the link tag. + $hook->add_html(''); } } diff --git a/public/admin/tool/mobile/classes/local/hooks/before_extend_ios_app_banner.php b/public/admin/tool/mobile/classes/local/hooks/before_extend_ios_app_banner.php new file mode 100644 index 0000000000000..8555bdcd6db3b --- /dev/null +++ b/public/admin/tool/mobile/classes/local/hooks/before_extend_ios_app_banner.php @@ -0,0 +1,78 @@ +. + +namespace tool_mobile\local\hooks; + +/** + * Allow adjustment of ios smart app banner fields. + * + * @package tool_mobile + * @copyright 2025 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +#[\core\attribute\label('Allow adjustment of ios smart app banner fields')] +#[\core\attribute\tags('mobile')] +final class before_extend_ios_app_banner { + /** + * Create a new instance of the hook. + * + * @param string $appid The app id for the ios smart banner + * @param string $appargument The app argument for the ios smart banner + */ + public function __construct( + /** @var string $appid The app id for the ios smart banner */ + private string $appid, + /** @var string $appargument The app argument for the ios smart banner */ + private string $appargument, + ) { + } + + /** + * Get the appid. + * + * @return string + */ + public function get_appid(): string { + return $this->appid; + } + + /** + * Set the appid. + * + * @param string $appid + */ + public function set_appid(string $appid): void { + $this->appid = $appid; + } + + /** + * Get the appargument. + * + * @return string + */ + public function get_appargument(): string { + return $this->appargument; + } + + /** + * Set the appargument. + * + * @param string $appargument + */ + public function set_appargument(string $appargument): void { + $this->appargument = $appargument; + } +} diff --git a/public/admin/tool/mobile/db/upgrade.php b/public/admin/tool/mobile/db/upgrade.php index 17676cbb5ebdf..612e6866f6676 100644 --- a/public/admin/tool/mobile/db/upgrade.php +++ b/public/admin/tool/mobile/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_tool_mobile_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/mobile/mobile.webmanifest.php b/public/admin/tool/mobile/mobile.webmanifest.php index 72a057b45475f..dfa1ec0980693 100644 --- a/public/admin/tool/mobile/mobile.webmanifest.php +++ b/public/admin/tool/mobile/mobile.webmanifest.php @@ -28,10 +28,13 @@ */ define('NO_DEBUG_DISPLAY', true); +define('NO_MOODLE_COOKIES', true); require_once(__DIR__ . '/../../../config.php'); header('Content-Type: application/json; charset: utf-8'); +header('Cache-Control: public, max-age=' . HOURSECS . ', no-transform'); +header('Expires: ' . gmdate('D, d M Y H:i:s', time() + HOURSECS) . ' GMT'); $mobilesettings = get_config('tool_mobile'); // Display manifest contents only if all the required conditions are met. diff --git a/public/admin/tool/monitor/db/upgrade.php b/public/admin/tool/monitor/db/upgrade.php index d5a9e8b2a7819..b90f7536df832 100644 --- a/public/admin/tool/monitor/db/upgrade.php +++ b/public/admin/tool/monitor/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_tool_monitor_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/moodlenet/classes/local/import_backup_helper.php b/public/admin/tool/moodlenet/classes/local/import_backup_helper.php index a2f923afb74e9..a53398dfb5fcd 100644 --- a/public/admin/tool/moodlenet/classes/local/import_backup_helper.php +++ b/public/admin/tool/moodlenet/classes/local/import_backup_helper.php @@ -86,7 +86,7 @@ public function get_stored_file(): \stored_file { } [$filepath, $filename] = $this->remoteresource->download_to_requestdir(); - \core\antivirus\manager::scan_file($filepath, $filename, true); + \core\antivirus\manager::scan_file($filepath . DIRECTORY_SEPARATOR . $filename, $filename, true); // Check the final size of file against the user upload limits. $localsize = filesize(sprintf('%s/%s', $filepath, $filename)); diff --git a/public/admin/tool/moodlenet/classes/local/import_strategy_file.php b/public/admin/tool/moodlenet/classes/local/import_strategy_file.php index e34092a62dc99..3a546401c10aa 100644 --- a/public/admin/tool/moodlenet/classes/local/import_strategy_file.php +++ b/public/admin/tool/moodlenet/classes/local/import_strategy_file.php @@ -78,7 +78,7 @@ public function import(remote_resource $resource, \stdClass $user, \stdClass $co // Download the file into a request directory and scan it. [$filepath, $filename] = $resource->download_to_requestdir(); - avmanager::scan_file($filepath, $filename, true); + avmanager::scan_file($filepath . DIRECTORY_SEPARATOR . $filename, $filename, true); // Check the final size of file against the user upload limits. $localsize = filesize(sprintf('%s/%s', $filepath, $filename)); diff --git a/public/admin/tool/moodlenet/db/upgrade.php b/public/admin/tool/moodlenet/db/upgrade.php index 795da5ff3e707..49f0d96dfb00d 100644 --- a/public/admin/tool/moodlenet/db/upgrade.php +++ b/public/admin/tool/moodlenet/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_tool_moodlenet_upgrade(int $oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/moodlenet/lang/en/tool_moodlenet.php b/public/admin/tool/moodlenet/lang/en/tool_moodlenet.php index 0e8ea8a0504c4..5c419a3b39277 100644 --- a/public/admin/tool/moodlenet/lang/en/tool_moodlenet.php +++ b/public/admin/tool/moodlenet/lang/en/tool_moodlenet.php @@ -38,6 +38,9 @@ $string['defaultmoodlenetname'] = "MoodleNet instance name"; $string['defaultmoodlenetnamevalue'] = 'MoodleNet Central'; $string['defaultmoodlenetname_desc'] = 'The name of the MoodleNet instance available via the activity chooser.'; +$string['removalwarning_feature'] = 'If you need to continue using MoodleNet, contact your site administrator about setting up a self-hosted MoodleNet instance.'; +$string['removalwarning_service'] = 'After the date, you will no longer be able to browse or add content from MoodleNet Central.'; +$string['removalwarning_title'] = 'The MoodleNet service will be shut down on 20 April 2026.'; $string['enablemoodlenet'] = 'Enable MoodleNet integration (inbound)'; $string['enablemoodlenet_desc'] = 'If enabled, a user with the capability to create and manage activities can browse MoodleNet via the activity chooser and import MoodleNet resources into their course. In addition, a user with the capability to restore backups can select a backup file on MoodleNet and restore it into Moodle.'; $string['errorduringdownload'] = 'An error occurred while downloading the file: {$a}'; diff --git a/public/admin/tool/moodlenet/templates/chooser_moodlenet.mustache b/public/admin/tool/moodlenet/templates/chooser_moodlenet.mustache index a812e5402cd6e..a2b37b5cfd5f8 100644 --- a/public/admin/tool/moodlenet/templates/chooser_moodlenet.mustache +++ b/public/admin/tool/moodlenet/templates/chooser_moodlenet.mustache @@ -27,8 +27,16 @@
- +

{{#str}} instancedescription, tool_moodlenet {{/str}}

+ + {{! Removal warning - always visible when MoodleNet integration is enabled }} + +

{{#str}} connectandbrowse, tool_moodlenet {{/str}}

help_icon('systemaccountconnected', 'tool_oauth2'), get_string('edit'), ]; - $table->attributes['class'] = 'admintable table generaltable'; + $table->attributes['class'] = 'admintable table generaltable table-hover'; $data = []; $index = 0; @@ -231,7 +231,7 @@ public function endpoints_table($endpoints, $issuerid) { get_string('url'), get_string('edit'), ]; - $table->attributes['class'] = 'admintable table generaltable'; + $table->attributes['class'] = 'admintable table generaltable table-hover'; $data = []; $index = 0; @@ -290,7 +290,7 @@ public function user_field_mappings_table($userfieldmappings, $issuerid) { get_string('userfieldinternalfield', 'tool_oauth2'), get_string('edit'), ]; - $table->attributes['class'] = 'admintable table generaltable'; + $table->attributes['class'] = 'admintable table generaltable table-hover'; $data = []; $index = 0; diff --git a/public/admin/tool/oauth2/tests/behat/basic_settings.feature b/public/admin/tool/oauth2/tests/behat/basic_settings.feature index d71f00edb9d12..7d3ff4dcf92d5 100644 --- a/public/admin/tool/oauth2/tests/behat/basic_settings.feature +++ b/public/admin/tool/oauth2/tests/behat/basic_settings.feature @@ -61,7 +61,8 @@ Feature: Basic OAuth2 functionality And I should see "device_authorization_endpoint" And I navigate to "Server > OAuth 2 services" in site administration And I click on "Configure user field mappings" "link" in the "Testing service" "table_row" - And I should see "firstname" in the "givenname" "table_row" + And I should see "firstname" in the "given_name" "table_row" + And I should see "lastname" in the "family_name" "table_row" And I should see "idnumber" in the "sub" "table_row" And I should see "email" in the "email" "table_row" And I should see "lang" in the "locale" "table_row" diff --git a/public/admin/tool/policy/classes/api.php b/public/admin/tool/policy/classes/api.php index 4d0d917982de9..6c257b8f00934 100644 --- a/public/admin/tool/policy/classes/api.php +++ b/public/admin/tool/policy/classes/api.php @@ -14,15 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Provides {@link tool_policy\output\renderer} class. - * - * @package tool_policy - * @category output - * @copyright 2018 David Mudrák - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace tool_policy; use coding_exception; @@ -33,13 +24,12 @@ use stdClass; use tool_policy\event\acceptance_created; use tool_policy\event\acceptance_updated; -use user_picture; - -defined('MOODLE_INTERNAL') || die(); /** * Provides the API of the policies plugin. * + * @package tool_policy + * @category output * @copyright 2018 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -1038,9 +1028,12 @@ public static function update_policyagreed($user = null) { // MDL-80973: At this point, the policyagreed value in DB could be 0 but $user->policyagreed could be 1 (as it was copied from $USER). // So we need to ensure that the value in DB is set true if all policies were responded. - if ($user->policyagreed != $allresponded || $allresponded) { + $userpolicyagreed = (bool) ($user->policyagreed ?? false); + if ($userpolicyagreed !== $allresponded || $allresponded) { $user->policyagreed = $allresponded; - $DB->set_field('user', 'policyagreed', $allresponded, ['id' => $user->id]); + if (!isguestuser($user)) { + $DB->set_field('user', 'policyagreed', $user->policyagreed, ['id' => $user->id]); + } } } diff --git a/public/admin/tool/policy/db/upgrade.php b/public/admin/tool/policy/db/upgrade.php index 718efad43577b..a39d0d670db4c 100644 --- a/public/admin/tool/policy/db/upgrade.php +++ b/public/admin/tool/policy/db/upgrade.php @@ -45,5 +45,8 @@ function xmldb_tool_policy_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/recyclebin/classes/course_bin.php b/public/admin/tool/recyclebin/classes/course_bin.php index e68fb1fbb2993..0fdddc30dca2d 100644 --- a/public/admin/tool/recyclebin/classes/course_bin.php +++ b/public/admin/tool/recyclebin/classes/course_bin.php @@ -145,6 +145,12 @@ public function store_item($cm) { return; } + // We never need badge information here. + if ($plan->setting_exists('badges')) { + $badges = $plan->get_setting('badges'); + $badges->set_value(false); + } + $controller->execute_plan(); // We don't need the forced setting anymore, hence restore previous settings. diff --git a/public/admin/tool/recyclebin/db/upgrade.php b/public/admin/tool/recyclebin/db/upgrade.php index d834688535fbc..374bafbadf7b8 100644 --- a/public/admin/tool/recyclebin/db/upgrade.php +++ b/public/admin/tool/recyclebin/db/upgrade.php @@ -49,8 +49,10 @@ function xmldb_tool_recyclebin_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. - if ($oldversion < 2025041401) { + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + if ($oldversion < 2025100601) { // Changing precision of field shortname on table tool_recyclebin_category to (1333). $table = new xmldb_table('tool_recyclebin_category'); $field = new xmldb_field('shortname', XMLDB_TYPE_CHAR, '1333', null, XMLDB_NOTNULL, null, null, 'categoryid'); @@ -73,7 +75,7 @@ function xmldb_tool_recyclebin_upgrade($oldversion) { $dbman->change_field_precision($table, $field); // Recyclebin savepoint reached. - upgrade_plugin_savepoint(true, 2025041401, 'tool', 'recyclebin'); + upgrade_plugin_savepoint(true, 2025100601, 'tool', 'recyclebin'); } return true; diff --git a/public/admin/tool/recyclebin/tests/behat/basic_functionality.feature b/public/admin/tool/recyclebin/tests/behat/basic_functionality.feature index ca888ef106981..307b328e49e84 100644 --- a/public/admin/tool/recyclebin/tests/behat/basic_functionality.feature +++ b/public/admin/tool/recyclebin/tests/behat/basic_functionality.feature @@ -39,11 +39,19 @@ Feature: Basic recycle bin functionality | student2 | G1 | | student2 | G2 | And the following config values are set as admin: - | coursebinenable | 1 | tool_recyclebin | - | categorybinenable | 1 | tool_recyclebin | - | coursebinexpiry | 604800 | tool_recyclebin | + | coursebinenable | 1 | tool_recyclebin | + | categorybinenable | 1 | tool_recyclebin | + | coursebinexpiry | 604800 | tool_recyclebin | | categorybinexpiry | 1209600 | tool_recyclebin | - | autohide | 0 | tool_recyclebin | + | autohide | 0 | tool_recyclebin | + And the following "core_badges > Badges" exist: + | name | course | description | image | status | type | + | My course 1 badge | C1 | Badge description | badges/tests/behat/badge.png | active | 2 | + | My course 2 badge | C2 | Badge description | badges/tests/behat/badge.png | active | 2 | + And the following "core_badges > Criterias" exist: + | badge | role | + | My course 1 badge | editingteacher | + | My course 2 badge | editingteacher | Scenario: Restore a deleted assignment Given I log in as "teacher1" @@ -58,6 +66,14 @@ Feature: Basic recycle bin functionality And I wait to be redirected And I am on "Course 1" course homepage And I should see "Test assign 1" in the "Section 1" "section" + # Check badges were not duplicated. + And I navigate to "Badges" in current page administration + And the following should exist in the "reportbuilder-table" table: + | Name | Badge status | + | My course 1 badge | Available | + And the following should not exist in the "reportbuilder-table" table: + | Name | Badge status | + | My course 1 badge | Not available | @javascript Scenario: Restore a deleted course @@ -83,6 +99,11 @@ Feature: Basic recycle bin functionality And "Student 1" "text" should exist in the "Group A" "table_row" And "Student 2" "text" should exist in the "Group A" "table_row" And "Student 2" "text" should exist in the "Group B" "table_row" + # Check badges are restored. + And I navigate to "Badges" in current page administration + And the following should exist in the "reportbuilder-table" table: + | Name | Badge status | + | My course 2 badge | Not available | @javascript Scenario: Deleting a single item from the recycle bin diff --git a/public/admin/tool/recyclebin/version.php b/public/admin/tool/recyclebin/version.php index e7578e5a05776..f459e8eb35cfb 100644 --- a/public/admin/tool/recyclebin/version.php +++ b/public/admin/tool/recyclebin/version.php @@ -24,6 +24,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2025100600; // The current plugin version (Date: YYYYMMDDXX). +$plugin->version = 2025100601; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025092600; // Requires this Moodle version. $plugin->component = 'tool_recyclebin'; // Full name of the plugin (used for diagnostics). diff --git a/public/admin/tool/task/renderer.php b/public/admin/tool/task/renderer.php index f031dded55319..0aa934c2793a4 100644 --- a/public/admin/tool/task/renderer.php +++ b/public/admin/tool/task/renderer.php @@ -58,7 +58,7 @@ public function adhoc_tasks_summary_table(array $summary): string { get_string('nextruntime', 'tool_task'), ]; - $table->attributes['class'] = 'admintable table generaltable'; + $table->attributes['class'] = 'admintable table generaltable table-hover'; $table->colclasses = []; // For each task entry (row) show action buttons/logs link depending on the user permissions. @@ -200,8 +200,9 @@ public function adhoc_tasks_class_table(string $classname, array $tasks, ?array }); } - return html_writer::table($table) - . html_writer::div( + $output = html_writer::table($table); + if ($canruntasks) { + $output .= html_writer::div( html_writer::link( new moodle_url( $adhocrunurl, @@ -210,7 +211,10 @@ public function adhoc_tasks_class_table(string $classname, array $tasks, ?array get_string('runclassname', 'tool_task') ), 'task-runnow' - ) + ); + } + + return $output . html_writer::div( html_writer::link( new moodle_url( @@ -259,7 +263,7 @@ private function generate_adhoc_tasks_simple_table(array $tasks, bool $wantrunta get_string('actions','tool_task'), ]; - $table->attributes['class'] = 'table generaltable'; + $table->attributes['class'] = 'table generaltable table-hover'; $table->colclasses = []; // For each task entry (row) show action buttons/logs link depending on the user permissions. @@ -384,7 +388,7 @@ public function scheduled_tasks_table($tasks, $lastchanged = '') { get_string('default', 'tool_task'), ]; - $table->attributes['class'] = 'admintable table generaltable'; + $table->attributes['class'] = 'admintable table generaltable table-hover'; $table->colclasses = []; if (!$showloglink) { diff --git a/public/admin/tool/uploaduser/classes/preview.php b/public/admin/tool/uploaduser/classes/preview.php index 83daeaea1f865..316cacc839a1f 100644 --- a/public/admin/tool/uploaduser/classes/preview.php +++ b/public/admin/tool/uploaduser/classes/preview.php @@ -64,7 +64,7 @@ public function __construct(\csv_import_reader $cir, array $filecolumns, int $pr $this->previewrows = $previewrows; $this->id = "uupreview"; - $this->attributes['class'] = 'table generaltable'; + $this->attributes['class'] = 'table generaltable table-hover'; $this->tablealign = 'center'; $this->summary = get_string('uploaduserspreview', 'tool_uploaduser'); $this->head = array(); diff --git a/public/admin/tool/uploaduser/locallib.php b/public/admin/tool/uploaduser/locallib.php index 56ea010d6e89f..6b8c88e19b363 100644 --- a/public/admin/tool/uploaduser/locallib.php +++ b/public/admin/tool/uploaduser/locallib.php @@ -94,7 +94,8 @@ public function __construct() { */ public function start() { $ci = 0; - echo '
{{typename}} {{#userdate}} {{timecreated}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}}{{requestedbyuser.fullname}}{{requestedbyuser.fullname}} {{statuslabel}}
'; + echo '
'; echo ''; foreach ($this->headers as $key => $header) { echo ''; diff --git a/public/admin/tool/uploaduser/tests/behat/suspend_user_enrolment.feature b/public/admin/tool/uploaduser/tests/behat/suspend_user_enrolment.feature new file mode 100644 index 0000000000000..cdbdef176b404 --- /dev/null +++ b/public/admin/tool/uploaduser/tests/behat/suspend_user_enrolment.feature @@ -0,0 +1,41 @@ +@tool @tool_uploaduser @_file_upload +Feature: Admin can suspend user course enrolment via CSV upload + In order to manage enrolments in bulk + As an administrator + I need to be able to enrol and suspend users using CSV upload + + Background: + Given the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + | Course 2 | C2 | + | Course 3 | C3 | + + @javascript + Scenario: Admin uploads enrol and suspend CSVs and verifies enrolment status + Given I log in as "admin" + And I navigate to "Users > Accounts > Upload users" in site administration + When I upload "lib/tests/fixtures/QA_user_enrol.txt" file to "File" filemanager + And I press "Upload users" + And I press "Upload users" + And I press "Continue" + And I upload "lib/tests/fixtures/QA_user_suspend.txt" file to "File" filemanager + And I press "Upload users" + And I set the field "Upload type" to "Update existing users only" + And I press "Upload users" + And I press "Continue" + And I am on the "Course 1" "enrolled users" page + Then the following should exist in the "participants" table: + | First name | Status | + | Learner One | Active | + | Learner Two | Active | + And I am on the "Course 2" "enrolled users" page + And the following should exist in the "participants" table: + | First name | Status | + | Learner One | Active | + | Learner Two | Suspended | + And I am on the "Course 3" "enrolled users" page + And the following should exist in the "participants" table: + | First name | Status | + | Learner One | Suspended | + | Learner Two | Active | diff --git a/public/admin/tool/usertours/amd/build/tour.min.js b/public/admin/tool/usertours/amd/build/tour.min.js index dc6f6196126f8..9e0d8d5ebc3b9 100644 --- a/public/admin/tool/usertours/amd/build/tour.min.js +++ b/public/admin/tool/usertours/amd/build/tour.min.js @@ -1,3 +1,3 @@ -define("tool_usertours/tour",["exports","jquery","core/aria","core/popper","core/event_dispatcher","./events","core/str","core/prefetch","core/event","core/pending"],(function(_exports,_jquery,Aria,_popper,_event_dispatcher,_events,_str,_prefetch,_event,_pending){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery),Aria=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Aria),_popper=_interopRequireDefault(_popper),_pending=_interopRequireDefault(_pending);var _default=class{constructor(config){_defineProperty(this,"tourRunning",!1),_defineProperty(this,"hasFixedPosition",(targetNode=>{let currentElement=targetNode[0];for(;currentElement;){if("fixed"===window.getComputedStyle(currentElement).position)return!0;currentElement=currentElement.parentElement}return!1})),this.init(config)}init(config){this.eventHandlers={},this.reset(),this.originalConfiguration=config||{},this.configure.apply(this,arguments),this.possitionNeedToBeRecalculated=!1,this.recalculatedNo=0;try{this.storage=window.sessionStorage,this.storageKey="tourstate_"+this.tourName}catch(e){this.storage=!1,this.storageKey=""}return(0,_prefetch.prefetchStrings)("tool_usertours",["nextstep_sequence","skip_tour"]),this}reset(){return this.hide(),this.eventHandlers=[],this.resetStepListeners(),this.originalConfiguration={},this.steps=[],this.currentStepNumber=0,this}configure(config){if("object"==typeof config){if(void 0!==config.tourName&&(this.tourName=config.tourName),config.eventHandlers)for(let eventName in config.eventHandlers)config.eventHandlers[eventName].forEach((function(handler){this.addEventHandler(eventName,handler)}),this);this.resetStepDefaults(!0),"object"==typeof config.steps&&(this.steps=config.steps),void 0!==config.template&&(this.templateContent=config.template)}return this.checkMinimumRequirements(),this}checkMinimumRequirements(){if(!this.tourName)throw new Error("Tour Name required");if(!this.steps||!this.steps.length)throw new Error("Steps must be specified")}resetStepDefaults(loadOriginalConfiguration){return void 0===loadOriginalConfiguration&&(loadOriginalConfiguration=!0),this.stepDefaults={},loadOriginalConfiguration&&void 0!==this.originalConfiguration.stepDefaults?this.setStepDefaults(this.originalConfiguration.stepDefaults):this.setStepDefaults({}),this}setStepDefaults(stepDefaults){return this.stepDefaults||(this.stepDefaults={}),_jquery.default.extend(this.stepDefaults,{element:"",placement:"top",delay:0,moveOnClick:!1,moveAfterTime:0,orphan:!1,direction:1},stepDefaults),this}getCurrentStepNumber(){return parseInt(this.currentStepNumber,10)}setCurrentStepNumber(stepNumber){if(this.currentStepNumber=stepNumber,this.storage)try{this.storage.setItem(this.storageKey,stepNumber)}catch(e){e.code===DOMException.QUOTA_EXCEEDED_ERR&&this.storage.removeItem(this.storageKey)}}getNextStepNumber(stepNumber){void 0===stepNumber&&(stepNumber=this.getCurrentStepNumber());let nextStepNumber=stepNumber+1;for(;nextStepNumber<=this.steps.length;){if(this.isStepPotentiallyVisible(this.getStepConfig(nextStepNumber)))return nextStepNumber;nextStepNumber++}return null}getPreviousStepNumber(stepNumber){void 0===stepNumber&&(stepNumber=this.getCurrentStepNumber());let previousStepNumber=stepNumber-1;for(;previousStepNumber>=0;){if(this.isStepPotentiallyVisible(this.getStepConfig(previousStepNumber)))return previousStepNumber;previousStepNumber--}return null}isLastStep(stepNumber){return null===this.getNextStepNumber(stepNumber)}isStepPotentiallyVisible(stepConfig){return!!stepConfig&&(!!this.isStepActuallyVisible(stepConfig)||(!(void 0===stepConfig.orphan||!stepConfig.orphan)||!(void 0===stepConfig.delay||!stepConfig.delay)))}getPotentiallyVisibleSteps(){let position=1,result=[];for(let stepNumber=0;stepNumber=this.steps.length)return null;let stepConfig=this.normalizeStepConfig(this.steps[stepNumber]);return stepConfig=_jquery.default.extend(stepConfig,{stepNumber:stepNumber}),stepConfig}normalizeStepConfig(stepConfig){return void 0!==stepConfig.reflex&&void 0===stepConfig.moveAfterClick&&(stepConfig.moveAfterClick=stepConfig.reflex),void 0!==stepConfig.element&&void 0===stepConfig.target&&(stepConfig.target=stepConfig.element),void 0!==stepConfig.content&&void 0===stepConfig.body&&(stepConfig.body=stepConfig.content),stepConfig=_jquery.default.extend({},this.stepDefaults,stepConfig),(stepConfig=_jquery.default.extend({},{attachTo:stepConfig.target,attachPoint:"after"},stepConfig)).attachTo&&(stepConfig.attachTo=(0,_jquery.default)(stepConfig.attachTo).first()),stepConfig}getStepTarget(stepConfig){return stepConfig.target?(0,_jquery.default)(stepConfig.target):null}dispatchEvent(eventName){let detail=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},cancelable=arguments.length>2&&void 0!==arguments[2]&&arguments[2];return(0,_event_dispatcher.dispatchEvent)(eventName,{tour:this,...detail},document,{cancelable:cancelable})}addEventHandler(eventName,handler){return void 0===this.eventHandlers[eventName]&&(this.eventHandlers[eventName]=[]),this.eventHandlers[eventName].push(handler),this}processStepListeners(stepConfig){if(this.listeners.push({node:this.currentStepNode,args:["click",'[data-role="next"]',_jquery.default.proxy(this.next,this)]},{node:this.currentStepNode,args:["click",'[data-role="end"]',_jquery.default.proxy(this.endTour,this)]},{node:(0,_jquery.default)('[data-flexitour="backdrop"]'),args:["click",_jquery.default.proxy(this.hide,this)]},{node:(0,_jquery.default)("body"),args:["keydown",_jquery.default.proxy(this.handleKeyDown,this)]}),stepConfig.moveOnClick){var targetNode=this.getStepTarget(stepConfig);this.listeners.push({node:targetNode,args:["click",_jquery.default.proxy((function(e){0===(0,_jquery.default)(e.target).parents('[data-flexitour="container"]').length&&window.setTimeout(_jquery.default.proxy(this.next,this),500)}),this)]})}return this.listeners.forEach((function(listener){listener.node.on.apply(listener.node,listener.args)})),this}resetStepListeners(){return this.listeners&&this.listeners.forEach((function(listener){listener.node.off.apply(listener.node,listener.args)})),this.listeners=[],this}renderStep(stepConfig){this.currentStepConfig=stepConfig,this.setCurrentStepNumber(stepConfig.stepNumber);let template=(0,_jquery.default)(this.getTemplateContent());template.find('[data-placeholder="title"]').html(stepConfig.title),template.find('[data-placeholder="body"]').html(stepConfig.body);const nextBtn=template.find('[data-role="next"]'),endBtn=template.find('[data-role="end"]');if(this.isLastStep(stepConfig.stepNumber)?(nextBtn.hide(),endBtn.removeClass("btn-secondary").addClass("btn-primary")):(nextBtn.prop("disabled",!1),(0,_str.getString)("skip_tour","tool_usertours").then((value=>{endBtn.html(value)})).catch()),nextBtn.attr("role","button"),endBtn.attr("role","button"),this.originalConfiguration.displaystepnumbers){const stepsPotentiallyVisible=this.getPotentiallyVisibleSteps(),totalStepsPotentiallyVisible=stepsPotentiallyVisible.length,position=stepsPotentiallyVisible[stepConfig.stepNumber].position;totalStepsPotentiallyVisible>1&&(0,_str.getString)("nextstep_sequence","tool_usertours",{position:position,total:totalStepsPotentiallyVisible}).then((value=>{nextBtn.html(value)})).catch()}return stepConfig.template=template,this.addStepToPage(stepConfig),this.processStepListeners(stepConfig),this}getTemplateContent(){return(0,_jquery.default)(this.templateContent).clone()}addStepToPage(stepConfig){let currentStepNode=(0,_jquery.default)('').html(stepConfig.template).hide();(0,_event.notifyFilterContentUpdated)(currentStepNode);let animationTarget=(0,_jquery.default)("body, html").stop(!0,!0);if(this.isStepActuallyVisible(stepConfig)){this.getStepTarget(stepConfig).data("flexitour","target"),this.positionBackdrop(stepConfig),(0,_jquery.default)(document.body).append(currentStepNode),this.currentStepNode=currentStepNode,this.currentStepNode.css({top:0,left:0});const pendingPromise=new _pending.default("tool_usertours/tour:addStepToPage-".concat(stepConfig.stepNumber));animationTarget.animate({scrollTop:this.calculateScrollTop(stepConfig)}).promise().then(function(){this.positionStep(stepConfig),this.revealStep(stepConfig),pendingPromise.resolve()}.bind(this)).catch((function(){}))}else stepConfig.orphan&&(stepConfig.isOrphan=!0,stepConfig.attachTo=(0,_jquery.default)("body").first(),stepConfig.attachPoint="append",this.positionBackdrop(stepConfig),currentStepNode.addClass("orphan"),(0,_jquery.default)(document.body).append(currentStepNode),this.currentStepNode=currentStepNode,this.currentStepNode.css("position","fixed"),this.currentStepPopper=new _popper.default((0,_jquery.default)("body"),this.currentStepNode[0],{removeOnDestroy:!0,placement:stepConfig.placement+"-start",arrowElement:'[data-role="arrow"]',modifiers:{hide:{enabled:!1},applyStyle:{onLoad:null,enabled:!1}},onCreate:()=>{const images=this.currentStepNode.find("img");images.length&&images.on("load",(()=>{this.calculateStepPositionInPage(currentStepNode)})),this.calculateStepPositionInPage(currentStepNode)}}),this.revealStep(stepConfig));return this}revealStep(stepConfig){const pendingPromise=new _pending.default("tool_usertours/tour:revealStep-".concat(stepConfig.stepNumber));return this.currentStepNode.fadeIn("",_jquery.default.proxy((function(){this.announceStep(stepConfig),this.currentStepNode.focus(),window.setTimeout(_jquery.default.proxy((function(){this.currentStepNode&&this.currentStepNode.focus(),pendingPromise.resolve()}),this),100)}),this)),this}announceStep(stepConfig){let stepId="tour-step-"+this.tourName+"-"+stepConfig.stepNumber;this.currentStepNode.attr("id",stepId);let bodyRegion=this.currentStepNode.find('[data-placeholder="body"]').first();bodyRegion.attr("id",stepId+"-body"),bodyRegion.attr("role","document");let headerRegion=this.currentStepNode.find('[data-placeholder="title"]').first();headerRegion.attr("id",stepId+"-title"),headerRegion.attr("aria-labelledby",stepId+"-body"),this.currentStepNode.attr("role","dialog"),this.currentStepNode.attr("tabindex",0),this.currentStepNode.attr("aria-labelledby",stepId+"-title"),this.currentStepNode.attr("aria-describedby",stepId+"-body");let target=this.getStepTarget(stepConfig);return target&&(target.data("original-tabindex",target.attr("tabindex")),target.attr("tabindex")||target.attr("tabindex",0),target.data("original-describedby",target.attr("aria-describedby")).attr("aria-describedby",stepId+"-body")),this.accessibilityShow(stepConfig),this}handleKeyDown(e){let tabbableSelector="a[href], link[href], [draggable=true], [contenteditable=true], ";switch(tabbableSelector+=":input:enabled, [tabindex], button:enabled",e.keyCode){case 27:this.endTour();break;case 9:(function(){if(!this.currentStepConfig.hasBackdrop)return;let currentIndex,nextIndex,nextNode,focusRelevant,activeElement=(0,_jquery.default)(document.activeElement),stepTarget=this.getStepTarget(this.currentStepConfig),tabbableNodes=(0,_jquery.default)(tabbableSelector),dialogContainer=(0,_jquery.default)('span[data-flexitour="container"]');if(stepTarget&&(tabbableNodes=tabbableNodes.filter((function(index,element){return null!==stepTarget&&(stepTarget.has(element).length||dialogContainer.has(element).length||stepTarget.is(element)||dialogContainer.is(element))}))),tabbableNodes.each((function(index,element){return!activeElement.is(element)||(currentIndex=index,!1)})),null!=currentIndex){let direction=1;e.shiftKey&&(direction=-1),nextIndex=currentIndex;do{nextIndex+=direction,nextNode=(0,_jquery.default)(tabbableNodes[nextIndex])}while(nextNode.length&&nextNode.is(":disabled")||nextNode.is(":hidden"));nextNode.length?(focusRelevant=nextNode.closest(stepTarget).length,focusRelevant=focusRelevant||nextNode.closest(this.currentStepNode).length):focusRelevant=!1}focusRelevant?nextNode.focus():e.shiftKey?this.currentStepNode.find(tabbableSelector).last().focus():this.currentStepConfig.isOrphan?this.currentStepNode.focus():stepTarget.focus(),e.preventDefault()}).call(this)}}startTour(startAt){if(this.storage&&void 0===startAt){let storageStartValue=this.storage.getItem(this.storageKey);if(storageStartValue){let storageStartAt=parseInt(storageStartValue,10);storageStartAt<=this.steps.length&&(startAt=storageStartAt)}}void 0===startAt&&(startAt=this.getCurrentStepNumber());return this.dispatchEvent(_events.eventTypes.tourStart,{startAt:startAt},!0).defaultPrevented||(this.gotoStep(startAt),this.tourRunning=!0,this.dispatchEvent(_events.eventTypes.tourStarted,{startAt:startAt})),this}restartTour(){return this.startTour(0)}endTour(){if(this.dispatchEvent(_events.eventTypes.tourEnd,{},!0).defaultPrevented)return this;if(this.currentStepConfig){let previousTarget=this.getStepTarget(this.currentStepConfig);previousTarget&&(previousTarget.attr("tabindex")||previousTarget.attr("tabindex","-1"),previousTarget.first().focus())}return this.hide(!0),this.tourRunning=!1,this.dispatchEvent(_events.eventTypes.tourEnded),this}hide(transition){if(this.dispatchEvent(_events.eventTypes.stepHide,{},!0).defaultPrevented)return this;const pendingPromise=new _pending.default("tool_usertours/tour:hide");if(this.currentStepNode&&this.currentStepNode.length&&(this.currentStepNode.hide(),this.currentStepPopper&&this.currentStepPopper.destroy()),this.currentStepConfig){let target=this.getStepTarget(this.currentStepConfig);target&&(target.data("original-labelledby")&&target.attr("aria-labelledby",target.data("original-labelledby")),target.data("original-describedby")&&target.attr("aria-describedby",target.data("original-describedby")),target.data("original-tabindex")?target.attr("tabindex",target.data("tabindex")):window.setTimeout((()=>{target.removeAttr("tabindex")}),400)),this.currentStepConfig=null}(0,_jquery.default)('[data-flexitour="highlight"]').removeAttr("data-flexitour");const backdrop=(0,_jquery.default)('[data-flexitour="backdrop"]');if(backdrop.length)if(transition){const backdropRemovalPromise=new _pending.default("tool_usertours/tour:hide:backdrop");backdrop.fadeOut(400,(function(){(0,_jquery.default)(this).remove(),backdropRemovalPromise.resolve()}))}else backdrop.remove();if(this.currentStepNode&&this.currentStepNode.length){let stepId=this.currentStepNode.attr("id");if(stepId){let currentStepElement='[aria-describedby="'+stepId+'-body"]';(0,_jquery.default)(currentStepElement).removeAttr("tabindex"),(0,_jquery.default)(currentStepElement).removeAttr("aria-describedby")}}return this.resetStepListeners(),this.accessibilityHide(),this.dispatchEvent(_events.eventTypes.stepHidden),this.currentStepNode=null,this.currentStepPopper=null,pendingPromise.resolve(),this}show(){let startAt=this.getCurrentStepNumber();return this.gotoStep(startAt)}getStepContainer(){return(0,_jquery.default)(this.currentStepNode)}calculateScrollTop(stepConfig){let viewportHeight=(0,_jquery.default)(window).height(),targetNode=this.getStepTarget(stepConfig),scrollParent=(0,_jquery.default)(window);targetNode.parents('[data-usertour="scroller"]').length&&(scrollParent=targetNode.parents('[data-usertour="scroller"]'));let scrollTop=scrollParent.scrollTop();return this.hasFixedPosition(targetNode)||(scrollTop="top"===stepConfig.placement?targetNode.offset().top-viewportHeight/2:"bottom"===stepConfig.placement?targetNode.offset().top+targetNode.height()+scrollTop-viewportHeight/2:targetNode.height()<=.8*viewportHeight?targetNode.offset().top-(viewportHeight-targetNode.height())/2:targetNode.offset().top-.2*viewportHeight),scrollTop=Math.max(0,scrollTop),scrollTop=Math.min((0,_jquery.default)(document).height()-viewportHeight,scrollTop),Math.ceil(scrollTop)}calculateStepPositionInPage(currentStepNode){let top=10;const viewportHeight=(0,_jquery.default)(window).height(),stepHeight=currentStepNode.height(),viewportWidth=(0,_jquery.default)(window).width(),stepWidth=currentStepNode.width();if(viewportHeight>=stepHeight+20)top=Math.ceil((viewportHeight-stepHeight)/2);else{var _currentStepNode$find,_currentStepNode$find2;const maxHeight=viewportHeight-20-(null!==(_currentStepNode$find=currentStepNode.find(".modal-header").first().outerHeight())&&void 0!==_currentStepNode$find?_currentStepNode$find:0)-(null!==(_currentStepNode$find2=currentStepNode.find(".modal-footer").first().outerHeight())&&void 0!==_currentStepNode$find2?_currentStepNode$find2:0);currentStepNode.find('[data-placeholder="body"]').first().css({"max-height":maxHeight+"px",overflow:"auto"})}currentStepNode.offset({top:top,left:Math.ceil((viewportWidth-stepWidth)/2)})}positionStep(stepConfig){let flipBehavior,content=this.currentStepNode,thisT=this;if(!content||!content.length)return this;switch(stepConfig.placement=this.recalculatePlacement(stepConfig),stepConfig.placement){case"left":flipBehavior=["left","right","top","bottom"];break;case"right":flipBehavior=["right","left","top","bottom"];break;case"top":flipBehavior=["top","bottom","right","left"];break;case"bottom":flipBehavior=["bottom","top","right","left"];break;default:flipBehavior="flip"}let offset="0";stepConfig.backdrop&&(offset="-".concat(10,", ").concat(10));let target=this.getStepTarget(stepConfig);var config={placement:stepConfig.placement+"-start",removeOnDestroy:!0,modifiers:{flip:{behaviour:flipBehavior},arrow:{element:'[data-role="arrow"]'},offset:{offset:offset}},onCreate:function(data){recalculateArrowPosition(data),recalculateStepPosition(data)},onUpdate:function(data){recalculateArrowPosition(data),thisT.possitionNeedToBeRecalculated&&(thisT.recalculatedNo++,thisT.possitionNeedToBeRecalculated=!1,recalculateStepPosition(data)),thisT.recalculateBackdropPosition(stepConfig)}};let recalculateArrowPosition=function(data){let placement=data.placement.split("-")[0];const isVertical=-1!==["left","right"].indexOf(placement),arrowElement=data.instance.popper.querySelector('[data-role="arrow"]'),stepElement=(0,_jquery.default)(data.instance.popper.querySelector('[data-role="flexitour-step"]'));if(isVertical){let arrowHeight=parseFloat(window.getComputedStyle(arrowElement).height),arrowOffset=parseFloat(window.getComputedStyle(arrowElement).top),popperHeight=parseFloat(window.getComputedStyle(data.instance.popper).height),popperOffset=parseFloat(window.getComputedStyle(data.instance.popper).top),popperBorderWidth=parseFloat(stepElement.css("borderTopWidth")),popperBorderRadiusWidth=2*parseFloat(stepElement.css("borderTopLeftRadius")),arrowPos=arrowOffset+arrowHeight/2,maxPos=popperHeight+popperOffset-popperBorderWidth-popperBorderRadiusWidth,minPos=popperOffset+popperBorderWidth+popperBorderRadiusWidth;if(arrowPos>=maxPos||arrowPos<=minPos){let newArrowPos=0;newArrowPos=arrowPos>popperHeight/2?maxPos-arrowHeight:minPos+arrowHeight,(0,_jquery.default)(arrowElement).css("top",newArrowPos)}}else{let arrowWidth=parseFloat(window.getComputedStyle(arrowElement).width),arrowOffset=parseFloat(window.getComputedStyle(arrowElement).left),popperWidth=parseFloat(window.getComputedStyle(data.instance.popper).width),popperOffset=parseFloat(window.getComputedStyle(data.instance.popper).left),popperBorderWidth=parseFloat(stepElement.css("borderTopWidth")),popperBorderRadiusWidth=2*parseFloat(stepElement.css("borderTopLeftRadius")),arrowPos=arrowOffset+arrowWidth/2,maxPos=popperWidth+popperOffset-popperBorderWidth-popperBorderRadiusWidth,minPos=popperOffset+popperBorderWidth+popperBorderRadiusWidth;if(arrowPos>=maxPos||arrowPos<=minPos){let newArrowPos=0;newArrowPos=arrowPos>popperWidth/2?maxPos-arrowWidth:minPos+arrowWidth,(0,_jquery.default)(arrowElement).css("left",newArrowPos)}}};const recalculateStepPosition=function(data){var _headerEle$outerHeigh,_footerEle$outerHeigh;const placement=data.placement.split("-")[0],isVertical=-1!==["left","right"].indexOf(placement),popperElement=(0,_jquery.default)(data.instance.popper),targetElement=(0,_jquery.default)(data.instance.reference),arrowElement=popperElement.find('[data-role="arrow"]'),stepElement=popperElement.find('[data-role="flexitour-step"]'),viewportHeight=(0,_jquery.default)(window).height(),viewportWidth=(0,_jquery.default)(window).width(),arrowHeight=parseFloat(arrowElement.outerHeight(!0)),popperHeight=parseFloat(popperElement.outerHeight(!0)),targetHeight=parseFloat(targetElement.outerHeight(!0)),arrowWidth=parseFloat(arrowElement.outerWidth(!0)),popperWidth=parseFloat(popperElement.outerWidth(!0)),targetWidth=parseFloat(targetElement.outerWidth(!0));let maxHeight;if(thisT.recalculatedNo>1&&(thisT.currentStepPopper.options.placement=isVertical?"auto-left":"auto-bottom"),thisT.recalculatedNo>2)return;if(isVertical){const leftSpace=targetElement.offset().left>0?targetElement.offset().left:0,rightSpace=viewportWidth-leftSpace-targetWidth,remainingSpace=leftSpace>=rightSpace?leftSpace:rightSpace;if(maxHeight=viewportHeight-20,remainingSpace0&&(popperElement.css({"max-width":maxWidth+"px"}),thisT.possitionNeedToBeRecalculated=!0)}else maxHeight0?targetElement.offset().top:0,bottomSpace=viewportHeight-topSpace-targetHeight,remainingSpace=topSpace>=bottomSpace?topSpace:bottomSpace;maxHeight=remainingSpace-10-arrowHeight,remainingSpace0?(headerEle.removeClass("minimal"),footerEle.removeClass("minimal"),currentStepBody.css({"max-height":maxHeight+"px",overflow:"auto"})):(headerEle.addClass("minimal"),footerEle.addClass("minimal")),thisT.currentStepPopper.update()};let background=(0,_jquery.default)('[data-flexitour="highlight"]');return background.length&&(target=background),this.currentStepPopper=new _popper.default(target,content[0],config),this}recalculatePlacement(stepConfig){let target=this.getStepTarget(stepConfig),widthContent=this.currentStepNode.width()+16,targetOffsetLeft=target.offset().left-10,targetOffsetRight=target.offset().left+target.width()+10,placement=stepConfig.placement;return-1!==["left","right"].indexOf(placement)&&targetOffsetLeftdocument.documentElement.clientWidth&&(placement="top"),placement}recalculateBackdropPosition(stepConfig){stepConfig.backdrop&&this.positionBackdrop(stepConfig)}positionBackdrop(stepConfig){if(stepConfig.backdrop){this.currentStepConfig.hasBackdrop=!0;let backdrop=(0,_jquery.default)('div[data-flexitour="backdrop"]');if(backdrop.length||(backdrop=(0,_jquery.default)('
'),(0,_jquery.default)("body").append(backdrop)),this.isStepActuallyVisible(stepConfig)){let targetNode=this.getStepTarget(stepConfig);targetNode.attr("data-flexitour","highlight");let distanceFromTop=targetNode[0].getBoundingClientRect().top,relativeTop=targetNode.offset().top-distanceFromTop;const viewportHeight=(0,_jquery.default)(window).height(),viewportWidth=(0,_jquery.default)(window).width(),elementWidth=targetNode.outerWidth()+20;let elementHeight=targetNode.outerHeight()+20;const elementLeft=targetNode.offset().left-10;let elementTop=targetNode.offset().top-10-relativeTop,navbarOverlap=0;if(targetNode.parents('[data-usertour="scroller"]').length){const navbarHeight=targetNode.parents('[data-usertour="scroller"]').offset().top;navbarOverlap=Math.max(Math.ceil(navbarHeight-elementTop),0),elementTop+=navbarOverlap,elementHeight-=navbarOverlap}if(this.currentStepNode&&this.currentStepNode.length){const xPlacement=this.currentStepNode[0].getAttribute("x-placement");this.currentStepNode[0].style.top="top-start"===xPlacement?"".concat(navbarOverlap,"px"):"0px"}const radius=10,bottomRight={x1:elementLeft+elementWidth-radius,y1:elementTop+elementHeight,x2:elementLeft+elementWidth,y2:elementTop+elementHeight-radius},topRight={x1:elementLeft+elementWidth,y1:elementTop+radius,x2:elementLeft+elementWidth-radius,y2:elementTop},topLeft={x1:elementLeft+radius,y1:elementTop,x2:elementLeft,y2:elementTop+radius},bottomLeft={x1:elementLeft,y1:elementTop+elementHeight-radius,x2:elementLeft+radius,y2:elementTop+elementHeight};document.querySelector('div[data-flexitour="backdrop"]').style.clipPath="path('M 0 0 L ".concat(viewportWidth," 0 L ").concat(viewportWidth," ").concat(viewportHeight," L 0 ").concat(viewportHeight," L 0 ").concat(elementTop+elementHeight," L ").concat(bottomRight.x1," ").concat(bottomRight.y1," C ").concat(bottomRight.x1," ").concat(bottomRight.y1," ").concat(bottomRight.x2," ").concat(bottomRight.y1," ").concat(bottomRight.x2," ").concat(bottomRight.y2," L ").concat(topRight.x1," ").concat(topRight.y1," C ").concat(topRight.x1," ").concat(topRight.y1," ").concat(topRight.x1," ").concat(topRight.y2," ").concat(topRight.x2," ").concat(topRight.y2," L ").concat(topLeft.x1," ").concat(topLeft.y1," C ").concat(topLeft.x1," ").concat(topLeft.y1," ").concat(topLeft.x2," ").concat(topLeft.y1," ").concat(topLeft.x2," ").concat(topLeft.y2," L ").concat(bottomLeft.x1," ").concat(bottomLeft.y1," C ").concat(bottomLeft.x1," ").concat(bottomLeft.y1," ").concat(bottomLeft.x1," ").concat(bottomLeft.y2," ").concat(bottomLeft.x2," ").concat(bottomLeft.y2," L 0 ").concat(elementTop+elementHeight," Z'\n )")}}return this}calculatePosition(elem){for(elem=(0,_jquery.default)(elem);elem.length&&elem[0]!==document;){let position=elem.css("position");if("static"!==position)return position;elem=elem.parent()}return null}accessibilityShow(){let hideFunction=function(child){let flexitourRole=child.data("flexitour");if(flexitourRole)switch(flexitourRole){case"container":case"target":return}child.attr("aria-hidden")||(child.attr("data-has-hidden",!0),Aria.hide(child))};this.currentStepNode.siblings().each((function(index,node){hideFunction((0,_jquery.default)(node))})),this.currentStepNode.parentsUntil("body").siblings().each((function(index,node){hideFunction((0,_jquery.default)(node))}))}accessibilityHide(){(0,_jquery.default)("[data-has-hidden]").each((function(index,node){var child;void 0!==(child=(0,_jquery.default)(node)).attr("data-has-hidden")&&(child.removeAttr("data-has-hidden"),Aria.unhide(child))}))}};return _exports.default=_default,_exports.default})); +define("tool_usertours/tour",["exports","jquery","core/aria","core/popper","core/event_dispatcher","./events","core/str","core/prefetch","core/event","core/pending"],(function(_exports,_jquery,Aria,_popper,_event_dispatcher,_events,_str,_prefetch,_event,_pending){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery),Aria=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Aria),_popper=_interopRequireDefault(_popper),_pending=_interopRequireDefault(_pending);var _default=class{constructor(config){_defineProperty(this,"tourRunning",!1),_defineProperty(this,"hasFixedPosition",(targetNode=>{let currentElement=targetNode[0];for(;currentElement;){if("fixed"===window.getComputedStyle(currentElement).position)return!0;currentElement=currentElement.parentElement}return!1})),this.init(config)}init(config){this.eventHandlers={},this.reset(),this.originalConfiguration=config||{},this.configure.apply(this,arguments),this.possitionNeedToBeRecalculated=!1,this.recalculatedNo=0;try{this.storage=window.sessionStorage,this.storageKey="tourstate_"+this.tourName}catch(e){this.storage=!1,this.storageKey=""}return(0,_prefetch.prefetchStrings)("tool_usertours",["nextstep_sequence","skip_tour"]),this}reset(){return this.hide(),this.eventHandlers=[],this.resetStepListeners(),this.originalConfiguration={},this.steps=[],this.currentStepNumber=0,this}configure(config){if("object"==typeof config){if(void 0!==config.tourName&&(this.tourName=config.tourName),config.eventHandlers)for(let eventName in config.eventHandlers)config.eventHandlers[eventName].forEach((function(handler){this.addEventHandler(eventName,handler)}),this);this.resetStepDefaults(!0),"object"==typeof config.steps&&(this.steps=config.steps),void 0!==config.template&&(this.templateContent=config.template)}return this.checkMinimumRequirements(),this}checkMinimumRequirements(){if(!this.tourName)throw new Error("Tour Name required");if(!this.steps||!this.steps.length)throw new Error("Steps must be specified")}resetStepDefaults(loadOriginalConfiguration){return void 0===loadOriginalConfiguration&&(loadOriginalConfiguration=!0),this.stepDefaults={},loadOriginalConfiguration&&void 0!==this.originalConfiguration.stepDefaults?this.setStepDefaults(this.originalConfiguration.stepDefaults):this.setStepDefaults({}),this}setStepDefaults(stepDefaults){return this.stepDefaults||(this.stepDefaults={}),_jquery.default.extend(this.stepDefaults,{element:"",placement:"top",delay:0,moveOnClick:!1,moveAfterTime:0,orphan:!1,direction:1},stepDefaults),this}getCurrentStepNumber(){return parseInt(this.currentStepNumber,10)}setCurrentStepNumber(stepNumber){if(this.currentStepNumber=stepNumber,this.storage)try{this.storage.setItem(this.storageKey,stepNumber)}catch(e){e.code===DOMException.QUOTA_EXCEEDED_ERR&&this.storage.removeItem(this.storageKey)}}getNextStepNumber(stepNumber){void 0===stepNumber&&(stepNumber=this.getCurrentStepNumber());let nextStepNumber=stepNumber+1;for(;nextStepNumber<=this.steps.length;){if(this.isStepPotentiallyVisible(this.getStepConfig(nextStepNumber)))return nextStepNumber;nextStepNumber++}return null}getPreviousStepNumber(stepNumber){void 0===stepNumber&&(stepNumber=this.getCurrentStepNumber());let previousStepNumber=stepNumber-1;for(;previousStepNumber>=0;){if(this.isStepPotentiallyVisible(this.getStepConfig(previousStepNumber)))return previousStepNumber;previousStepNumber--}return null}isLastStep(stepNumber){return null===this.getNextStepNumber(stepNumber)}isStepPotentiallyVisible(stepConfig){return!!stepConfig&&(!!this.isStepActuallyVisible(stepConfig)||(!(void 0===stepConfig.orphan||!stepConfig.orphan)||!(void 0===stepConfig.delay||!stepConfig.delay)))}getPotentiallyVisibleSteps(){let position=1,result=[];for(let stepNumber=0;stepNumber=this.steps.length)return null;let stepConfig=this.normalizeStepConfig(this.steps[stepNumber]);return stepConfig=_jquery.default.extend(stepConfig,{stepNumber:stepNumber}),stepConfig}normalizeStepConfig(stepConfig){return void 0!==stepConfig.reflex&&void 0===stepConfig.moveAfterClick&&(stepConfig.moveAfterClick=stepConfig.reflex),void 0!==stepConfig.element&&void 0===stepConfig.target&&(stepConfig.target=stepConfig.element),void 0!==stepConfig.content&&void 0===stepConfig.body&&(stepConfig.body=stepConfig.content),stepConfig=_jquery.default.extend({},this.stepDefaults,stepConfig),(stepConfig=_jquery.default.extend({},{attachTo:stepConfig.target,attachPoint:"after"},stepConfig)).attachTo&&(stepConfig.attachTo=(0,_jquery.default)(stepConfig.attachTo).first()),stepConfig}getStepTarget(stepConfig){return stepConfig.target?(0,_jquery.default)(stepConfig.target):null}dispatchEvent(eventName){let detail=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},cancelable=arguments.length>2&&void 0!==arguments[2]&&arguments[2];return(0,_event_dispatcher.dispatchEvent)(eventName,{tour:this,...detail},document,{cancelable:cancelable})}addEventHandler(eventName,handler){return void 0===this.eventHandlers[eventName]&&(this.eventHandlers[eventName]=[]),this.eventHandlers[eventName].push(handler),this}processStepListeners(stepConfig){if(this.listeners.push({node:this.currentStepNode,args:["click",'[data-role="next"]',_jquery.default.proxy(this.next,this)]},{node:this.currentStepNode,args:["click",'[data-role="end"]',_jquery.default.proxy(this.endTour,this)]},{node:(0,_jquery.default)('[data-flexitour="backdrop"]'),args:["click",_jquery.default.proxy(this.hide,this)]},{node:(0,_jquery.default)("body"),args:["keydown",_jquery.default.proxy(this.handleKeyDown,this)]}),stepConfig.moveOnClick){var targetNode=this.getStepTarget(stepConfig);this.listeners.push({node:targetNode,args:["click",_jquery.default.proxy((function(e){0===(0,_jquery.default)(e.target).parents('[data-flexitour="container"]').length&&window.setTimeout(_jquery.default.proxy(this.next,this),500)}),this)]})}return this.listeners.forEach((function(listener){listener.node.on.apply(listener.node,listener.args)})),this}resetStepListeners(){return this.listeners&&this.listeners.forEach((function(listener){listener.node.off.apply(listener.node,listener.args)})),this.listeners=[],this}renderStep(stepConfig){this.currentStepConfig=stepConfig,this.setCurrentStepNumber(stepConfig.stepNumber);let template=(0,_jquery.default)(this.getTemplateContent());template.find('[data-placeholder="title"]').html(stepConfig.title),template.find('[data-placeholder="body"]').html(stepConfig.body);const nextBtn=template.find('[data-role="next"]'),endBtn=template.find('[data-role="end"]');if(this.isLastStep(stepConfig.stepNumber)?(nextBtn.hide(),endBtn.removeClass("btn-secondary").addClass("btn-primary")):(nextBtn.prop("disabled",!1),(0,_str.getString)("skip_tour","tool_usertours").then((value=>{endBtn.html(value)})).catch()),nextBtn.attr("role","button"),endBtn.attr("role","button"),this.originalConfiguration.displaystepnumbers){const stepsPotentiallyVisible=this.getPotentiallyVisibleSteps(),totalStepsPotentiallyVisible=stepsPotentiallyVisible.length,position=stepsPotentiallyVisible[stepConfig.stepNumber].position;totalStepsPotentiallyVisible>1&&(0,_str.getString)("nextstep_sequence","tool_usertours",{position:position,total:totalStepsPotentiallyVisible}).then((value=>{nextBtn.html(value)})).catch()}return stepConfig.template=template,this.addStepToPage(stepConfig),this.processStepListeners(stepConfig),this}getTemplateContent(){return(0,_jquery.default)(this.templateContent).clone()}addStepToPage(stepConfig){let currentStepNode=(0,_jquery.default)('').html(stepConfig.template).hide();(0,_event.notifyFilterContentUpdated)(currentStepNode);let animationTarget=(0,_jquery.default)("body, html").stop(!0,!0);if(this.isStepActuallyVisible(stepConfig)){this.getStepTarget(stepConfig).data("flexitour","target"),(0,_jquery.default)(document.body).append(currentStepNode),this.currentStepNode=currentStepNode,this.currentStepNode.css({top:0,left:0});const pendingPromise=new _pending.default("tool_usertours/tour:addStepToPage-".concat(stepConfig.stepNumber));animationTarget.animate({scrollTop:this.calculateScrollTop(stepConfig)}).promise().then(function(){this.positionBackdrop(stepConfig),this.positionStep(stepConfig),this.revealStep(stepConfig),pendingPromise.resolve()}.bind(this)).catch((function(){}))}else stepConfig.orphan&&(stepConfig.isOrphan=!0,stepConfig.attachTo=(0,_jquery.default)("body").first(),stepConfig.attachPoint="append",this.positionBackdrop(stepConfig),currentStepNode.addClass("orphan"),(0,_jquery.default)(document.body).append(currentStepNode),this.currentStepNode=currentStepNode,this.currentStepNode.css("position","fixed"),this.currentStepPopper=new _popper.default((0,_jquery.default)("body"),this.currentStepNode[0],{removeOnDestroy:!0,placement:stepConfig.placement+"-start",arrowElement:'[data-role="arrow"]',modifiers:{hide:{enabled:!1},applyStyle:{onLoad:null,enabled:!1}},onCreate:()=>{const images=this.currentStepNode.find("img");images.length&&images.on("load",(()=>{this.calculateStepPositionInPage(currentStepNode)})),this.calculateStepPositionInPage(currentStepNode)}}),this.revealStep(stepConfig));return this}revealStep(stepConfig){const pendingPromise=new _pending.default("tool_usertours/tour:revealStep-".concat(stepConfig.stepNumber));return this.currentStepNode.fadeIn("",_jquery.default.proxy((function(){this.announceStep(stepConfig),this.currentStepNode.focus(),window.setTimeout(_jquery.default.proxy((function(){this.currentStepNode&&this.currentStepNode.focus(),pendingPromise.resolve()}),this),100)}),this)),this}announceStep(stepConfig){let stepId="tour-step-"+this.tourName+"-"+stepConfig.stepNumber;this.currentStepNode.attr("id",stepId);let bodyRegion=this.currentStepNode.find('[data-placeholder="body"]').first();bodyRegion.attr("id",stepId+"-body"),bodyRegion.attr("role","document");let headerRegion=this.currentStepNode.find('[data-placeholder="title"]').first();headerRegion.attr("id",stepId+"-title"),headerRegion.attr("aria-labelledby",stepId+"-body"),this.currentStepNode.attr("role","dialog"),this.currentStepNode.attr("tabindex",0),this.currentStepNode.attr("aria-labelledby",stepId+"-title"),this.currentStepNode.attr("aria-describedby",stepId+"-body");let target=this.getStepTarget(stepConfig);return target&&(target.data("original-tabindex",target.attr("tabindex")),target.attr("tabindex")||target.attr("tabindex",0),target.data("original-describedby",target.attr("aria-describedby")).attr("aria-describedby",stepId+"-body")),this.accessibilityShow(stepConfig),this}handleKeyDown(e){let tabbableSelector="a[href], link[href], [draggable=true], [contenteditable=true], ";switch(tabbableSelector+=":input:enabled, [tabindex], button:enabled",e.keyCode){case 27:this.endTour();break;case 9:(function(){if(!this.currentStepConfig.hasBackdrop)return;let currentIndex,nextIndex,nextNode,focusRelevant,activeElement=(0,_jquery.default)(document.activeElement),stepTarget=this.getStepTarget(this.currentStepConfig),tabbableNodes=(0,_jquery.default)(tabbableSelector),dialogContainer=(0,_jquery.default)('span[data-flexitour="container"]');if(stepTarget&&(tabbableNodes=tabbableNodes.filter((function(index,element){return null!==stepTarget&&(stepTarget.has(element).length||dialogContainer.has(element).length||stepTarget.is(element)||dialogContainer.is(element))}))),tabbableNodes.each((function(index,element){return!activeElement.is(element)||(currentIndex=index,!1)})),null!=currentIndex){let direction=1;e.shiftKey&&(direction=-1),nextIndex=currentIndex;do{nextIndex+=direction,nextNode=(0,_jquery.default)(tabbableNodes[nextIndex])}while(nextNode.length&&nextNode.is(":disabled")||nextNode.is(":hidden"));nextNode.length?(focusRelevant=nextNode.closest(stepTarget).length,focusRelevant=focusRelevant||nextNode.closest(this.currentStepNode).length):focusRelevant=!1}focusRelevant?nextNode.focus():e.shiftKey?this.currentStepNode.find(tabbableSelector).last().focus():this.currentStepConfig.isOrphan?this.currentStepNode.focus():stepTarget.focus(),e.preventDefault()}).call(this)}}startTour(startAt){if(this.storage&&void 0===startAt){let storageStartValue=this.storage.getItem(this.storageKey);if(storageStartValue){let storageStartAt=parseInt(storageStartValue,10);storageStartAt<=this.steps.length&&(startAt=storageStartAt)}}void 0===startAt&&(startAt=this.getCurrentStepNumber());return this.dispatchEvent(_events.eventTypes.tourStart,{startAt:startAt},!0).defaultPrevented||(this.gotoStep(startAt),this.tourRunning=!0,this.dispatchEvent(_events.eventTypes.tourStarted,{startAt:startAt})),this}restartTour(){return this.startTour(0)}endTour(){if(this.dispatchEvent(_events.eventTypes.tourEnd,{},!0).defaultPrevented)return this;if(this.currentStepConfig){let previousTarget=this.getStepTarget(this.currentStepConfig);previousTarget&&(previousTarget.attr("tabindex")||previousTarget.attr("tabindex","-1"),previousTarget.first().focus())}return this.hide(!0),this.tourRunning=!1,this.dispatchEvent(_events.eventTypes.tourEnded),this}hide(transition){if(this.dispatchEvent(_events.eventTypes.stepHide,{},!0).defaultPrevented)return this;const pendingPromise=new _pending.default("tool_usertours/tour:hide");if(this.currentStepNode&&this.currentStepNode.length&&(this.currentStepNode.hide(),this.currentStepPopper&&this.currentStepPopper.destroy()),this.currentStepConfig){let target=this.getStepTarget(this.currentStepConfig);target&&(target.data("original-labelledby")&&target.attr("aria-labelledby",target.data("original-labelledby")),target.data("original-describedby")&&target.attr("aria-describedby",target.data("original-describedby")),target.data("original-tabindex")?target.attr("tabindex",target.data("tabindex")):window.setTimeout((()=>{target.removeAttr("tabindex")}),400)),this.currentStepConfig=null}(0,_jquery.default)('[data-flexitour="highlight"]').removeAttr("data-flexitour");const backdrop=(0,_jquery.default)('[data-flexitour="backdrop"]');if(backdrop.length)if(transition){const backdropRemovalPromise=new _pending.default("tool_usertours/tour:hide:backdrop");backdrop.fadeOut(400,(function(){(0,_jquery.default)(this).remove(),backdropRemovalPromise.resolve()}))}else backdrop.remove();if(this.currentStepNode&&this.currentStepNode.length){let stepId=this.currentStepNode.attr("id");if(stepId){let currentStepElement='[aria-describedby="'+stepId+'-body"]';(0,_jquery.default)(currentStepElement).removeAttr("tabindex"),(0,_jquery.default)(currentStepElement).removeAttr("aria-describedby")}}return this.resetStepListeners(),this.accessibilityHide(),this.dispatchEvent(_events.eventTypes.stepHidden),this.currentStepNode=null,this.currentStepPopper=null,pendingPromise.resolve(),this}show(){let startAt=this.getCurrentStepNumber();return this.gotoStep(startAt)}getStepContainer(){return(0,_jquery.default)(this.currentStepNode)}calculateScrollTop(stepConfig){let viewportHeight=(0,_jquery.default)(window).height(),targetNode=this.getStepTarget(stepConfig),scrollParent=(0,_jquery.default)(window);targetNode.parents('[data-usertour="scroller"]').length&&(scrollParent=targetNode.parents('[data-usertour="scroller"]'));let scrollTop=scrollParent.scrollTop();return this.hasFixedPosition(targetNode)||(scrollTop="top"===stepConfig.placement?targetNode.offset().top-viewportHeight/2:"bottom"===stepConfig.placement?targetNode.offset().top+targetNode.height()+scrollTop-viewportHeight/2:targetNode.height()<=.8*viewportHeight?targetNode.offset().top-(viewportHeight-targetNode.height())/2:targetNode.offset().top-.2*viewportHeight),scrollTop=Math.max(0,scrollTop),scrollTop=Math.min((0,_jquery.default)(document).height()-viewportHeight,scrollTop),Math.ceil(scrollTop)}calculateStepPositionInPage(currentStepNode){let top=10;const viewportHeight=(0,_jquery.default)(window).height(),stepHeight=currentStepNode.height(),viewportWidth=(0,_jquery.default)(window).width(),stepWidth=currentStepNode.width();if(viewportHeight>=stepHeight+20)top=Math.ceil((viewportHeight-stepHeight)/2);else{var _currentStepNode$find,_currentStepNode$find2;const maxHeight=viewportHeight-20-(null!==(_currentStepNode$find=currentStepNode.find(".modal-header").first().outerHeight())&&void 0!==_currentStepNode$find?_currentStepNode$find:0)-(null!==(_currentStepNode$find2=currentStepNode.find(".modal-footer").first().outerHeight())&&void 0!==_currentStepNode$find2?_currentStepNode$find2:0);currentStepNode.find('[data-placeholder="body"]').first().css({"max-height":maxHeight+"px",overflow:"auto"})}currentStepNode.offset({top:top,left:Math.ceil((viewportWidth-stepWidth)/2)})}positionStep(stepConfig){let flipBehavior,content=this.currentStepNode,thisT=this;if(!content||!content.length)return this;switch(stepConfig.placement=this.recalculatePlacement(stepConfig),stepConfig.placement){case"left":flipBehavior=["left","right","top","bottom"];break;case"right":flipBehavior=["right","left","top","bottom"];break;case"top":flipBehavior=["top","bottom","right","left"];break;case"bottom":flipBehavior=["bottom","top","right","left"];break;default:flipBehavior="flip"}let offset="0";stepConfig.backdrop&&(offset="-".concat(10,", ").concat(10));let target=this.getStepTarget(stepConfig);var config={placement:stepConfig.placement+"-start",removeOnDestroy:!0,modifiers:{flip:{behaviour:flipBehavior},arrow:{element:'[data-role="arrow"]'},offset:{offset:offset}},onCreate:function(data){recalculateArrowPosition(data),recalculateStepPosition(data)},onUpdate:function(data){recalculateArrowPosition(data),thisT.possitionNeedToBeRecalculated&&(thisT.recalculatedNo++,thisT.possitionNeedToBeRecalculated=!1,recalculateStepPosition(data)),thisT.recalculateBackdropPosition(stepConfig)}};let recalculateArrowPosition=function(data){let placement=data.placement.split("-")[0];const isVertical=-1!==["left","right"].indexOf(placement),arrowElement=data.instance.popper.querySelector('[data-role="arrow"]'),stepElement=(0,_jquery.default)(data.instance.popper.querySelector('[data-role="flexitour-step"]'));if(isVertical){let arrowHeight=parseFloat(window.getComputedStyle(arrowElement).height),arrowOffset=parseFloat(window.getComputedStyle(arrowElement).top),popperHeight=parseFloat(window.getComputedStyle(data.instance.popper).height),popperOffset=parseFloat(window.getComputedStyle(data.instance.popper).top),popperBorderWidth=parseFloat(stepElement.css("borderTopWidth")),popperBorderRadiusWidth=2*parseFloat(stepElement.css("borderTopLeftRadius")),arrowPos=arrowOffset+arrowHeight/2,maxPos=popperHeight+popperOffset-popperBorderWidth-popperBorderRadiusWidth,minPos=popperOffset+popperBorderWidth+popperBorderRadiusWidth;if(arrowPos>=maxPos||arrowPos<=minPos){let newArrowPos=0;newArrowPos=arrowPos>popperHeight/2?maxPos-arrowHeight:minPos+arrowHeight,(0,_jquery.default)(arrowElement).css("top",newArrowPos)}}else{let arrowWidth=parseFloat(window.getComputedStyle(arrowElement).width),arrowOffset=parseFloat(window.getComputedStyle(arrowElement).left),popperWidth=parseFloat(window.getComputedStyle(data.instance.popper).width),popperOffset=parseFloat(window.getComputedStyle(data.instance.popper).left),popperBorderWidth=parseFloat(stepElement.css("borderTopWidth")),popperBorderRadiusWidth=2*parseFloat(stepElement.css("borderTopLeftRadius")),arrowPos=arrowOffset+arrowWidth/2,maxPos=popperWidth+popperOffset-popperBorderWidth-popperBorderRadiusWidth,minPos=popperOffset+popperBorderWidth+popperBorderRadiusWidth;if(arrowPos>=maxPos||arrowPos<=minPos){let newArrowPos=0;newArrowPos=arrowPos>popperWidth/2?maxPos-arrowWidth:minPos+arrowWidth,(0,_jquery.default)(arrowElement).css("left",newArrowPos)}}};const recalculateStepPosition=function(data){var _headerEle$outerHeigh,_footerEle$outerHeigh;const placement=data.placement.split("-")[0],isVertical=-1!==["left","right"].indexOf(placement),popperElement=(0,_jquery.default)(data.instance.popper),targetElement=(0,_jquery.default)(data.instance.reference),arrowElement=popperElement.find('[data-role="arrow"]'),stepElement=popperElement.find('[data-role="flexitour-step"]'),viewportHeight=(0,_jquery.default)(window).height(),viewportWidth=(0,_jquery.default)(window).width(),arrowHeight=parseFloat(arrowElement.outerHeight(!0)),popperHeight=parseFloat(popperElement.outerHeight(!0)),targetHeight=parseFloat(targetElement.outerHeight(!0)),arrowWidth=parseFloat(arrowElement.outerWidth(!0)),popperWidth=parseFloat(popperElement.outerWidth(!0)),targetWidth=parseFloat(targetElement.outerWidth(!0));let maxHeight;if(thisT.recalculatedNo>1&&(thisT.currentStepPopper.options.placement=isVertical?"auto-left":"auto-bottom"),thisT.recalculatedNo>2)return;if(isVertical){const leftSpace=targetElement.offset().left>0?targetElement.offset().left:0,rightSpace=viewportWidth-leftSpace-targetWidth,remainingSpace=leftSpace>=rightSpace?leftSpace:rightSpace;if(maxHeight=viewportHeight-20,remainingSpace0&&(popperElement.css({"max-width":maxWidth+"px"}),thisT.possitionNeedToBeRecalculated=!0)}else maxHeight0?targetElement.offset().top:0,bottomSpace=viewportHeight-topSpace-targetHeight,remainingSpace=topSpace>=bottomSpace?topSpace:bottomSpace;maxHeight=remainingSpace-10-arrowHeight,remainingSpace0?(headerEle.removeClass("minimal"),footerEle.removeClass("minimal"),currentStepBody.css({"max-height":maxHeight+"px",overflow:"auto"})):(headerEle.addClass("minimal"),footerEle.addClass("minimal")),thisT.currentStepPopper.update()};let background=(0,_jquery.default)('[data-flexitour="highlight"]');return background.length&&(target=background),this.currentStepPopper=new _popper.default(target,content[0],config),this}recalculatePlacement(stepConfig){let target=this.getStepTarget(stepConfig),widthContent=this.currentStepNode.width()+16,targetOffsetLeft=target.offset().left-10,targetOffsetRight=target.offset().left+target.width()+10,placement=stepConfig.placement;return-1!==["left","right"].indexOf(placement)&&targetOffsetLeftdocument.documentElement.clientWidth&&(placement="top"),placement}recalculateBackdropPosition(stepConfig){stepConfig.backdrop&&this.positionBackdrop(stepConfig)}positionBackdrop(stepConfig){if(stepConfig.backdrop){this.currentStepConfig.hasBackdrop=!0;let backdrop=(0,_jquery.default)('div[data-flexitour="backdrop"]');if(backdrop.length||(backdrop=(0,_jquery.default)('
'),(0,_jquery.default)("body").append(backdrop)),this.isStepActuallyVisible(stepConfig)){let targetNode=this.getStepTarget(stepConfig);targetNode.attr("data-flexitour","highlight");let distanceFromTop=targetNode[0].getBoundingClientRect().top,relativeTop=targetNode.offset().top-distanceFromTop;const viewportHeight=(0,_jquery.default)(window).height(),viewportWidth=(0,_jquery.default)(window).width(),elementWidth=targetNode.outerWidth()+20;let elementHeight=targetNode.outerHeight()+20;const elementLeft=targetNode.offset().left-10;let elementTop=targetNode.offset().top-10-relativeTop,navbarOverlap=0;if(targetNode.parents('[data-usertour="scroller"]').length){const navbarHeight=targetNode.parents('[data-usertour="scroller"]').offset().top;navbarOverlap=Math.max(Math.ceil(navbarHeight-elementTop),0),elementTop+=navbarOverlap,elementHeight-=navbarOverlap}if(this.currentStepNode&&this.currentStepNode.length){const xPlacement=this.currentStepNode[0].getAttribute("x-placement");this.currentStepNode[0].style.top="top-start"===xPlacement?"".concat(navbarOverlap,"px"):"0px"}const radius=10,bottomRight={x1:elementLeft+elementWidth-radius,y1:elementTop+elementHeight,x2:elementLeft+elementWidth,y2:elementTop+elementHeight-radius},topRight={x1:elementLeft+elementWidth,y1:elementTop+radius,x2:elementLeft+elementWidth-radius,y2:elementTop},topLeft={x1:elementLeft+radius,y1:elementTop,x2:elementLeft,y2:elementTop+radius},bottomLeft={x1:elementLeft,y1:elementTop+elementHeight-radius,x2:elementLeft+radius,y2:elementTop+elementHeight};document.querySelector('div[data-flexitour="backdrop"]').style.clipPath="path('M 0 0 L ".concat(viewportWidth," 0 L ").concat(viewportWidth," ").concat(viewportHeight," L 0 ").concat(viewportHeight," L 0 ").concat(elementTop+elementHeight," L ").concat(bottomRight.x1," ").concat(bottomRight.y1," C ").concat(bottomRight.x1," ").concat(bottomRight.y1," ").concat(bottomRight.x2," ").concat(bottomRight.y1," ").concat(bottomRight.x2," ").concat(bottomRight.y2," L ").concat(topRight.x1," ").concat(topRight.y1," C ").concat(topRight.x1," ").concat(topRight.y1," ").concat(topRight.x1," ").concat(topRight.y2," ").concat(topRight.x2," ").concat(topRight.y2," L ").concat(topLeft.x1," ").concat(topLeft.y1," C ").concat(topLeft.x1," ").concat(topLeft.y1," ").concat(topLeft.x2," ").concat(topLeft.y1," ").concat(topLeft.x2," ").concat(topLeft.y2," L ").concat(bottomLeft.x1," ").concat(bottomLeft.y1," C ").concat(bottomLeft.x1," ").concat(bottomLeft.y1," ").concat(bottomLeft.x1," ").concat(bottomLeft.y2," ").concat(bottomLeft.x2," ").concat(bottomLeft.y2," L 0 ").concat(elementTop+elementHeight," Z'\n )")}}return this}calculatePosition(elem){for(elem=(0,_jquery.default)(elem);elem.length&&elem[0]!==document;){let position=elem.css("position");if("static"!==position)return position;elem=elem.parent()}return null}accessibilityShow(){let hideFunction=function(child){let flexitourRole=child.data("flexitour");if(flexitourRole)switch(flexitourRole){case"container":case"target":return}child.attr("aria-hidden")||(child.attr("data-has-hidden",!0),Aria.hide(child))};this.currentStepNode.siblings().each((function(index,node){hideFunction((0,_jquery.default)(node))})),this.currentStepNode.parentsUntil("body").siblings().each((function(index,node){hideFunction((0,_jquery.default)(node))}))}accessibilityHide(){(0,_jquery.default)("[data-has-hidden]").each((function(index,node){var child;void 0!==(child=(0,_jquery.default)(node)).attr("data-has-hidden")&&(child.removeAttr("data-has-hidden"),Aria.unhide(child))}))}};return _exports.default=_default,_exports.default})); //# sourceMappingURL=tour.min.js.map \ No newline at end of file diff --git a/public/admin/tool/usertours/amd/build/tour.min.js.map b/public/admin/tool/usertours/amd/build/tour.min.js.map index b78faff3f640e..03d63fe307a23 100644 --- a/public/admin/tool/usertours/amd/build/tour.min.js.map +++ b/public/admin/tool/usertours/amd/build/tour.min.js.map @@ -1 +1 @@ -{"version":3,"file":"tour.min.js","sources":["../src/tour.js"],"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 user tour.\n *\n * @module tool_usertours/tour\n * @copyright 2018 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * A list of steps.\n *\n * @typedef {Object[]} StepList\n * @property {Number} stepId The id of the step in the database\n * @property {Number} position The position of the step within the tour (zero-indexed)\n */\n\nimport $ from 'jquery';\nimport * as Aria from 'core/aria';\nimport Popper from 'core/popper';\nimport {dispatchEvent} from 'core/event_dispatcher';\nimport {eventTypes} from './events';\nimport {getString} from 'core/str';\nimport {prefetchStrings} from 'core/prefetch';\nimport {notifyFilterContentUpdated} from 'core/event';\nimport PendingPromise from 'core/pending';\n\n/**\n * The minimum spacing for tour step to display.\n *\n * @private\n * @constant\n * @type {number}\n */\nconst MINSPACING = 10;\nconst BUFFER = 10;\n\n/**\n * A user tour.\n *\n * @class tool_usertours/tour\n * @property {boolean} tourRunning Whether the tour is currently running.\n */\nconst Tour = class {\n tourRunning = false;\n\n /**\n * @param {object} config The configuration object.\n */\n constructor(config) {\n this.init(config);\n }\n\n /**\n * Initialise the tour.\n *\n * @method init\n * @param {Object} config The configuration object.\n * @chainable\n * @return {Object} this.\n */\n init(config) {\n // Unset all handlers.\n this.eventHandlers = {};\n\n // Reset the current tour states.\n this.reset();\n\n // Store the initial configuration.\n this.originalConfiguration = config || {};\n\n // Apply configuration.\n this.configure.apply(this, arguments);\n\n // Unset recalculate state.\n this.possitionNeedToBeRecalculated = false;\n\n // Unset recalculate count.\n this.recalculatedNo = 0;\n\n try {\n this.storage = window.sessionStorage;\n this.storageKey = 'tourstate_' + this.tourName;\n } catch (e) {\n this.storage = false;\n this.storageKey = '';\n }\n\n prefetchStrings('tool_usertours', [\n 'nextstep_sequence',\n 'skip_tour'\n ]);\n\n return this;\n }\n\n /**\n * Reset the current tour state.\n *\n * @method reset\n * @chainable\n * @return {Object} this.\n */\n reset() {\n // Hide the current step.\n this.hide();\n\n // Unset all handlers.\n this.eventHandlers = [];\n\n // Unset all listeners.\n this.resetStepListeners();\n\n // Unset the original configuration.\n this.originalConfiguration = {};\n\n // Reset the current step number and list of steps.\n this.steps = [];\n\n // Reset the current step number.\n this.currentStepNumber = 0;\n\n return this;\n }\n\n /**\n * Prepare tour configuration.\n *\n * @method configure\n * @param {Object} config The configuration object.\n * @chainable\n * @return {Object} this.\n */\n configure(config) {\n if (typeof config === 'object') {\n // Tour name.\n if (typeof config.tourName !== 'undefined') {\n this.tourName = config.tourName;\n }\n\n // Set up eventHandlers.\n if (config.eventHandlers) {\n for (let eventName in config.eventHandlers) {\n config.eventHandlers[eventName].forEach(function(handler) {\n this.addEventHandler(eventName, handler);\n }, this);\n }\n }\n\n // Reset the step configuration.\n this.resetStepDefaults(true);\n\n // Configure the steps.\n if (typeof config.steps === 'object') {\n this.steps = config.steps;\n }\n\n if (typeof config.template !== 'undefined') {\n this.templateContent = config.template;\n }\n }\n\n // Check that we have enough to start the tour.\n this.checkMinimumRequirements();\n\n return this;\n }\n\n /**\n * Check that the configuration meets the minimum requirements.\n *\n * @method checkMinimumRequirements\n */\n checkMinimumRequirements() {\n // Need a tourName.\n if (!this.tourName) {\n throw new Error(\"Tour Name required\");\n }\n\n // Need a minimum of one step.\n if (!this.steps || !this.steps.length) {\n throw new Error(\"Steps must be specified\");\n }\n }\n\n /**\n * Reset step default configuration.\n *\n * @method resetStepDefaults\n * @param {Boolean} loadOriginalConfiguration Whether to load the original configuration supplied with the Tour.\n * @chainable\n * @return {Object} this.\n */\n resetStepDefaults(loadOriginalConfiguration) {\n if (typeof loadOriginalConfiguration === 'undefined') {\n loadOriginalConfiguration = true;\n }\n\n this.stepDefaults = {};\n if (!loadOriginalConfiguration || typeof this.originalConfiguration.stepDefaults === 'undefined') {\n this.setStepDefaults({});\n } else {\n this.setStepDefaults(this.originalConfiguration.stepDefaults);\n }\n\n return this;\n }\n\n /**\n * Set the step defaults.\n *\n * @method setStepDefaults\n * @param {Object} stepDefaults The step defaults to apply to all steps\n * @chainable\n * @return {Object} this.\n */\n setStepDefaults(stepDefaults) {\n if (!this.stepDefaults) {\n this.stepDefaults = {};\n }\n $.extend(\n this.stepDefaults,\n {\n element: '',\n placement: 'top',\n delay: 0,\n moveOnClick: false,\n moveAfterTime: 0,\n orphan: false,\n direction: 1,\n },\n stepDefaults\n );\n\n return this;\n }\n\n /**\n * Retrieve the current step number.\n *\n * @method getCurrentStepNumber\n * @return {Number} The current step number\n */\n getCurrentStepNumber() {\n return parseInt(this.currentStepNumber, 10);\n }\n\n /**\n * Store the current step number.\n *\n * @method setCurrentStepNumber\n * @param {Number} stepNumber The current step number\n * @chainable\n */\n setCurrentStepNumber(stepNumber) {\n this.currentStepNumber = stepNumber;\n if (this.storage) {\n try {\n this.storage.setItem(this.storageKey, stepNumber);\n } catch (e) {\n if (e.code === DOMException.QUOTA_EXCEEDED_ERR) {\n this.storage.removeItem(this.storageKey);\n }\n }\n }\n }\n\n /**\n * Get the next step number after the currently displayed step.\n *\n * @method getNextStepNumber\n * @param {Number} stepNumber The current step number\n * @return {Number} The next step number to display\n */\n getNextStepNumber(stepNumber) {\n if (typeof stepNumber === 'undefined') {\n stepNumber = this.getCurrentStepNumber();\n }\n let nextStepNumber = stepNumber + 1;\n\n // Keep checking the remaining steps.\n while (nextStepNumber <= this.steps.length) {\n if (this.isStepPotentiallyVisible(this.getStepConfig(nextStepNumber))) {\n return nextStepNumber;\n }\n nextStepNumber++;\n }\n\n return null;\n }\n\n /**\n * Get the previous step number before the currently displayed step.\n *\n * @method getPreviousStepNumber\n * @param {Number} stepNumber The current step number\n * @return {Number} The previous step number to display\n */\n getPreviousStepNumber(stepNumber) {\n if (typeof stepNumber === 'undefined') {\n stepNumber = this.getCurrentStepNumber();\n }\n let previousStepNumber = stepNumber - 1;\n\n // Keep checking the remaining steps.\n while (previousStepNumber >= 0) {\n if (this.isStepPotentiallyVisible(this.getStepConfig(previousStepNumber))) {\n return previousStepNumber;\n }\n previousStepNumber--;\n }\n\n return null;\n }\n\n /**\n * Is the step the final step number?\n *\n * @method isLastStep\n * @param {Number} stepNumber Step number to test\n * @return {Boolean} Whether the step is the final step\n */\n isLastStep(stepNumber) {\n let nextStepNumber = this.getNextStepNumber(stepNumber);\n\n return nextStepNumber === null;\n }\n\n /**\n * Is this step potentially visible?\n *\n * @method isStepPotentiallyVisible\n * @param {Object} stepConfig The step configuration to normalise\n * @return {Boolean} Whether the step is the potentially visible\n */\n isStepPotentiallyVisible(stepConfig) {\n if (!stepConfig) {\n // Without step config, there can be no step.\n return false;\n }\n\n if (this.isStepActuallyVisible(stepConfig)) {\n // If it is actually visible, it is already potentially visible.\n return true;\n }\n\n if (typeof stepConfig.orphan !== 'undefined' && stepConfig.orphan) {\n // Orphan steps have no target. They are always visible.\n return true;\n }\n\n if (typeof stepConfig.delay !== 'undefined' && stepConfig.delay) {\n // Only return true if the activated has not been used yet.\n return true;\n }\n\n // Not theoretically, or actually visible.\n return false;\n }\n\n /**\n * Get potentially visible steps in a tour.\n *\n * @returns {StepList} A list of ordered steps\n */\n getPotentiallyVisibleSteps() {\n let position = 1;\n let result = [];\n // Checking the total steps.\n for (let stepNumber = 0; stepNumber < this.steps.length; stepNumber++) {\n const stepConfig = this.getStepConfig(stepNumber);\n if (this.isStepPotentiallyVisible(stepConfig)) {\n result[stepNumber] = {stepId: stepConfig.stepid, position: position};\n position++;\n }\n }\n\n return result;\n }\n\n /**\n * Is this step actually visible?\n *\n * @method isStepActuallyVisible\n * @param {Object} stepConfig The step configuration to normalise\n * @return {Boolean} Whether the step is actually visible\n */\n isStepActuallyVisible(stepConfig) {\n if (!stepConfig) {\n // Without step config, there can be no step.\n return false;\n }\n\n // Check if the CSS styles are allowed on the browser or not.\n if (!this.isCSSAllowed()) {\n return false;\n }\n\n let target = this.getStepTarget(stepConfig);\n if (target && target.length && target.is(':visible')) {\n // Without a target, there can be no step.\n return !!target.length;\n }\n\n return false;\n }\n\n /**\n * Is the browser actually allow CSS styles?\n *\n * @returns {boolean} True if the browser is allowing CSS styles\n */\n isCSSAllowed() {\n const testCSSElement = document.createElement('div');\n testCSSElement.classList.add('hide');\n document.body.appendChild(testCSSElement);\n const styles = window.getComputedStyle(testCSSElement);\n const isAllowed = styles.display === 'none';\n testCSSElement.remove();\n\n return isAllowed;\n }\n\n /**\n * Go to the next step in the tour.\n *\n * @method next\n * @chainable\n * @return {Object} this.\n */\n next() {\n return this.gotoStep(this.getNextStepNumber());\n }\n\n /**\n * Go to the previous step in the tour.\n *\n * @method previous\n * @chainable\n * @return {Object} this.\n */\n previous() {\n return this.gotoStep(this.getPreviousStepNumber(), -1);\n }\n\n /**\n * Go to the specified step in the tour.\n *\n * @method gotoStep\n * @param {Number} stepNumber The step number to display\n * @param {Number} direction Next or previous step\n * @chainable\n * @return {Object} this.\n * @fires tool_usertours/stepRender\n * @fires tool_usertours/stepRendered\n * @fires tool_usertours/stepHide\n * @fires tool_usertours/stepHidden\n */\n gotoStep(stepNumber, direction) {\n if (stepNumber < 0) {\n return this.endTour();\n }\n\n let stepConfig = this.getStepConfig(stepNumber);\n if (stepConfig === null) {\n return this.endTour();\n }\n\n return this._gotoStep(stepConfig, direction);\n }\n\n _gotoStep(stepConfig, direction) {\n if (!stepConfig) {\n return this.endTour();\n }\n\n const pendingPromise = new PendingPromise(`tool_usertours/tour:_gotoStep-${stepConfig.stepNumber}`);\n\n if (typeof stepConfig.delay !== 'undefined' && stepConfig.delay && !stepConfig.delayed) {\n stepConfig.delayed = true;\n window.setTimeout(function(stepConfig, direction) {\n this._gotoStep(stepConfig, direction);\n pendingPromise.resolve();\n }, stepConfig.delay, stepConfig, direction);\n\n return this;\n } else if (!stepConfig.orphan && !this.isStepActuallyVisible(stepConfig)) {\n const fn = direction == -1 ? 'getPreviousStepNumber' : 'getNextStepNumber';\n this.gotoStep(this[fn](stepConfig.stepNumber), direction);\n\n pendingPromise.resolve();\n return this;\n }\n\n this.hide();\n\n const stepRenderEvent = this.dispatchEvent(eventTypes.stepRender, {stepConfig}, true);\n if (!stepRenderEvent.defaultPrevented) {\n this.renderStep(stepConfig);\n this.dispatchEvent(eventTypes.stepRendered, {stepConfig});\n }\n\n pendingPromise.resolve();\n return this;\n }\n\n /**\n * Fetch the normalised step configuration for the specified step number.\n *\n * @method getStepConfig\n * @param {Number} stepNumber The step number to fetch configuration for\n * @return {Object} The step configuration\n */\n getStepConfig(stepNumber) {\n if (stepNumber === null || stepNumber < 0 || stepNumber >= this.steps.length) {\n return null;\n }\n\n // Normalise the step configuration.\n let stepConfig = this.normalizeStepConfig(this.steps[stepNumber]);\n\n // Add the stepNumber to the stepConfig.\n stepConfig = $.extend(stepConfig, {stepNumber: stepNumber});\n\n return stepConfig;\n }\n\n /**\n * Normalise the supplied step configuration.\n *\n * @method normalizeStepConfig\n * @param {Object} stepConfig The step configuration to normalise\n * @return {Object} The normalised step configuration\n */\n normalizeStepConfig(stepConfig) {\n\n if (typeof stepConfig.reflex !== 'undefined' && typeof stepConfig.moveAfterClick === 'undefined') {\n stepConfig.moveAfterClick = stepConfig.reflex;\n }\n\n if (typeof stepConfig.element !== 'undefined' && typeof stepConfig.target === 'undefined') {\n stepConfig.target = stepConfig.element;\n }\n\n if (typeof stepConfig.content !== 'undefined' && typeof stepConfig.body === 'undefined') {\n stepConfig.body = stepConfig.content;\n }\n\n stepConfig = $.extend({}, this.stepDefaults, stepConfig);\n\n stepConfig = $.extend({}, {\n attachTo: stepConfig.target,\n attachPoint: 'after',\n }, stepConfig);\n\n if (stepConfig.attachTo) {\n stepConfig.attachTo = $(stepConfig.attachTo).first();\n }\n\n return stepConfig;\n }\n\n /**\n * Fetch the actual step target from the selector.\n *\n * This should not be called until after any delay has completed.\n *\n * @method getStepTarget\n * @param {Object} stepConfig The step configuration\n * @return {$}\n */\n getStepTarget(stepConfig) {\n if (stepConfig.target) {\n return $(stepConfig.target);\n }\n\n return null;\n }\n\n /**\n * Fire any event handlers for the specified event.\n *\n * @param {String} eventName The name of the event\n * @param {Object} [detail={}] Any additional details to pass into the eveent\n * @param {Boolean} [cancelable=false] Whether preventDefault() can be called\n * @returns {CustomEvent}\n */\n dispatchEvent(\n eventName,\n detail = {},\n cancelable = false\n ) {\n return dispatchEvent(eventName, {\n // Add the tour to the detail.\n tour: this,\n ...detail,\n }, document, {\n cancelable,\n });\n }\n\n /**\n * @method addEventHandler\n * @param {string} eventName The name of the event to listen for\n * @param {function} handler The event handler to call\n * @return {Object} this.\n */\n addEventHandler(eventName, handler) {\n if (typeof this.eventHandlers[eventName] === 'undefined') {\n this.eventHandlers[eventName] = [];\n }\n\n this.eventHandlers[eventName].push(handler);\n\n return this;\n }\n\n /**\n * Process listeners for the step being shown.\n *\n * @method processStepListeners\n * @param {object} stepConfig The configuration for the step\n * @chainable\n * @return {Object} this.\n */\n processStepListeners(stepConfig) {\n this.listeners.push(\n // Next button.\n {\n node: this.currentStepNode,\n args: ['click', '[data-role=\"next\"]', $.proxy(this.next, this)]\n },\n\n // Close and end tour buttons.\n {\n node: this.currentStepNode,\n args: ['click', '[data-role=\"end\"]', $.proxy(this.endTour, this)]\n },\n\n // Click backdrop and hide tour.\n {\n node: $('[data-flexitour=\"backdrop\"]'),\n args: ['click', $.proxy(this.hide, this)]\n },\n\n // Keypresses.\n {\n node: $('body'),\n args: ['keydown', $.proxy(this.handleKeyDown, this)]\n });\n\n if (stepConfig.moveOnClick) {\n var targetNode = this.getStepTarget(stepConfig);\n this.listeners.push({\n node: targetNode,\n args: ['click', $.proxy(function(e) {\n if ($(e.target).parents('[data-flexitour=\"container\"]').length === 0) {\n // Ignore clicks when they are in the flexitour.\n window.setTimeout($.proxy(this.next, this), 500);\n }\n }, this)]\n });\n }\n\n this.listeners.forEach(function(listener) {\n listener.node.on.apply(listener.node, listener.args);\n });\n\n return this;\n }\n\n /**\n * Reset step listeners.\n *\n * @method resetStepListeners\n * @chainable\n * @return {Object} this.\n */\n resetStepListeners() {\n // Stop listening to all external handlers.\n if (this.listeners) {\n this.listeners.forEach(function(listener) {\n listener.node.off.apply(listener.node, listener.args);\n });\n }\n this.listeners = [];\n\n return this;\n }\n\n /**\n * The standard step renderer.\n *\n * @method renderStep\n * @param {Object} stepConfig The step configuration of the step\n * @chainable\n * @return {Object} this.\n */\n renderStep(stepConfig) {\n // Store the current step configuration for later.\n this.currentStepConfig = stepConfig;\n this.setCurrentStepNumber(stepConfig.stepNumber);\n\n // Fetch the template and convert it to a $ object.\n let template = $(this.getTemplateContent());\n\n // Title.\n template.find('[data-placeholder=\"title\"]')\n .html(stepConfig.title);\n\n // Body.\n template.find('[data-placeholder=\"body\"]')\n .html(stepConfig.body);\n\n // Buttons.\n const nextBtn = template.find('[data-role=\"next\"]');\n const endBtn = template.find('[data-role=\"end\"]');\n\n // Is this the final step?\n if (this.isLastStep(stepConfig.stepNumber)) {\n nextBtn.hide();\n endBtn.removeClass(\"btn-secondary\").addClass(\"btn-primary\");\n } else {\n nextBtn.prop('disabled', false);\n // Use Skip tour label for the End tour button.\n getString('skip_tour', 'tool_usertours').then(value => {\n endBtn.html(value);\n return;\n }).catch();\n }\n\n nextBtn.attr('role', 'button');\n endBtn.attr('role', 'button');\n\n if (this.originalConfiguration.displaystepnumbers) {\n const stepsPotentiallyVisible = this.getPotentiallyVisibleSteps();\n const totalStepsPotentiallyVisible = stepsPotentiallyVisible.length;\n const position = stepsPotentiallyVisible[stepConfig.stepNumber].position;\n if (totalStepsPotentiallyVisible > 1) {\n // Change the label of the Next button to include the sequence.\n getString('nextstep_sequence', 'tool_usertours',\n {position: position, total: totalStepsPotentiallyVisible}).then(value => {\n nextBtn.html(value);\n return;\n }).catch();\n }\n }\n\n // Replace the template with the updated version.\n stepConfig.template = template;\n\n // Add to the page.\n this.addStepToPage(stepConfig);\n\n // Process step listeners after adding to the page.\n // This uses the currentNode.\n this.processStepListeners(stepConfig);\n\n return this;\n }\n\n /**\n * Getter for the template content.\n *\n * @method getTemplateContent\n * @return {$}\n */\n getTemplateContent() {\n return $(this.templateContent).clone();\n }\n\n /**\n * Helper to add a step to the page.\n *\n * @method addStepToPage\n * @param {Object} stepConfig The step configuration of the step\n * @chainable\n * @return {Object} this.\n */\n addStepToPage(stepConfig) {\n // Create the stepNode from the template data.\n let currentStepNode = $('')\n .html(stepConfig.template)\n .hide();\n // Trigger the Moodle filters.\n notifyFilterContentUpdated(currentStepNode);\n\n // The scroll animation occurs on the body or html.\n let animationTarget = $('body, html')\n .stop(true, true);\n\n if (this.isStepActuallyVisible(stepConfig)) {\n let targetNode = this.getStepTarget(stepConfig);\n\n targetNode.data('flexitour', 'target');\n\n // Add the backdrop.\n this.positionBackdrop(stepConfig);\n\n $(document.body).append(currentStepNode);\n this.currentStepNode = currentStepNode;\n\n // Ensure that the step node is positioned.\n // Some situations mean that the value is not properly calculated without this step.\n this.currentStepNode.css({\n top: 0,\n left: 0,\n });\n\n const pendingPromise = new PendingPromise(`tool_usertours/tour:addStepToPage-${stepConfig.stepNumber}`);\n animationTarget\n .animate({\n scrollTop: this.calculateScrollTop(stepConfig),\n }).promise().then(function() {\n this.positionStep(stepConfig);\n this.revealStep(stepConfig);\n pendingPromise.resolve();\n return;\n }.bind(this))\n .catch(function() {\n // Silently fail.\n });\n\n } else if (stepConfig.orphan) {\n stepConfig.isOrphan = true;\n\n // This will be appended to the body instead.\n stepConfig.attachTo = $('body').first();\n stepConfig.attachPoint = 'append';\n\n // Add the backdrop.\n this.positionBackdrop(stepConfig);\n\n // This is an orphaned step.\n currentStepNode.addClass('orphan');\n\n // It lives in the body.\n $(document.body).append(currentStepNode);\n this.currentStepNode = currentStepNode;\n\n this.currentStepNode.css('position', 'fixed');\n\n this.currentStepPopper = new Popper(\n $('body'),\n this.currentStepNode[0], {\n removeOnDestroy: true,\n placement: stepConfig.placement + '-start',\n arrowElement: '[data-role=\"arrow\"]',\n // Empty the modifiers. We've already placed the step and don't want it moved.\n modifiers: {\n hide: {\n enabled: false,\n },\n applyStyle: {\n onLoad: null,\n enabled: false,\n },\n },\n onCreate: () => {\n // First, we need to check if the step's content contains any images.\n const images = this.currentStepNode.find('img');\n if (images.length) {\n // Images found, need to calculate the position when the image is loaded.\n images.on('load', () => {\n this.calculateStepPositionInPage(currentStepNode);\n });\n }\n this.calculateStepPositionInPage(currentStepNode);\n }\n }\n );\n\n this.revealStep(stepConfig);\n }\n\n return this;\n }\n\n /**\n * Make the given step visible.\n *\n * @method revealStep\n * @param {Object} stepConfig The step configuration of the step\n * @chainable\n * @return {Object} this.\n */\n revealStep(stepConfig) {\n // Fade the step in.\n const pendingPromise = new PendingPromise(`tool_usertours/tour:revealStep-${stepConfig.stepNumber}`);\n this.currentStepNode.fadeIn('', $.proxy(function() {\n // Announce via ARIA.\n this.announceStep(stepConfig);\n\n // Focus on the current step Node.\n this.currentStepNode.focus();\n window.setTimeout($.proxy(function() {\n // After a brief delay, focus again.\n // There seems to be an issue with Jaws where it only reads the dialogue title initially.\n // This second focus helps it to read the full dialogue.\n if (this.currentStepNode) {\n this.currentStepNode.focus();\n }\n pendingPromise.resolve();\n }, this), 100);\n\n }, this));\n\n return this;\n }\n\n /**\n * Helper to announce the step on the page.\n *\n * @method announceStep\n * @param {Object} stepConfig The step configuration of the step\n * @chainable\n * @return {Object} this.\n */\n announceStep(stepConfig) {\n // Setup the step Dialogue as per:\n // * https://www.w3.org/TR/wai-aria-practices/#dialog_nonmodal\n // * https://www.w3.org/TR/wai-aria-practices/#dialog_modal\n\n // Generate an ID for the current step node.\n let stepId = 'tour-step-' + this.tourName + '-' + stepConfig.stepNumber;\n this.currentStepNode.attr('id', stepId);\n\n let bodyRegion = this.currentStepNode.find('[data-placeholder=\"body\"]').first();\n bodyRegion.attr('id', stepId + '-body');\n bodyRegion.attr('role', 'document');\n\n let headerRegion = this.currentStepNode.find('[data-placeholder=\"title\"]').first();\n headerRegion.attr('id', stepId + '-title');\n headerRegion.attr('aria-labelledby', stepId + '-body');\n\n // Generally, a modal dialog has a role of dialog.\n this.currentStepNode.attr('role', 'dialog');\n this.currentStepNode.attr('tabindex', 0);\n this.currentStepNode.attr('aria-labelledby', stepId + '-title');\n this.currentStepNode.attr('aria-describedby', stepId + '-body');\n\n // Configure ARIA attributes on the target.\n let target = this.getStepTarget(stepConfig);\n if (target) {\n target.data('original-tabindex', target.attr('tabindex'));\n if (!target.attr('tabindex')) {\n target.attr('tabindex', 0);\n }\n\n target\n .data('original-describedby', target.attr('aria-describedby'))\n .attr('aria-describedby', stepId + '-body')\n ;\n }\n\n this.accessibilityShow(stepConfig);\n\n return this;\n }\n\n /**\n * Handle key down events.\n *\n * @method handleKeyDown\n * @param {EventFacade} e\n */\n handleKeyDown(e) {\n let tabbableSelector = 'a[href], link[href], [draggable=true], [contenteditable=true], ';\n tabbableSelector += ':input:enabled, [tabindex], button:enabled';\n switch (e.keyCode) {\n case 27:\n this.endTour();\n break;\n\n // 9 == Tab - trap focus for items with a backdrop.\n case 9:\n // Tab must be handled on key up only in this instance.\n (function() {\n if (!this.currentStepConfig.hasBackdrop) {\n // Trapping tab focus is only handled for those steps with a backdrop.\n return;\n }\n\n // Find all tabbable locations.\n let activeElement = $(document.activeElement);\n let stepTarget = this.getStepTarget(this.currentStepConfig);\n let tabbableNodes = $(tabbableSelector);\n let dialogContainer = $('span[data-flexitour=\"container\"]');\n let currentIndex;\n // Filter out element which is not belong to target section or dialogue.\n if (stepTarget) {\n tabbableNodes = tabbableNodes.filter(function(index, element) {\n return stepTarget !== null\n && (stepTarget.has(element).length\n || dialogContainer.has(element).length\n || stepTarget.is(element)\n || dialogContainer.is(element));\n });\n }\n\n // Find index of focusing element.\n tabbableNodes.each(function(index, element) {\n if (activeElement.is(element)) {\n currentIndex = index;\n return false;\n }\n // Keep looping.\n return true;\n });\n\n let nextIndex;\n let nextNode;\n let focusRelevant;\n if (currentIndex != void 0) {\n let direction = 1;\n if (e.shiftKey) {\n direction = -1;\n }\n nextIndex = currentIndex;\n do {\n nextIndex += direction;\n nextNode = $(tabbableNodes[nextIndex]);\n } while (nextNode.length && nextNode.is(':disabled') || nextNode.is(':hidden'));\n if (nextNode.length) {\n // A new f\n focusRelevant = nextNode.closest(stepTarget).length;\n focusRelevant = focusRelevant || nextNode.closest(this.currentStepNode).length;\n } else {\n // Unable to find the target somehow.\n focusRelevant = false;\n }\n }\n\n if (focusRelevant) {\n nextNode.focus();\n } else {\n if (e.shiftKey) {\n // Focus on the last tabbable node in the step.\n this.currentStepNode.find(tabbableSelector).last().focus();\n } else {\n if (this.currentStepConfig.isOrphan) {\n // Focus on the step - there is no target.\n this.currentStepNode.focus();\n } else {\n // Focus on the step target.\n stepTarget.focus();\n }\n }\n }\n e.preventDefault();\n }).call(this);\n break;\n }\n }\n\n /**\n * Start the current tour.\n *\n * @method startTour\n * @param {Number} startAt Which step number to start at. If not specified, starts at the last point.\n * @chainable\n * @return {Object} this.\n * @fires tool_usertours/tourStart\n * @fires tool_usertours/tourStarted\n */\n startTour(startAt) {\n if (this.storage && typeof startAt === 'undefined') {\n let storageStartValue = this.storage.getItem(this.storageKey);\n if (storageStartValue) {\n let storageStartAt = parseInt(storageStartValue, 10);\n if (storageStartAt <= this.steps.length) {\n startAt = storageStartAt;\n }\n }\n }\n\n if (typeof startAt === 'undefined') {\n startAt = this.getCurrentStepNumber();\n }\n\n const tourStartEvent = this.dispatchEvent(eventTypes.tourStart, {startAt}, true);\n if (!tourStartEvent.defaultPrevented) {\n this.gotoStep(startAt);\n this.tourRunning = true;\n this.dispatchEvent(eventTypes.tourStarted, {startAt});\n }\n\n return this;\n }\n\n /**\n * Restart the tour from the beginning, resetting the completionlag.\n *\n * @method restartTour\n * @chainable\n * @return {Object} this.\n */\n restartTour() {\n return this.startTour(0);\n }\n\n /**\n * End the current tour.\n *\n * @method endTour\n * @chainable\n * @return {Object} this.\n * @fires tool_usertours/tourEnd\n * @fires tool_usertours/tourEnded\n */\n endTour() {\n const tourEndEvent = this.dispatchEvent(eventTypes.tourEnd, {}, true);\n if (tourEndEvent.defaultPrevented) {\n return this;\n }\n\n if (this.currentStepConfig) {\n let previousTarget = this.getStepTarget(this.currentStepConfig);\n if (previousTarget) {\n if (!previousTarget.attr('tabindex')) {\n previousTarget.attr('tabindex', '-1');\n }\n previousTarget.first().focus();\n }\n }\n\n this.hide(true);\n\n this.tourRunning = false;\n this.dispatchEvent(eventTypes.tourEnded);\n\n return this;\n }\n\n /**\n * Hide any currently visible steps.\n *\n * @method hide\n * @param {Bool} transition Animate the visibility change\n * @chainable\n * @return {Object} this.\n * @fires tool_usertours/stepHide\n * @fires tool_usertours/stepHidden\n */\n hide(transition) {\n const stepHideEvent = this.dispatchEvent(eventTypes.stepHide, {}, true);\n if (stepHideEvent.defaultPrevented) {\n return this;\n }\n\n const pendingPromise = new PendingPromise('tool_usertours/tour:hide');\n if (this.currentStepNode && this.currentStepNode.length) {\n this.currentStepNode.hide();\n if (this.currentStepPopper) {\n this.currentStepPopper.destroy();\n }\n }\n\n // Restore original target configuration.\n if (this.currentStepConfig) {\n let target = this.getStepTarget(this.currentStepConfig);\n if (target) {\n if (target.data('original-labelledby')) {\n target.attr('aria-labelledby', target.data('original-labelledby'));\n }\n\n if (target.data('original-describedby')) {\n target.attr('aria-describedby', target.data('original-describedby'));\n }\n\n if (target.data('original-tabindex')) {\n target.attr('tabindex', target.data('tabindex'));\n } else {\n // If the target does not have the tabindex attribute at the beginning. We need to remove it.\n // We should wait a little here before removing the attribute to prevent the browser from adding it again.\n window.setTimeout(() => {\n target.removeAttr('tabindex');\n }, 400);\n }\n }\n\n // Clear the step configuration.\n this.currentStepConfig = null;\n }\n\n // Remove the highlight attribute when the hide occurs.\n $('[data-flexitour=\"highlight\"]').removeAttr('data-flexitour');\n\n const backdrop = $('[data-flexitour=\"backdrop\"]');\n if (backdrop.length) {\n if (transition) {\n const backdropRemovalPromise = new PendingPromise('tool_usertours/tour:hide:backdrop');\n backdrop.fadeOut(400, function() {\n $(this).remove();\n backdropRemovalPromise.resolve();\n });\n } else {\n backdrop.remove();\n }\n }\n\n // Remove aria-describedby and tabindex attributes.\n if (this.currentStepNode && this.currentStepNode.length) {\n let stepId = this.currentStepNode.attr('id');\n if (stepId) {\n let currentStepElement = '[aria-describedby=\"' + stepId + '-body\"]';\n $(currentStepElement).removeAttr('tabindex');\n $(currentStepElement).removeAttr('aria-describedby');\n }\n }\n\n // Reset the listeners.\n this.resetStepListeners();\n\n this.accessibilityHide();\n\n this.dispatchEvent(eventTypes.stepHidden);\n\n this.currentStepNode = null;\n this.currentStepPopper = null;\n\n pendingPromise.resolve();\n return this;\n }\n\n /**\n * Show the current steps.\n *\n * @method show\n * @chainable\n * @return {Object} this.\n */\n show() {\n // Show the current step.\n let startAt = this.getCurrentStepNumber();\n\n return this.gotoStep(startAt);\n }\n\n /**\n * Return the current step node.\n *\n * @method getStepContainer\n * @return {jQuery}\n */\n getStepContainer() {\n return $(this.currentStepNode);\n }\n\n /**\n * Check whether the target node has a fixed position, or is nested within one.\n *\n * @param {Object} targetNode The target element to check.\n * @return {Boolean} Return true if fixed position found.\n */\n hasFixedPosition = (targetNode) => {\n let currentElement = targetNode[0];\n while (currentElement) {\n const computedStyle = window.getComputedStyle(currentElement);\n if (computedStyle.position === 'fixed') {\n return true;\n }\n currentElement = currentElement.parentElement;\n }\n\n return false;\n };\n\n /**\n * Calculate scrollTop.\n *\n * @method calculateScrollTop\n * @param {Object} stepConfig The step configuration of the step\n * @return {Number}\n */\n calculateScrollTop(stepConfig) {\n let viewportHeight = $(window).height();\n let targetNode = this.getStepTarget(stepConfig);\n\n let scrollParent = $(window);\n if (targetNode.parents('[data-usertour=\"scroller\"]').length) {\n scrollParent = targetNode.parents('[data-usertour=\"scroller\"]');\n }\n let scrollTop = scrollParent.scrollTop();\n\n if (this.hasFixedPosition(targetNode)) {\n // Target must be in a fixed or custom position. No need to modify the scrollTop.\n } else if (stepConfig.placement === 'top') {\n // If the placement is top, center scroll at the top of the target.\n scrollTop = targetNode.offset().top - (viewportHeight / 2);\n } else if (stepConfig.placement === 'bottom') {\n // If the placement is bottom, center scroll at the bottom of the target.\n scrollTop = targetNode.offset().top + targetNode.height() + scrollTop - (viewportHeight / 2);\n } else if (targetNode.height() <= (viewportHeight * 0.8)) {\n // If the placement is left/right, and the target fits in the viewport, centre screen on the target\n scrollTop = targetNode.offset().top - ((viewportHeight - targetNode.height()) / 2);\n } else {\n // If the placement is left/right, and the target is bigger than the viewport, set scrollTop to target.top + buffer\n // and change step attachmentTarget to top+.\n scrollTop = targetNode.offset().top - (viewportHeight * 0.2);\n }\n\n // Never scroll over the top.\n scrollTop = Math.max(0, scrollTop);\n\n // Never scroll beyond the bottom.\n scrollTop = Math.min($(document).height() - viewportHeight, scrollTop);\n\n return Math.ceil(scrollTop);\n }\n\n /**\n * Calculate dialogue position for page middle.\n *\n * @param {jQuery} currentStepNode Current step node\n * @method calculateScrollTop\n */\n calculateStepPositionInPage(currentStepNode) {\n let top = MINSPACING;\n const viewportHeight = $(window).height();\n const stepHeight = currentStepNode.height();\n const viewportWidth = $(window).width();\n const stepWidth = currentStepNode.width();\n if (viewportHeight >= (stepHeight + (MINSPACING * 2))) {\n top = Math.ceil((viewportHeight - stepHeight) / 2);\n } else {\n const headerHeight = currentStepNode.find('.modal-header').first().outerHeight() ?? 0;\n const footerHeight = currentStepNode.find('.modal-footer').first().outerHeight() ?? 0;\n const currentStepBody = currentStepNode.find('[data-placeholder=\"body\"]').first();\n const maxHeight = viewportHeight - (MINSPACING * 2) - headerHeight - footerHeight;\n currentStepBody.css({\n 'max-height': maxHeight + 'px',\n 'overflow': 'auto',\n });\n }\n currentStepNode.offset({\n top: top,\n left: Math.ceil((viewportWidth - stepWidth) / 2)\n });\n }\n\n /**\n * Position the step on the page.\n *\n * @method positionStep\n * @param {Object} stepConfig The step configuration of the step\n * @chainable\n * @return {Object} this.\n */\n positionStep(stepConfig) {\n let content = this.currentStepNode;\n let thisT = this;\n if (!content || !content.length) {\n // Unable to find the step node.\n return this;\n }\n\n stepConfig.placement = this.recalculatePlacement(stepConfig);\n let flipBehavior;\n switch (stepConfig.placement) {\n case 'left':\n flipBehavior = ['left', 'right', 'top', 'bottom'];\n break;\n case 'right':\n flipBehavior = ['right', 'left', 'top', 'bottom'];\n break;\n case 'top':\n flipBehavior = ['top', 'bottom', 'right', 'left'];\n break;\n case 'bottom':\n flipBehavior = ['bottom', 'top', 'right', 'left'];\n break;\n default:\n flipBehavior = 'flip';\n break;\n }\n\n let offset = '0';\n if (stepConfig.backdrop) {\n // Offset the arrow so that it points to the cut-out in the backdrop.\n offset = `-${BUFFER}, ${BUFFER}`;\n }\n\n let target = this.getStepTarget(stepConfig);\n var config = {\n placement: stepConfig.placement + '-start',\n removeOnDestroy: true,\n modifiers: {\n flip: {\n behaviour: flipBehavior,\n },\n arrow: {\n element: '[data-role=\"arrow\"]',\n },\n offset: {\n offset: offset\n }\n },\n onCreate: function(data) {\n recalculateArrowPosition(data);\n recalculateStepPosition(data);\n },\n onUpdate: function(data) {\n recalculateArrowPosition(data);\n if (thisT.possitionNeedToBeRecalculated) {\n thisT.recalculatedNo++;\n thisT.possitionNeedToBeRecalculated = false;\n recalculateStepPosition(data);\n }\n // Reset backdrop position when things update.\n thisT.recalculateBackdropPosition(stepConfig);\n },\n };\n\n let recalculateArrowPosition = function(data) {\n let placement = data.placement.split('-')[0];\n const isVertical = ['left', 'right'].indexOf(placement) !== -1;\n const arrowElement = data.instance.popper.querySelector('[data-role=\"arrow\"]');\n const stepElement = $(data.instance.popper.querySelector('[data-role=\"flexitour-step\"]'));\n if (isVertical) {\n let arrowHeight = parseFloat(window.getComputedStyle(arrowElement).height);\n let arrowOffset = parseFloat(window.getComputedStyle(arrowElement).top);\n let popperHeight = parseFloat(window.getComputedStyle(data.instance.popper).height);\n let popperOffset = parseFloat(window.getComputedStyle(data.instance.popper).top);\n let popperBorderWidth = parseFloat(stepElement.css('borderTopWidth'));\n let popperBorderRadiusWidth = parseFloat(stepElement.css('borderTopLeftRadius')) * 2;\n let arrowPos = arrowOffset + (arrowHeight / 2);\n let maxPos = popperHeight + popperOffset - popperBorderWidth - popperBorderRadiusWidth;\n let minPos = popperOffset + popperBorderWidth + popperBorderRadiusWidth;\n if (arrowPos >= maxPos || arrowPos <= minPos) {\n let newArrowPos = 0;\n if (arrowPos > (popperHeight / 2)) {\n newArrowPos = maxPos - arrowHeight;\n } else {\n newArrowPos = minPos + arrowHeight;\n }\n $(arrowElement).css('top', newArrowPos);\n }\n } else {\n let arrowWidth = parseFloat(window.getComputedStyle(arrowElement).width);\n let arrowOffset = parseFloat(window.getComputedStyle(arrowElement).left);\n let popperWidth = parseFloat(window.getComputedStyle(data.instance.popper).width);\n let popperOffset = parseFloat(window.getComputedStyle(data.instance.popper).left);\n let popperBorderWidth = parseFloat(stepElement.css('borderTopWidth'));\n let popperBorderRadiusWidth = parseFloat(stepElement.css('borderTopLeftRadius')) * 2;\n let arrowPos = arrowOffset + (arrowWidth / 2);\n let maxPos = popperWidth + popperOffset - popperBorderWidth - popperBorderRadiusWidth;\n let minPos = popperOffset + popperBorderWidth + popperBorderRadiusWidth;\n if (arrowPos >= maxPos || arrowPos <= minPos) {\n let newArrowPos = 0;\n if (arrowPos > (popperWidth / 2)) {\n newArrowPos = maxPos - arrowWidth;\n } else {\n newArrowPos = minPos + arrowWidth;\n }\n $(arrowElement).css('left', newArrowPos);\n }\n }\n };\n\n const recalculateStepPosition = function(data) {\n const placement = data.placement.split('-')[0];\n const isVertical = ['left', 'right'].indexOf(placement) !== -1;\n const popperElement = $(data.instance.popper);\n const targetElement = $(data.instance.reference);\n const arrowElement = popperElement.find('[data-role=\"arrow\"]');\n const stepElement = popperElement.find('[data-role=\"flexitour-step\"]');\n const viewportHeight = $(window).height();\n const viewportWidth = $(window).width();\n const arrowHeight = parseFloat(arrowElement.outerHeight(true));\n const popperHeight = parseFloat(popperElement.outerHeight(true));\n const targetHeight = parseFloat(targetElement.outerHeight(true));\n const arrowWidth = parseFloat(arrowElement.outerWidth(true));\n const popperWidth = parseFloat(popperElement.outerWidth(true));\n const targetWidth = parseFloat(targetElement.outerWidth(true));\n let maxHeight;\n\n if (thisT.recalculatedNo > 1) {\n // The current screen is too small, and cannot fit with the original placement.\n // We should set the placement to auto so the PopperJS can calculate the perfect placement.\n thisT.currentStepPopper.options.placement = isVertical ? 'auto-left' : 'auto-bottom';\n }\n if (thisT.recalculatedNo > 2) {\n // Return here to prevent recursive calling.\n return;\n }\n\n if (isVertical) {\n // Find the best place to put the tour: Left of right.\n const leftSpace = targetElement.offset().left > 0 ? targetElement.offset().left : 0;\n const rightSpace = viewportWidth - leftSpace - targetWidth;\n const remainingSpace = leftSpace >= rightSpace ? leftSpace : rightSpace;\n maxHeight = viewportHeight - MINSPACING * 2;\n if (remainingSpace < (popperWidth + arrowWidth)) {\n const maxWidth = remainingSpace - MINSPACING - arrowWidth;\n if (maxWidth > 0) {\n popperElement.css({\n 'max-width': maxWidth + 'px',\n });\n // Not enough space, flag true to make Popper to recalculate the position.\n thisT.possitionNeedToBeRecalculated = true;\n }\n } else if (maxHeight < popperHeight) {\n // Check if the Popper's height can fit the viewport height or not.\n // If not, set the correct max-height value for the Popper element.\n popperElement.css({\n 'max-height': maxHeight + 'px',\n });\n }\n } else {\n // Find the best place to put the tour: Top of bottom.\n const topSpace = targetElement.offset().top > 0 ? targetElement.offset().top : 0;\n const bottomSpace = viewportHeight - topSpace - targetHeight;\n const remainingSpace = topSpace >= bottomSpace ? topSpace : bottomSpace;\n maxHeight = remainingSpace - MINSPACING - arrowHeight;\n if (remainingSpace < (popperHeight + arrowHeight)) {\n // Not enough space, flag true to make Popper to recalculate the position.\n thisT.possitionNeedToBeRecalculated = true;\n }\n }\n\n // Check if the Popper's height can fit the viewport height or not.\n // If not, set the correct max-height value for the body.\n const currentStepBody = stepElement.find('[data-placeholder=\"body\"]').first();\n const headerEle = stepElement.find('.modal-header').first();\n const footerEle = stepElement.find('.modal-footer').first();\n const headerHeight = headerEle.outerHeight(true) ?? 0;\n const footerHeight = footerEle.outerHeight(true) ?? 0;\n maxHeight = maxHeight - headerHeight - footerHeight;\n if (maxHeight > 0) {\n headerEle.removeClass('minimal');\n footerEle.removeClass('minimal');\n currentStepBody.css({\n 'max-height': maxHeight + 'px',\n 'overflow': 'auto',\n });\n } else {\n headerEle.addClass('minimal');\n footerEle.addClass('minimal');\n }\n // Call the Popper update method to update the position.\n thisT.currentStepPopper.update();\n };\n\n let background = $('[data-flexitour=\"highlight\"]');\n if (background.length) {\n target = background;\n }\n this.currentStepPopper = new Popper(target, content[0], config);\n\n return this;\n }\n\n /**\n * For left/right placement, checks that there is room for the step at current window size.\n *\n * If there is not enough room, changes placement to 'top'.\n *\n * @method recalculatePlacement\n * @param {Object} stepConfig The step configuration of the step\n * @return {String} The placement after recalculate\n */\n recalculatePlacement(stepConfig) {\n const arrowWidth = 16;\n let target = this.getStepTarget(stepConfig);\n let widthContent = this.currentStepNode.width() + arrowWidth;\n let targetOffsetLeft = target.offset().left - BUFFER;\n let targetOffsetRight = target.offset().left + target.width() + BUFFER;\n let placement = stepConfig.placement;\n\n if (['left', 'right'].indexOf(placement) !== -1) {\n if ((targetOffsetLeft < (widthContent + BUFFER)) &&\n ((targetOffsetRight + widthContent + BUFFER) > document.documentElement.clientWidth)) {\n placement = 'top';\n }\n }\n return placement;\n }\n\n /**\n * Recaculate where the backdrop and its cut-out should be.\n *\n * This is needed when highlighted elements are off the page.\n * This can be called on update to recalculate it all.\n *\n * @method recalculateBackdropPosition\n * @param {Object} stepConfig The step configuration of the step\n */\n recalculateBackdropPosition(stepConfig) {\n if (stepConfig.backdrop) {\n this.positionBackdrop(stepConfig);\n }\n }\n\n /**\n * Add the backdrop.\n *\n * @method positionBackdrop\n * @param {Object} stepConfig The step configuration of the step\n * @chainable\n * @return {Object} this.\n */\n positionBackdrop(stepConfig) {\n if (stepConfig.backdrop) {\n this.currentStepConfig.hasBackdrop = true;\n\n // Position our backdrop above everything else.\n let backdrop = $('div[data-flexitour=\"backdrop\"]');\n if (!backdrop.length) {\n backdrop = $('
');\n $('body').append(backdrop);\n }\n\n if (this.isStepActuallyVisible(stepConfig)) {\n let targetNode = this.getStepTarget(stepConfig);\n targetNode.attr('data-flexitour', 'highlight');\n\n let distanceFromTop = targetNode[0].getBoundingClientRect().top;\n let relativeTop = targetNode.offset().top - distanceFromTop;\n\n /*\n Draw a clip-path that makes the backdrop a window.\n The clip-path is drawn with x/y coordinates in the following sequence.\n\n 1--------------------------------------------------2\n 11 |\n | |\n | 8-----------------------------7 |\n | | | |\n | | | |\n | | | |\n 10-------9 | |\n 5--------------------------------------6 |\n | |\n | |\n 4--------------------------------------------------3\n */\n\n // These values will help us draw the backdrop.\n const viewportHeight = $(window).height();\n const viewportWidth = $(window).width();\n const elementWidth = targetNode.outerWidth() + (BUFFER * 2);\n let elementHeight = targetNode.outerHeight() + (BUFFER * 2);\n const elementLeft = targetNode.offset().left - BUFFER;\n let elementTop = targetNode.offset().top - BUFFER - relativeTop;\n\n // Check the amount of navbar overlap the highlight element has.\n // We will adjust the backdrop shape to compensate for the fixed navbar.\n let navbarOverlap = 0;\n if (targetNode.parents('[data-usertour=\"scroller\"]').length) {\n // Determine the navbar height.\n const scrollerElement = targetNode.parents('[data-usertour=\"scroller\"]');\n const navbarHeight = scrollerElement.offset().top;\n navbarOverlap = Math.max(Math.ceil(navbarHeight - elementTop), 0);\n elementTop = elementTop + navbarOverlap;\n elementHeight = elementHeight - navbarOverlap;\n }\n\n // Check if the step container is in the 'top' position.\n // We will re-anchor the step container to the shifted backdrop edge as opposed to the actual element.\n if (this.currentStepNode && this.currentStepNode.length) {\n const xPlacement = this.currentStepNode[0].getAttribute('x-placement');\n if (xPlacement === 'top-start') {\n this.currentStepNode[0].style.top = `${navbarOverlap}px`;\n } else {\n this.currentStepNode[0].style.top = '0px';\n }\n }\n\n let backdropPath = document.querySelector('div[data-flexitour=\"backdrop\"]');\n const radius = 10;\n\n const bottomRight = {\n 'x1': elementLeft + elementWidth - radius,\n 'y1': elementTop + elementHeight,\n 'x2': elementLeft + elementWidth,\n 'y2': elementTop + elementHeight - radius,\n };\n\n const topRight = {\n 'x1': elementLeft + elementWidth,\n 'y1': elementTop + radius,\n 'x2': elementLeft + elementWidth - radius,\n 'y2': elementTop,\n };\n\n const topLeft = {\n 'x1': elementLeft + radius,\n 'y1': elementTop,\n 'x2': elementLeft,\n 'y2': elementTop + radius,\n };\n\n const bottomLeft = {\n 'x1': elementLeft,\n 'y1': elementTop + elementHeight - radius,\n 'x2': elementLeft + radius,\n 'y2': elementTop + elementHeight,\n };\n\n // L = line.\n // C = Bezier curve.\n // Z = Close path.\n backdropPath.style.clipPath = `path('M 0 0 \\\n L ${viewportWidth} 0 \\\n L ${viewportWidth} ${viewportHeight} \\\n L 0 ${viewportHeight} \\\n L 0 ${elementTop + elementHeight} \\\n L ${bottomRight.x1} ${bottomRight.y1} \\\n C ${bottomRight.x1} ${bottomRight.y1} ${bottomRight.x2} ${bottomRight.y1} ${bottomRight.x2} ${bottomRight.y2} \\\n L ${topRight.x1} ${topRight.y1} \\\n C ${topRight.x1} ${topRight.y1} ${topRight.x1} ${topRight.y2} ${topRight.x2} ${topRight.y2} \\\n L ${topLeft.x1} ${topLeft.y1} \\\n C ${topLeft.x1} ${topLeft.y1} ${topLeft.x2} ${topLeft.y1} ${topLeft.x2} ${topLeft.y2} \\\n L ${bottomLeft.x1} ${bottomLeft.y1} \\\n C ${bottomLeft.x1} ${bottomLeft.y1} ${bottomLeft.x1} ${bottomLeft.y2} ${bottomLeft.x2} ${bottomLeft.y2} \\\n L 0 ${elementTop + elementHeight} \\\n Z'\n )`;\n }\n }\n return this;\n }\n\n /**\n * Calculate the inheritted position.\n *\n * @method calculatePosition\n * @param {jQuery} elem The element to calculate position for\n * @return {String} Calculated position\n */\n calculatePosition(elem) {\n elem = $(elem);\n while (elem.length && elem[0] !== document) {\n let position = elem.css('position');\n if (position !== 'static') {\n return position;\n }\n elem = elem.parent();\n }\n\n return null;\n }\n\n /**\n * Perform accessibility changes for step shown.\n *\n * This will add aria-hidden=\"true\" to all siblings and parent siblings.\n *\n * @method accessibilityShow\n */\n accessibilityShow() {\n let stateHolder = 'data-has-hidden';\n let attrName = 'aria-hidden';\n let hideFunction = function(child) {\n let flexitourRole = child.data('flexitour');\n if (flexitourRole) {\n switch (flexitourRole) {\n case 'container':\n case 'target':\n return;\n }\n }\n\n let hidden = child.attr(attrName);\n if (!hidden) {\n child.attr(stateHolder, true);\n Aria.hide(child);\n }\n };\n\n this.currentStepNode.siblings().each(function(index, node) {\n hideFunction($(node));\n });\n this.currentStepNode.parentsUntil('body').siblings().each(function(index, node) {\n hideFunction($(node));\n });\n }\n\n /**\n * Perform accessibility changes for step hidden.\n *\n * This will remove any newly added aria-hidden=\"true\".\n *\n * @method accessibilityHide\n */\n accessibilityHide() {\n let stateHolder = 'data-has-hidden';\n let showFunction = function(child) {\n let hidden = child.attr(stateHolder);\n if (typeof hidden !== 'undefined') {\n child.removeAttr(stateHolder);\n Aria.unhide(child);\n }\n };\n\n $('[' + stateHolder + ']').each(function(index, node) {\n showFunction($(node));\n });\n }\n};\n\nexport default Tour;\n"],"names":["constructor","config","targetNode","currentElement","window","getComputedStyle","position","parentElement","init","eventHandlers","reset","originalConfiguration","configure","apply","this","arguments","possitionNeedToBeRecalculated","recalculatedNo","storage","sessionStorage","storageKey","tourName","e","hide","resetStepListeners","steps","currentStepNumber","eventName","forEach","handler","addEventHandler","resetStepDefaults","template","templateContent","checkMinimumRequirements","Error","length","loadOriginalConfiguration","stepDefaults","setStepDefaults","extend","element","placement","delay","moveOnClick","moveAfterTime","orphan","direction","getCurrentStepNumber","parseInt","setCurrentStepNumber","stepNumber","setItem","code","DOMException","QUOTA_EXCEEDED_ERR","removeItem","getNextStepNumber","nextStepNumber","isStepPotentiallyVisible","getStepConfig","getPreviousStepNumber","previousStepNumber","isLastStep","stepConfig","isStepActuallyVisible","getPotentiallyVisibleSteps","result","stepId","stepid","isCSSAllowed","target","getStepTarget","is","testCSSElement","document","createElement","classList","add","body","appendChild","isAllowed","display","remove","next","gotoStep","previous","endTour","_gotoStep","pendingPromise","PendingPromise","delayed","setTimeout","resolve","fn","dispatchEvent","eventTypes","stepRender","defaultPrevented","renderStep","stepRendered","normalizeStepConfig","$","reflex","moveAfterClick","content","attachTo","attachPoint","first","detail","cancelable","tour","push","processStepListeners","listeners","node","currentStepNode","args","proxy","handleKeyDown","parents","listener","on","off","currentStepConfig","getTemplateContent","find","html","title","nextBtn","endBtn","removeClass","addClass","prop","then","value","catch","attr","displaystepnumbers","stepsPotentiallyVisible","totalStepsPotentiallyVisible","total","addStepToPage","clone","animationTarget","stop","data","positionBackdrop","append","css","top","left","animate","scrollTop","calculateScrollTop","promise","positionStep","revealStep","bind","isOrphan","currentStepPopper","Popper","removeOnDestroy","arrowElement","modifiers","enabled","applyStyle","onLoad","onCreate","images","calculateStepPositionInPage","fadeIn","announceStep","focus","bodyRegion","headerRegion","accessibilityShow","tabbableSelector","keyCode","hasBackdrop","currentIndex","nextIndex","nextNode","focusRelevant","activeElement","stepTarget","tabbableNodes","dialogContainer","filter","index","has","each","shiftKey","closest","last","preventDefault","call","startTour","startAt","storageStartValue","getItem","storageStartAt","tourStart","tourRunning","tourStarted","restartTour","tourEnd","previousTarget","tourEnded","transition","stepHide","destroy","removeAttr","backdrop","backdropRemovalPromise","fadeOut","currentStepElement","accessibilityHide","stepHidden","show","getStepContainer","viewportHeight","height","scrollParent","hasFixedPosition","offset","Math","max","min","ceil","stepHeight","viewportWidth","width","stepWidth","MINSPACING","maxHeight","outerHeight","flipBehavior","thisT","recalculatePlacement","flip","behaviour","arrow","recalculateArrowPosition","recalculateStepPosition","onUpdate","recalculateBackdropPosition","split","isVertical","indexOf","instance","popper","querySelector","stepElement","arrowHeight","parseFloat","arrowOffset","popperHeight","popperOffset","popperBorderWidth","popperBorderRadiusWidth","arrowPos","maxPos","minPos","newArrowPos","arrowWidth","popperWidth","popperElement","targetElement","reference","targetHeight","outerWidth","targetWidth","options","leftSpace","rightSpace","remainingSpace","maxWidth","topSpace","bottomSpace","currentStepBody","headerEle","footerEle","update","background","widthContent","targetOffsetLeft","targetOffsetRight","documentElement","clientWidth","distanceFromTop","getBoundingClientRect","relativeTop","elementWidth","BUFFER","elementHeight","elementLeft","elementTop","navbarOverlap","navbarHeight","xPlacement","getAttribute","style","radius","bottomRight","topRight","topLeft","bottomLeft","clipPath","x1","y1","x2","y2","calculatePosition","elem","parent","hideFunction","child","flexitourRole","Aria","siblings","parentsUntil","unhide"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"tour.min.js","sources":["../src/tour.js"],"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 user tour.\n *\n * @module tool_usertours/tour\n * @copyright 2018 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * A list of steps.\n *\n * @typedef {Object[]} StepList\n * @property {Number} stepId The id of the step in the database\n * @property {Number} position The position of the step within the tour (zero-indexed)\n */\n\nimport $ from 'jquery';\nimport * as Aria from 'core/aria';\nimport Popper from 'core/popper';\nimport {dispatchEvent} from 'core/event_dispatcher';\nimport {eventTypes} from './events';\nimport {getString} from 'core/str';\nimport {prefetchStrings} from 'core/prefetch';\nimport {notifyFilterContentUpdated} from 'core/event';\nimport PendingPromise from 'core/pending';\n\n/**\n * The minimum spacing for tour step to display.\n *\n * @private\n * @constant\n * @type {number}\n */\nconst MINSPACING = 10;\nconst BUFFER = 10;\n\n/**\n * A user tour.\n *\n * @class tool_usertours/tour\n * @property {boolean} tourRunning Whether the tour is currently running.\n */\nconst Tour = class {\n tourRunning = false;\n\n /**\n * @param {object} config The configuration object.\n */\n constructor(config) {\n this.init(config);\n }\n\n /**\n * Initialise the tour.\n *\n * @method init\n * @param {Object} config The configuration object.\n * @chainable\n * @return {Object} this.\n */\n init(config) {\n // Unset all handlers.\n this.eventHandlers = {};\n\n // Reset the current tour states.\n this.reset();\n\n // Store the initial configuration.\n this.originalConfiguration = config || {};\n\n // Apply configuration.\n this.configure.apply(this, arguments);\n\n // Unset recalculate state.\n this.possitionNeedToBeRecalculated = false;\n\n // Unset recalculate count.\n this.recalculatedNo = 0;\n\n try {\n this.storage = window.sessionStorage;\n this.storageKey = 'tourstate_' + this.tourName;\n } catch (e) {\n this.storage = false;\n this.storageKey = '';\n }\n\n prefetchStrings('tool_usertours', [\n 'nextstep_sequence',\n 'skip_tour'\n ]);\n\n return this;\n }\n\n /**\n * Reset the current tour state.\n *\n * @method reset\n * @chainable\n * @return {Object} this.\n */\n reset() {\n // Hide the current step.\n this.hide();\n\n // Unset all handlers.\n this.eventHandlers = [];\n\n // Unset all listeners.\n this.resetStepListeners();\n\n // Unset the original configuration.\n this.originalConfiguration = {};\n\n // Reset the current step number and list of steps.\n this.steps = [];\n\n // Reset the current step number.\n this.currentStepNumber = 0;\n\n return this;\n }\n\n /**\n * Prepare tour configuration.\n *\n * @method configure\n * @param {Object} config The configuration object.\n * @chainable\n * @return {Object} this.\n */\n configure(config) {\n if (typeof config === 'object') {\n // Tour name.\n if (typeof config.tourName !== 'undefined') {\n this.tourName = config.tourName;\n }\n\n // Set up eventHandlers.\n if (config.eventHandlers) {\n for (let eventName in config.eventHandlers) {\n config.eventHandlers[eventName].forEach(function(handler) {\n this.addEventHandler(eventName, handler);\n }, this);\n }\n }\n\n // Reset the step configuration.\n this.resetStepDefaults(true);\n\n // Configure the steps.\n if (typeof config.steps === 'object') {\n this.steps = config.steps;\n }\n\n if (typeof config.template !== 'undefined') {\n this.templateContent = config.template;\n }\n }\n\n // Check that we have enough to start the tour.\n this.checkMinimumRequirements();\n\n return this;\n }\n\n /**\n * Check that the configuration meets the minimum requirements.\n *\n * @method checkMinimumRequirements\n */\n checkMinimumRequirements() {\n // Need a tourName.\n if (!this.tourName) {\n throw new Error(\"Tour Name required\");\n }\n\n // Need a minimum of one step.\n if (!this.steps || !this.steps.length) {\n throw new Error(\"Steps must be specified\");\n }\n }\n\n /**\n * Reset step default configuration.\n *\n * @method resetStepDefaults\n * @param {Boolean} loadOriginalConfiguration Whether to load the original configuration supplied with the Tour.\n * @chainable\n * @return {Object} this.\n */\n resetStepDefaults(loadOriginalConfiguration) {\n if (typeof loadOriginalConfiguration === 'undefined') {\n loadOriginalConfiguration = true;\n }\n\n this.stepDefaults = {};\n if (!loadOriginalConfiguration || typeof this.originalConfiguration.stepDefaults === 'undefined') {\n this.setStepDefaults({});\n } else {\n this.setStepDefaults(this.originalConfiguration.stepDefaults);\n }\n\n return this;\n }\n\n /**\n * Set the step defaults.\n *\n * @method setStepDefaults\n * @param {Object} stepDefaults The step defaults to apply to all steps\n * @chainable\n * @return {Object} this.\n */\n setStepDefaults(stepDefaults) {\n if (!this.stepDefaults) {\n this.stepDefaults = {};\n }\n $.extend(\n this.stepDefaults,\n {\n element: '',\n placement: 'top',\n delay: 0,\n moveOnClick: false,\n moveAfterTime: 0,\n orphan: false,\n direction: 1,\n },\n stepDefaults\n );\n\n return this;\n }\n\n /**\n * Retrieve the current step number.\n *\n * @method getCurrentStepNumber\n * @return {Number} The current step number\n */\n getCurrentStepNumber() {\n return parseInt(this.currentStepNumber, 10);\n }\n\n /**\n * Store the current step number.\n *\n * @method setCurrentStepNumber\n * @param {Number} stepNumber The current step number\n * @chainable\n */\n setCurrentStepNumber(stepNumber) {\n this.currentStepNumber = stepNumber;\n if (this.storage) {\n try {\n this.storage.setItem(this.storageKey, stepNumber);\n } catch (e) {\n if (e.code === DOMException.QUOTA_EXCEEDED_ERR) {\n this.storage.removeItem(this.storageKey);\n }\n }\n }\n }\n\n /**\n * Get the next step number after the currently displayed step.\n *\n * @method getNextStepNumber\n * @param {Number} stepNumber The current step number\n * @return {Number} The next step number to display\n */\n getNextStepNumber(stepNumber) {\n if (typeof stepNumber === 'undefined') {\n stepNumber = this.getCurrentStepNumber();\n }\n let nextStepNumber = stepNumber + 1;\n\n // Keep checking the remaining steps.\n while (nextStepNumber <= this.steps.length) {\n if (this.isStepPotentiallyVisible(this.getStepConfig(nextStepNumber))) {\n return nextStepNumber;\n }\n nextStepNumber++;\n }\n\n return null;\n }\n\n /**\n * Get the previous step number before the currently displayed step.\n *\n * @method getPreviousStepNumber\n * @param {Number} stepNumber The current step number\n * @return {Number} The previous step number to display\n */\n getPreviousStepNumber(stepNumber) {\n if (typeof stepNumber === 'undefined') {\n stepNumber = this.getCurrentStepNumber();\n }\n let previousStepNumber = stepNumber - 1;\n\n // Keep checking the remaining steps.\n while (previousStepNumber >= 0) {\n if (this.isStepPotentiallyVisible(this.getStepConfig(previousStepNumber))) {\n return previousStepNumber;\n }\n previousStepNumber--;\n }\n\n return null;\n }\n\n /**\n * Is the step the final step number?\n *\n * @method isLastStep\n * @param {Number} stepNumber Step number to test\n * @return {Boolean} Whether the step is the final step\n */\n isLastStep(stepNumber) {\n let nextStepNumber = this.getNextStepNumber(stepNumber);\n\n return nextStepNumber === null;\n }\n\n /**\n * Is this step potentially visible?\n *\n * @method isStepPotentiallyVisible\n * @param {Object} stepConfig The step configuration to normalise\n * @return {Boolean} Whether the step is the potentially visible\n */\n isStepPotentiallyVisible(stepConfig) {\n if (!stepConfig) {\n // Without step config, there can be no step.\n return false;\n }\n\n if (this.isStepActuallyVisible(stepConfig)) {\n // If it is actually visible, it is already potentially visible.\n return true;\n }\n\n if (typeof stepConfig.orphan !== 'undefined' && stepConfig.orphan) {\n // Orphan steps have no target. They are always visible.\n return true;\n }\n\n if (typeof stepConfig.delay !== 'undefined' && stepConfig.delay) {\n // Only return true if the activated has not been used yet.\n return true;\n }\n\n // Not theoretically, or actually visible.\n return false;\n }\n\n /**\n * Get potentially visible steps in a tour.\n *\n * @returns {StepList} A list of ordered steps\n */\n getPotentiallyVisibleSteps() {\n let position = 1;\n let result = [];\n // Checking the total steps.\n for (let stepNumber = 0; stepNumber < this.steps.length; stepNumber++) {\n const stepConfig = this.getStepConfig(stepNumber);\n if (this.isStepPotentiallyVisible(stepConfig)) {\n result[stepNumber] = {stepId: stepConfig.stepid, position: position};\n position++;\n }\n }\n\n return result;\n }\n\n /**\n * Is this step actually visible?\n *\n * @method isStepActuallyVisible\n * @param {Object} stepConfig The step configuration to normalise\n * @return {Boolean} Whether the step is actually visible\n */\n isStepActuallyVisible(stepConfig) {\n if (!stepConfig) {\n // Without step config, there can be no step.\n return false;\n }\n\n // Check if the CSS styles are allowed on the browser or not.\n if (!this.isCSSAllowed()) {\n return false;\n }\n\n let target = this.getStepTarget(stepConfig);\n if (target && target.length && target.is(':visible')) {\n // Without a target, there can be no step.\n return !!target.length;\n }\n\n return false;\n }\n\n /**\n * Is the browser actually allow CSS styles?\n *\n * @returns {boolean} True if the browser is allowing CSS styles\n */\n isCSSAllowed() {\n const testCSSElement = document.createElement('div');\n testCSSElement.classList.add('hide');\n document.body.appendChild(testCSSElement);\n const styles = window.getComputedStyle(testCSSElement);\n const isAllowed = styles.display === 'none';\n testCSSElement.remove();\n\n return isAllowed;\n }\n\n /**\n * Go to the next step in the tour.\n *\n * @method next\n * @chainable\n * @return {Object} this.\n */\n next() {\n return this.gotoStep(this.getNextStepNumber());\n }\n\n /**\n * Go to the previous step in the tour.\n *\n * @method previous\n * @chainable\n * @return {Object} this.\n */\n previous() {\n return this.gotoStep(this.getPreviousStepNumber(), -1);\n }\n\n /**\n * Go to the specified step in the tour.\n *\n * @method gotoStep\n * @param {Number} stepNumber The step number to display\n * @param {Number} direction Next or previous step\n * @chainable\n * @return {Object} this.\n * @fires tool_usertours/stepRender\n * @fires tool_usertours/stepRendered\n * @fires tool_usertours/stepHide\n * @fires tool_usertours/stepHidden\n */\n gotoStep(stepNumber, direction) {\n if (stepNumber < 0) {\n return this.endTour();\n }\n\n let stepConfig = this.getStepConfig(stepNumber);\n if (stepConfig === null) {\n return this.endTour();\n }\n\n return this._gotoStep(stepConfig, direction);\n }\n\n _gotoStep(stepConfig, direction) {\n if (!stepConfig) {\n return this.endTour();\n }\n\n const pendingPromise = new PendingPromise(`tool_usertours/tour:_gotoStep-${stepConfig.stepNumber}`);\n\n if (typeof stepConfig.delay !== 'undefined' && stepConfig.delay && !stepConfig.delayed) {\n stepConfig.delayed = true;\n window.setTimeout(function(stepConfig, direction) {\n this._gotoStep(stepConfig, direction);\n pendingPromise.resolve();\n }, stepConfig.delay, stepConfig, direction);\n\n return this;\n } else if (!stepConfig.orphan && !this.isStepActuallyVisible(stepConfig)) {\n const fn = direction == -1 ? 'getPreviousStepNumber' : 'getNextStepNumber';\n this.gotoStep(this[fn](stepConfig.stepNumber), direction);\n\n pendingPromise.resolve();\n return this;\n }\n\n this.hide();\n\n const stepRenderEvent = this.dispatchEvent(eventTypes.stepRender, {stepConfig}, true);\n if (!stepRenderEvent.defaultPrevented) {\n this.renderStep(stepConfig);\n this.dispatchEvent(eventTypes.stepRendered, {stepConfig});\n }\n\n pendingPromise.resolve();\n return this;\n }\n\n /**\n * Fetch the normalised step configuration for the specified step number.\n *\n * @method getStepConfig\n * @param {Number} stepNumber The step number to fetch configuration for\n * @return {Object} The step configuration\n */\n getStepConfig(stepNumber) {\n if (stepNumber === null || stepNumber < 0 || stepNumber >= this.steps.length) {\n return null;\n }\n\n // Normalise the step configuration.\n let stepConfig = this.normalizeStepConfig(this.steps[stepNumber]);\n\n // Add the stepNumber to the stepConfig.\n stepConfig = $.extend(stepConfig, {stepNumber: stepNumber});\n\n return stepConfig;\n }\n\n /**\n * Normalise the supplied step configuration.\n *\n * @method normalizeStepConfig\n * @param {Object} stepConfig The step configuration to normalise\n * @return {Object} The normalised step configuration\n */\n normalizeStepConfig(stepConfig) {\n\n if (typeof stepConfig.reflex !== 'undefined' && typeof stepConfig.moveAfterClick === 'undefined') {\n stepConfig.moveAfterClick = stepConfig.reflex;\n }\n\n if (typeof stepConfig.element !== 'undefined' && typeof stepConfig.target === 'undefined') {\n stepConfig.target = stepConfig.element;\n }\n\n if (typeof stepConfig.content !== 'undefined' && typeof stepConfig.body === 'undefined') {\n stepConfig.body = stepConfig.content;\n }\n\n stepConfig = $.extend({}, this.stepDefaults, stepConfig);\n\n stepConfig = $.extend({}, {\n attachTo: stepConfig.target,\n attachPoint: 'after',\n }, stepConfig);\n\n if (stepConfig.attachTo) {\n stepConfig.attachTo = $(stepConfig.attachTo).first();\n }\n\n return stepConfig;\n }\n\n /**\n * Fetch the actual step target from the selector.\n *\n * This should not be called until after any delay has completed.\n *\n * @method getStepTarget\n * @param {Object} stepConfig The step configuration\n * @return {$}\n */\n getStepTarget(stepConfig) {\n if (stepConfig.target) {\n return $(stepConfig.target);\n }\n\n return null;\n }\n\n /**\n * Fire any event handlers for the specified event.\n *\n * @param {String} eventName The name of the event\n * @param {Object} [detail={}] Any additional details to pass into the eveent\n * @param {Boolean} [cancelable=false] Whether preventDefault() can be called\n * @returns {CustomEvent}\n */\n dispatchEvent(\n eventName,\n detail = {},\n cancelable = false\n ) {\n return dispatchEvent(eventName, {\n // Add the tour to the detail.\n tour: this,\n ...detail,\n }, document, {\n cancelable,\n });\n }\n\n /**\n * @method addEventHandler\n * @param {string} eventName The name of the event to listen for\n * @param {function} handler The event handler to call\n * @return {Object} this.\n */\n addEventHandler(eventName, handler) {\n if (typeof this.eventHandlers[eventName] === 'undefined') {\n this.eventHandlers[eventName] = [];\n }\n\n this.eventHandlers[eventName].push(handler);\n\n return this;\n }\n\n /**\n * Process listeners for the step being shown.\n *\n * @method processStepListeners\n * @param {object} stepConfig The configuration for the step\n * @chainable\n * @return {Object} this.\n */\n processStepListeners(stepConfig) {\n this.listeners.push(\n // Next button.\n {\n node: this.currentStepNode,\n args: ['click', '[data-role=\"next\"]', $.proxy(this.next, this)]\n },\n\n // Close and end tour buttons.\n {\n node: this.currentStepNode,\n args: ['click', '[data-role=\"end\"]', $.proxy(this.endTour, this)]\n },\n\n // Click backdrop and hide tour.\n {\n node: $('[data-flexitour=\"backdrop\"]'),\n args: ['click', $.proxy(this.hide, this)]\n },\n\n // Keypresses.\n {\n node: $('body'),\n args: ['keydown', $.proxy(this.handleKeyDown, this)]\n });\n\n if (stepConfig.moveOnClick) {\n var targetNode = this.getStepTarget(stepConfig);\n this.listeners.push({\n node: targetNode,\n args: ['click', $.proxy(function(e) {\n if ($(e.target).parents('[data-flexitour=\"container\"]').length === 0) {\n // Ignore clicks when they are in the flexitour.\n window.setTimeout($.proxy(this.next, this), 500);\n }\n }, this)]\n });\n }\n\n this.listeners.forEach(function(listener) {\n listener.node.on.apply(listener.node, listener.args);\n });\n\n return this;\n }\n\n /**\n * Reset step listeners.\n *\n * @method resetStepListeners\n * @chainable\n * @return {Object} this.\n */\n resetStepListeners() {\n // Stop listening to all external handlers.\n if (this.listeners) {\n this.listeners.forEach(function(listener) {\n listener.node.off.apply(listener.node, listener.args);\n });\n }\n this.listeners = [];\n\n return this;\n }\n\n /**\n * The standard step renderer.\n *\n * @method renderStep\n * @param {Object} stepConfig The step configuration of the step\n * @chainable\n * @return {Object} this.\n */\n renderStep(stepConfig) {\n // Store the current step configuration for later.\n this.currentStepConfig = stepConfig;\n this.setCurrentStepNumber(stepConfig.stepNumber);\n\n // Fetch the template and convert it to a $ object.\n let template = $(this.getTemplateContent());\n\n // Title.\n template.find('[data-placeholder=\"title\"]')\n .html(stepConfig.title);\n\n // Body.\n template.find('[data-placeholder=\"body\"]')\n .html(stepConfig.body);\n\n // Buttons.\n const nextBtn = template.find('[data-role=\"next\"]');\n const endBtn = template.find('[data-role=\"end\"]');\n\n // Is this the final step?\n if (this.isLastStep(stepConfig.stepNumber)) {\n nextBtn.hide();\n endBtn.removeClass(\"btn-secondary\").addClass(\"btn-primary\");\n } else {\n nextBtn.prop('disabled', false);\n // Use Skip tour label for the End tour button.\n getString('skip_tour', 'tool_usertours').then(value => {\n endBtn.html(value);\n return;\n }).catch();\n }\n\n nextBtn.attr('role', 'button');\n endBtn.attr('role', 'button');\n\n if (this.originalConfiguration.displaystepnumbers) {\n const stepsPotentiallyVisible = this.getPotentiallyVisibleSteps();\n const totalStepsPotentiallyVisible = stepsPotentiallyVisible.length;\n const position = stepsPotentiallyVisible[stepConfig.stepNumber].position;\n if (totalStepsPotentiallyVisible > 1) {\n // Change the label of the Next button to include the sequence.\n getString('nextstep_sequence', 'tool_usertours',\n {position: position, total: totalStepsPotentiallyVisible}).then(value => {\n nextBtn.html(value);\n return;\n }).catch();\n }\n }\n\n // Replace the template with the updated version.\n stepConfig.template = template;\n\n // Add to the page.\n this.addStepToPage(stepConfig);\n\n // Process step listeners after adding to the page.\n // This uses the currentNode.\n this.processStepListeners(stepConfig);\n\n return this;\n }\n\n /**\n * Getter for the template content.\n *\n * @method getTemplateContent\n * @return {$}\n */\n getTemplateContent() {\n return $(this.templateContent).clone();\n }\n\n /**\n * Helper to add a step to the page.\n *\n * @method addStepToPage\n * @param {Object} stepConfig The step configuration of the step\n * @chainable\n * @return {Object} this.\n */\n addStepToPage(stepConfig) {\n // Create the stepNode from the template data.\n let currentStepNode = $('')\n .html(stepConfig.template)\n .hide();\n // Trigger the Moodle filters.\n notifyFilterContentUpdated(currentStepNode);\n\n // The scroll animation occurs on the body or html.\n let animationTarget = $('body, html')\n .stop(true, true);\n\n if (this.isStepActuallyVisible(stepConfig)) {\n let targetNode = this.getStepTarget(stepConfig);\n\n targetNode.data('flexitour', 'target');\n\n $(document.body).append(currentStepNode);\n this.currentStepNode = currentStepNode;\n\n // Ensure that the step node is positioned.\n // Some situations mean that the value is not properly calculated without this step.\n this.currentStepNode.css({\n top: 0,\n left: 0,\n });\n\n const pendingPromise = new PendingPromise(`tool_usertours/tour:addStepToPage-${stepConfig.stepNumber}`);\n animationTarget\n .animate({\n scrollTop: this.calculateScrollTop(stepConfig),\n }).promise().then(function() {\n this.positionBackdrop(stepConfig);\n this.positionStep(stepConfig);\n this.revealStep(stepConfig);\n pendingPromise.resolve();\n return;\n }.bind(this))\n .catch(function() {\n // Silently fail.\n });\n\n } else if (stepConfig.orphan) {\n stepConfig.isOrphan = true;\n\n // This will be appended to the body instead.\n stepConfig.attachTo = $('body').first();\n stepConfig.attachPoint = 'append';\n\n // Add the backdrop.\n this.positionBackdrop(stepConfig);\n\n // This is an orphaned step.\n currentStepNode.addClass('orphan');\n\n // It lives in the body.\n $(document.body).append(currentStepNode);\n this.currentStepNode = currentStepNode;\n\n this.currentStepNode.css('position', 'fixed');\n\n this.currentStepPopper = new Popper(\n $('body'),\n this.currentStepNode[0], {\n removeOnDestroy: true,\n placement: stepConfig.placement + '-start',\n arrowElement: '[data-role=\"arrow\"]',\n // Empty the modifiers. We've already placed the step and don't want it moved.\n modifiers: {\n hide: {\n enabled: false,\n },\n applyStyle: {\n onLoad: null,\n enabled: false,\n },\n },\n onCreate: () => {\n // First, we need to check if the step's content contains any images.\n const images = this.currentStepNode.find('img');\n if (images.length) {\n // Images found, need to calculate the position when the image is loaded.\n images.on('load', () => {\n this.calculateStepPositionInPage(currentStepNode);\n });\n }\n this.calculateStepPositionInPage(currentStepNode);\n }\n }\n );\n\n this.revealStep(stepConfig);\n }\n\n return this;\n }\n\n /**\n * Make the given step visible.\n *\n * @method revealStep\n * @param {Object} stepConfig The step configuration of the step\n * @chainable\n * @return {Object} this.\n */\n revealStep(stepConfig) {\n // Fade the step in.\n const pendingPromise = new PendingPromise(`tool_usertours/tour:revealStep-${stepConfig.stepNumber}`);\n this.currentStepNode.fadeIn('', $.proxy(function() {\n // Announce via ARIA.\n this.announceStep(stepConfig);\n\n // Focus on the current step Node.\n this.currentStepNode.focus();\n window.setTimeout($.proxy(function() {\n // After a brief delay, focus again.\n // There seems to be an issue with Jaws where it only reads the dialogue title initially.\n // This second focus helps it to read the full dialogue.\n if (this.currentStepNode) {\n this.currentStepNode.focus();\n }\n pendingPromise.resolve();\n }, this), 100);\n\n }, this));\n\n return this;\n }\n\n /**\n * Helper to announce the step on the page.\n *\n * @method announceStep\n * @param {Object} stepConfig The step configuration of the step\n * @chainable\n * @return {Object} this.\n */\n announceStep(stepConfig) {\n // Setup the step Dialogue as per:\n // * https://www.w3.org/TR/wai-aria-practices/#dialog_nonmodal\n // * https://www.w3.org/TR/wai-aria-practices/#dialog_modal\n\n // Generate an ID for the current step node.\n let stepId = 'tour-step-' + this.tourName + '-' + stepConfig.stepNumber;\n this.currentStepNode.attr('id', stepId);\n\n let bodyRegion = this.currentStepNode.find('[data-placeholder=\"body\"]').first();\n bodyRegion.attr('id', stepId + '-body');\n bodyRegion.attr('role', 'document');\n\n let headerRegion = this.currentStepNode.find('[data-placeholder=\"title\"]').first();\n headerRegion.attr('id', stepId + '-title');\n headerRegion.attr('aria-labelledby', stepId + '-body');\n\n // Generally, a modal dialog has a role of dialog.\n this.currentStepNode.attr('role', 'dialog');\n this.currentStepNode.attr('tabindex', 0);\n this.currentStepNode.attr('aria-labelledby', stepId + '-title');\n this.currentStepNode.attr('aria-describedby', stepId + '-body');\n\n // Configure ARIA attributes on the target.\n let target = this.getStepTarget(stepConfig);\n if (target) {\n target.data('original-tabindex', target.attr('tabindex'));\n if (!target.attr('tabindex')) {\n target.attr('tabindex', 0);\n }\n\n target\n .data('original-describedby', target.attr('aria-describedby'))\n .attr('aria-describedby', stepId + '-body')\n ;\n }\n\n this.accessibilityShow(stepConfig);\n\n return this;\n }\n\n /**\n * Handle key down events.\n *\n * @method handleKeyDown\n * @param {EventFacade} e\n */\n handleKeyDown(e) {\n let tabbableSelector = 'a[href], link[href], [draggable=true], [contenteditable=true], ';\n tabbableSelector += ':input:enabled, [tabindex], button:enabled';\n switch (e.keyCode) {\n case 27:\n this.endTour();\n break;\n\n // 9 == Tab - trap focus for items with a backdrop.\n case 9:\n // Tab must be handled on key up only in this instance.\n (function() {\n if (!this.currentStepConfig.hasBackdrop) {\n // Trapping tab focus is only handled for those steps with a backdrop.\n return;\n }\n\n // Find all tabbable locations.\n let activeElement = $(document.activeElement);\n let stepTarget = this.getStepTarget(this.currentStepConfig);\n let tabbableNodes = $(tabbableSelector);\n let dialogContainer = $('span[data-flexitour=\"container\"]');\n let currentIndex;\n // Filter out element which is not belong to target section or dialogue.\n if (stepTarget) {\n tabbableNodes = tabbableNodes.filter(function(index, element) {\n return stepTarget !== null\n && (stepTarget.has(element).length\n || dialogContainer.has(element).length\n || stepTarget.is(element)\n || dialogContainer.is(element));\n });\n }\n\n // Find index of focusing element.\n tabbableNodes.each(function(index, element) {\n if (activeElement.is(element)) {\n currentIndex = index;\n return false;\n }\n // Keep looping.\n return true;\n });\n\n let nextIndex;\n let nextNode;\n let focusRelevant;\n if (currentIndex != void 0) {\n let direction = 1;\n if (e.shiftKey) {\n direction = -1;\n }\n nextIndex = currentIndex;\n do {\n nextIndex += direction;\n nextNode = $(tabbableNodes[nextIndex]);\n } while (nextNode.length && nextNode.is(':disabled') || nextNode.is(':hidden'));\n if (nextNode.length) {\n // A new f\n focusRelevant = nextNode.closest(stepTarget).length;\n focusRelevant = focusRelevant || nextNode.closest(this.currentStepNode).length;\n } else {\n // Unable to find the target somehow.\n focusRelevant = false;\n }\n }\n\n if (focusRelevant) {\n nextNode.focus();\n } else {\n if (e.shiftKey) {\n // Focus on the last tabbable node in the step.\n this.currentStepNode.find(tabbableSelector).last().focus();\n } else {\n if (this.currentStepConfig.isOrphan) {\n // Focus on the step - there is no target.\n this.currentStepNode.focus();\n } else {\n // Focus on the step target.\n stepTarget.focus();\n }\n }\n }\n e.preventDefault();\n }).call(this);\n break;\n }\n }\n\n /**\n * Start the current tour.\n *\n * @method startTour\n * @param {Number} startAt Which step number to start at. If not specified, starts at the last point.\n * @chainable\n * @return {Object} this.\n * @fires tool_usertours/tourStart\n * @fires tool_usertours/tourStarted\n */\n startTour(startAt) {\n if (this.storage && typeof startAt === 'undefined') {\n let storageStartValue = this.storage.getItem(this.storageKey);\n if (storageStartValue) {\n let storageStartAt = parseInt(storageStartValue, 10);\n if (storageStartAt <= this.steps.length) {\n startAt = storageStartAt;\n }\n }\n }\n\n if (typeof startAt === 'undefined') {\n startAt = this.getCurrentStepNumber();\n }\n\n const tourStartEvent = this.dispatchEvent(eventTypes.tourStart, {startAt}, true);\n if (!tourStartEvent.defaultPrevented) {\n this.gotoStep(startAt);\n this.tourRunning = true;\n this.dispatchEvent(eventTypes.tourStarted, {startAt});\n }\n\n return this;\n }\n\n /**\n * Restart the tour from the beginning, resetting the completionlag.\n *\n * @method restartTour\n * @chainable\n * @return {Object} this.\n */\n restartTour() {\n return this.startTour(0);\n }\n\n /**\n * End the current tour.\n *\n * @method endTour\n * @chainable\n * @return {Object} this.\n * @fires tool_usertours/tourEnd\n * @fires tool_usertours/tourEnded\n */\n endTour() {\n const tourEndEvent = this.dispatchEvent(eventTypes.tourEnd, {}, true);\n if (tourEndEvent.defaultPrevented) {\n return this;\n }\n\n if (this.currentStepConfig) {\n let previousTarget = this.getStepTarget(this.currentStepConfig);\n if (previousTarget) {\n if (!previousTarget.attr('tabindex')) {\n previousTarget.attr('tabindex', '-1');\n }\n previousTarget.first().focus();\n }\n }\n\n this.hide(true);\n\n this.tourRunning = false;\n this.dispatchEvent(eventTypes.tourEnded);\n\n return this;\n }\n\n /**\n * Hide any currently visible steps.\n *\n * @method hide\n * @param {Bool} transition Animate the visibility change\n * @chainable\n * @return {Object} this.\n * @fires tool_usertours/stepHide\n * @fires tool_usertours/stepHidden\n */\n hide(transition) {\n const stepHideEvent = this.dispatchEvent(eventTypes.stepHide, {}, true);\n if (stepHideEvent.defaultPrevented) {\n return this;\n }\n\n const pendingPromise = new PendingPromise('tool_usertours/tour:hide');\n if (this.currentStepNode && this.currentStepNode.length) {\n this.currentStepNode.hide();\n if (this.currentStepPopper) {\n this.currentStepPopper.destroy();\n }\n }\n\n // Restore original target configuration.\n if (this.currentStepConfig) {\n let target = this.getStepTarget(this.currentStepConfig);\n if (target) {\n if (target.data('original-labelledby')) {\n target.attr('aria-labelledby', target.data('original-labelledby'));\n }\n\n if (target.data('original-describedby')) {\n target.attr('aria-describedby', target.data('original-describedby'));\n }\n\n if (target.data('original-tabindex')) {\n target.attr('tabindex', target.data('tabindex'));\n } else {\n // If the target does not have the tabindex attribute at the beginning. We need to remove it.\n // We should wait a little here before removing the attribute to prevent the browser from adding it again.\n window.setTimeout(() => {\n target.removeAttr('tabindex');\n }, 400);\n }\n }\n\n // Clear the step configuration.\n this.currentStepConfig = null;\n }\n\n // Remove the highlight attribute when the hide occurs.\n $('[data-flexitour=\"highlight\"]').removeAttr('data-flexitour');\n\n const backdrop = $('[data-flexitour=\"backdrop\"]');\n if (backdrop.length) {\n if (transition) {\n const backdropRemovalPromise = new PendingPromise('tool_usertours/tour:hide:backdrop');\n backdrop.fadeOut(400, function() {\n $(this).remove();\n backdropRemovalPromise.resolve();\n });\n } else {\n backdrop.remove();\n }\n }\n\n // Remove aria-describedby and tabindex attributes.\n if (this.currentStepNode && this.currentStepNode.length) {\n let stepId = this.currentStepNode.attr('id');\n if (stepId) {\n let currentStepElement = '[aria-describedby=\"' + stepId + '-body\"]';\n $(currentStepElement).removeAttr('tabindex');\n $(currentStepElement).removeAttr('aria-describedby');\n }\n }\n\n // Reset the listeners.\n this.resetStepListeners();\n\n this.accessibilityHide();\n\n this.dispatchEvent(eventTypes.stepHidden);\n\n this.currentStepNode = null;\n this.currentStepPopper = null;\n\n pendingPromise.resolve();\n return this;\n }\n\n /**\n * Show the current steps.\n *\n * @method show\n * @chainable\n * @return {Object} this.\n */\n show() {\n // Show the current step.\n let startAt = this.getCurrentStepNumber();\n\n return this.gotoStep(startAt);\n }\n\n /**\n * Return the current step node.\n *\n * @method getStepContainer\n * @return {jQuery}\n */\n getStepContainer() {\n return $(this.currentStepNode);\n }\n\n /**\n * Check whether the target node has a fixed position, or is nested within one.\n *\n * @param {Object} targetNode The target element to check.\n * @return {Boolean} Return true if fixed position found.\n */\n hasFixedPosition = (targetNode) => {\n let currentElement = targetNode[0];\n while (currentElement) {\n const computedStyle = window.getComputedStyle(currentElement);\n if (computedStyle.position === 'fixed') {\n return true;\n }\n currentElement = currentElement.parentElement;\n }\n\n return false;\n };\n\n /**\n * Calculate scrollTop.\n *\n * @method calculateScrollTop\n * @param {Object} stepConfig The step configuration of the step\n * @return {Number}\n */\n calculateScrollTop(stepConfig) {\n let viewportHeight = $(window).height();\n let targetNode = this.getStepTarget(stepConfig);\n\n let scrollParent = $(window);\n if (targetNode.parents('[data-usertour=\"scroller\"]').length) {\n scrollParent = targetNode.parents('[data-usertour=\"scroller\"]');\n }\n let scrollTop = scrollParent.scrollTop();\n\n if (this.hasFixedPosition(targetNode)) {\n // Target must be in a fixed or custom position. No need to modify the scrollTop.\n } else if (stepConfig.placement === 'top') {\n // If the placement is top, center scroll at the top of the target.\n scrollTop = targetNode.offset().top - (viewportHeight / 2);\n } else if (stepConfig.placement === 'bottom') {\n // If the placement is bottom, center scroll at the bottom of the target.\n scrollTop = targetNode.offset().top + targetNode.height() + scrollTop - (viewportHeight / 2);\n } else if (targetNode.height() <= (viewportHeight * 0.8)) {\n // If the placement is left/right, and the target fits in the viewport, centre screen on the target\n scrollTop = targetNode.offset().top - ((viewportHeight - targetNode.height()) / 2);\n } else {\n // If the placement is left/right, and the target is bigger than the viewport, set scrollTop to target.top + buffer\n // and change step attachmentTarget to top+.\n scrollTop = targetNode.offset().top - (viewportHeight * 0.2);\n }\n\n // Never scroll over the top.\n scrollTop = Math.max(0, scrollTop);\n\n // Never scroll beyond the bottom.\n scrollTop = Math.min($(document).height() - viewportHeight, scrollTop);\n\n return Math.ceil(scrollTop);\n }\n\n /**\n * Calculate dialogue position for page middle.\n *\n * @param {jQuery} currentStepNode Current step node\n * @method calculateScrollTop\n */\n calculateStepPositionInPage(currentStepNode) {\n let top = MINSPACING;\n const viewportHeight = $(window).height();\n const stepHeight = currentStepNode.height();\n const viewportWidth = $(window).width();\n const stepWidth = currentStepNode.width();\n if (viewportHeight >= (stepHeight + (MINSPACING * 2))) {\n top = Math.ceil((viewportHeight - stepHeight) / 2);\n } else {\n const headerHeight = currentStepNode.find('.modal-header').first().outerHeight() ?? 0;\n const footerHeight = currentStepNode.find('.modal-footer').first().outerHeight() ?? 0;\n const currentStepBody = currentStepNode.find('[data-placeholder=\"body\"]').first();\n const maxHeight = viewportHeight - (MINSPACING * 2) - headerHeight - footerHeight;\n currentStepBody.css({\n 'max-height': maxHeight + 'px',\n 'overflow': 'auto',\n });\n }\n currentStepNode.offset({\n top: top,\n left: Math.ceil((viewportWidth - stepWidth) / 2)\n });\n }\n\n /**\n * Position the step on the page.\n *\n * @method positionStep\n * @param {Object} stepConfig The step configuration of the step\n * @chainable\n * @return {Object} this.\n */\n positionStep(stepConfig) {\n let content = this.currentStepNode;\n let thisT = this;\n if (!content || !content.length) {\n // Unable to find the step node.\n return this;\n }\n\n stepConfig.placement = this.recalculatePlacement(stepConfig);\n let flipBehavior;\n switch (stepConfig.placement) {\n case 'left':\n flipBehavior = ['left', 'right', 'top', 'bottom'];\n break;\n case 'right':\n flipBehavior = ['right', 'left', 'top', 'bottom'];\n break;\n case 'top':\n flipBehavior = ['top', 'bottom', 'right', 'left'];\n break;\n case 'bottom':\n flipBehavior = ['bottom', 'top', 'right', 'left'];\n break;\n default:\n flipBehavior = 'flip';\n break;\n }\n\n let offset = '0';\n if (stepConfig.backdrop) {\n // Offset the arrow so that it points to the cut-out in the backdrop.\n offset = `-${BUFFER}, ${BUFFER}`;\n }\n\n let target = this.getStepTarget(stepConfig);\n var config = {\n placement: stepConfig.placement + '-start',\n removeOnDestroy: true,\n modifiers: {\n flip: {\n behaviour: flipBehavior,\n },\n arrow: {\n element: '[data-role=\"arrow\"]',\n },\n offset: {\n offset: offset\n }\n },\n onCreate: function(data) {\n recalculateArrowPosition(data);\n recalculateStepPosition(data);\n },\n onUpdate: function(data) {\n recalculateArrowPosition(data);\n if (thisT.possitionNeedToBeRecalculated) {\n thisT.recalculatedNo++;\n thisT.possitionNeedToBeRecalculated = false;\n recalculateStepPosition(data);\n }\n // Reset backdrop position when things update.\n thisT.recalculateBackdropPosition(stepConfig);\n },\n };\n\n let recalculateArrowPosition = function(data) {\n let placement = data.placement.split('-')[0];\n const isVertical = ['left', 'right'].indexOf(placement) !== -1;\n const arrowElement = data.instance.popper.querySelector('[data-role=\"arrow\"]');\n const stepElement = $(data.instance.popper.querySelector('[data-role=\"flexitour-step\"]'));\n if (isVertical) {\n let arrowHeight = parseFloat(window.getComputedStyle(arrowElement).height);\n let arrowOffset = parseFloat(window.getComputedStyle(arrowElement).top);\n let popperHeight = parseFloat(window.getComputedStyle(data.instance.popper).height);\n let popperOffset = parseFloat(window.getComputedStyle(data.instance.popper).top);\n let popperBorderWidth = parseFloat(stepElement.css('borderTopWidth'));\n let popperBorderRadiusWidth = parseFloat(stepElement.css('borderTopLeftRadius')) * 2;\n let arrowPos = arrowOffset + (arrowHeight / 2);\n let maxPos = popperHeight + popperOffset - popperBorderWidth - popperBorderRadiusWidth;\n let minPos = popperOffset + popperBorderWidth + popperBorderRadiusWidth;\n if (arrowPos >= maxPos || arrowPos <= minPos) {\n let newArrowPos = 0;\n if (arrowPos > (popperHeight / 2)) {\n newArrowPos = maxPos - arrowHeight;\n } else {\n newArrowPos = minPos + arrowHeight;\n }\n $(arrowElement).css('top', newArrowPos);\n }\n } else {\n let arrowWidth = parseFloat(window.getComputedStyle(arrowElement).width);\n let arrowOffset = parseFloat(window.getComputedStyle(arrowElement).left);\n let popperWidth = parseFloat(window.getComputedStyle(data.instance.popper).width);\n let popperOffset = parseFloat(window.getComputedStyle(data.instance.popper).left);\n let popperBorderWidth = parseFloat(stepElement.css('borderTopWidth'));\n let popperBorderRadiusWidth = parseFloat(stepElement.css('borderTopLeftRadius')) * 2;\n let arrowPos = arrowOffset + (arrowWidth / 2);\n let maxPos = popperWidth + popperOffset - popperBorderWidth - popperBorderRadiusWidth;\n let minPos = popperOffset + popperBorderWidth + popperBorderRadiusWidth;\n if (arrowPos >= maxPos || arrowPos <= minPos) {\n let newArrowPos = 0;\n if (arrowPos > (popperWidth / 2)) {\n newArrowPos = maxPos - arrowWidth;\n } else {\n newArrowPos = minPos + arrowWidth;\n }\n $(arrowElement).css('left', newArrowPos);\n }\n }\n };\n\n const recalculateStepPosition = function(data) {\n const placement = data.placement.split('-')[0];\n const isVertical = ['left', 'right'].indexOf(placement) !== -1;\n const popperElement = $(data.instance.popper);\n const targetElement = $(data.instance.reference);\n const arrowElement = popperElement.find('[data-role=\"arrow\"]');\n const stepElement = popperElement.find('[data-role=\"flexitour-step\"]');\n const viewportHeight = $(window).height();\n const viewportWidth = $(window).width();\n const arrowHeight = parseFloat(arrowElement.outerHeight(true));\n const popperHeight = parseFloat(popperElement.outerHeight(true));\n const targetHeight = parseFloat(targetElement.outerHeight(true));\n const arrowWidth = parseFloat(arrowElement.outerWidth(true));\n const popperWidth = parseFloat(popperElement.outerWidth(true));\n const targetWidth = parseFloat(targetElement.outerWidth(true));\n let maxHeight;\n\n if (thisT.recalculatedNo > 1) {\n // The current screen is too small, and cannot fit with the original placement.\n // We should set the placement to auto so the PopperJS can calculate the perfect placement.\n thisT.currentStepPopper.options.placement = isVertical ? 'auto-left' : 'auto-bottom';\n }\n if (thisT.recalculatedNo > 2) {\n // Return here to prevent recursive calling.\n return;\n }\n\n if (isVertical) {\n // Find the best place to put the tour: Left of right.\n const leftSpace = targetElement.offset().left > 0 ? targetElement.offset().left : 0;\n const rightSpace = viewportWidth - leftSpace - targetWidth;\n const remainingSpace = leftSpace >= rightSpace ? leftSpace : rightSpace;\n maxHeight = viewportHeight - MINSPACING * 2;\n if (remainingSpace < (popperWidth + arrowWidth)) {\n const maxWidth = remainingSpace - MINSPACING - arrowWidth;\n if (maxWidth > 0) {\n popperElement.css({\n 'max-width': maxWidth + 'px',\n });\n // Not enough space, flag true to make Popper to recalculate the position.\n thisT.possitionNeedToBeRecalculated = true;\n }\n } else if (maxHeight < popperHeight) {\n // Check if the Popper's height can fit the viewport height or not.\n // If not, set the correct max-height value for the Popper element.\n popperElement.css({\n 'max-height': maxHeight + 'px',\n });\n }\n } else {\n // Find the best place to put the tour: Top of bottom.\n const topSpace = targetElement.offset().top > 0 ? targetElement.offset().top : 0;\n const bottomSpace = viewportHeight - topSpace - targetHeight;\n const remainingSpace = topSpace >= bottomSpace ? topSpace : bottomSpace;\n maxHeight = remainingSpace - MINSPACING - arrowHeight;\n if (remainingSpace < (popperHeight + arrowHeight)) {\n // Not enough space, flag true to make Popper to recalculate the position.\n thisT.possitionNeedToBeRecalculated = true;\n }\n }\n\n // Check if the Popper's height can fit the viewport height or not.\n // If not, set the correct max-height value for the body.\n const currentStepBody = stepElement.find('[data-placeholder=\"body\"]').first();\n const headerEle = stepElement.find('.modal-header').first();\n const footerEle = stepElement.find('.modal-footer').first();\n const headerHeight = headerEle.outerHeight(true) ?? 0;\n const footerHeight = footerEle.outerHeight(true) ?? 0;\n maxHeight = maxHeight - headerHeight - footerHeight;\n if (maxHeight > 0) {\n headerEle.removeClass('minimal');\n footerEle.removeClass('minimal');\n currentStepBody.css({\n 'max-height': maxHeight + 'px',\n 'overflow': 'auto',\n });\n } else {\n headerEle.addClass('minimal');\n footerEle.addClass('minimal');\n }\n // Call the Popper update method to update the position.\n thisT.currentStepPopper.update();\n };\n\n let background = $('[data-flexitour=\"highlight\"]');\n if (background.length) {\n target = background;\n }\n this.currentStepPopper = new Popper(target, content[0], config);\n\n return this;\n }\n\n /**\n * For left/right placement, checks that there is room for the step at current window size.\n *\n * If there is not enough room, changes placement to 'top'.\n *\n * @method recalculatePlacement\n * @param {Object} stepConfig The step configuration of the step\n * @return {String} The placement after recalculate\n */\n recalculatePlacement(stepConfig) {\n const arrowWidth = 16;\n let target = this.getStepTarget(stepConfig);\n let widthContent = this.currentStepNode.width() + arrowWidth;\n let targetOffsetLeft = target.offset().left - BUFFER;\n let targetOffsetRight = target.offset().left + target.width() + BUFFER;\n let placement = stepConfig.placement;\n\n if (['left', 'right'].indexOf(placement) !== -1) {\n if ((targetOffsetLeft < (widthContent + BUFFER)) &&\n ((targetOffsetRight + widthContent + BUFFER) > document.documentElement.clientWidth)) {\n placement = 'top';\n }\n }\n return placement;\n }\n\n /**\n * Recaculate where the backdrop and its cut-out should be.\n *\n * This is needed when highlighted elements are off the page.\n * This can be called on update to recalculate it all.\n *\n * @method recalculateBackdropPosition\n * @param {Object} stepConfig The step configuration of the step\n */\n recalculateBackdropPosition(stepConfig) {\n if (stepConfig.backdrop) {\n this.positionBackdrop(stepConfig);\n }\n }\n\n /**\n * Add the backdrop.\n *\n * @method positionBackdrop\n * @param {Object} stepConfig The step configuration of the step\n * @chainable\n * @return {Object} this.\n */\n positionBackdrop(stepConfig) {\n if (stepConfig.backdrop) {\n this.currentStepConfig.hasBackdrop = true;\n\n // Position our backdrop above everything else.\n let backdrop = $('div[data-flexitour=\"backdrop\"]');\n if (!backdrop.length) {\n backdrop = $('
');\n $('body').append(backdrop);\n }\n\n if (this.isStepActuallyVisible(stepConfig)) {\n let targetNode = this.getStepTarget(stepConfig);\n targetNode.attr('data-flexitour', 'highlight');\n\n let distanceFromTop = targetNode[0].getBoundingClientRect().top;\n let relativeTop = targetNode.offset().top - distanceFromTop;\n\n /*\n Draw a clip-path that makes the backdrop a window.\n The clip-path is drawn with x/y coordinates in the following sequence.\n\n 1--------------------------------------------------2\n 11 |\n | |\n | 8-----------------------------7 |\n | | | |\n | | | |\n | | | |\n 10-------9 | |\n 5--------------------------------------6 |\n | |\n | |\n 4--------------------------------------------------3\n */\n\n // These values will help us draw the backdrop.\n const viewportHeight = $(window).height();\n const viewportWidth = $(window).width();\n const elementWidth = targetNode.outerWidth() + (BUFFER * 2);\n let elementHeight = targetNode.outerHeight() + (BUFFER * 2);\n const elementLeft = targetNode.offset().left - BUFFER;\n let elementTop = targetNode.offset().top - BUFFER - relativeTop;\n\n // Check the amount of navbar overlap the highlight element has.\n // We will adjust the backdrop shape to compensate for the fixed navbar.\n let navbarOverlap = 0;\n if (targetNode.parents('[data-usertour=\"scroller\"]').length) {\n // Determine the navbar height.\n const scrollerElement = targetNode.parents('[data-usertour=\"scroller\"]');\n const navbarHeight = scrollerElement.offset().top;\n navbarOverlap = Math.max(Math.ceil(navbarHeight - elementTop), 0);\n elementTop = elementTop + navbarOverlap;\n elementHeight = elementHeight - navbarOverlap;\n }\n\n // Check if the step container is in the 'top' position.\n // We will re-anchor the step container to the shifted backdrop edge as opposed to the actual element.\n if (this.currentStepNode && this.currentStepNode.length) {\n const xPlacement = this.currentStepNode[0].getAttribute('x-placement');\n if (xPlacement === 'top-start') {\n this.currentStepNode[0].style.top = `${navbarOverlap}px`;\n } else {\n this.currentStepNode[0].style.top = '0px';\n }\n }\n\n let backdropPath = document.querySelector('div[data-flexitour=\"backdrop\"]');\n const radius = 10;\n\n const bottomRight = {\n 'x1': elementLeft + elementWidth - radius,\n 'y1': elementTop + elementHeight,\n 'x2': elementLeft + elementWidth,\n 'y2': elementTop + elementHeight - radius,\n };\n\n const topRight = {\n 'x1': elementLeft + elementWidth,\n 'y1': elementTop + radius,\n 'x2': elementLeft + elementWidth - radius,\n 'y2': elementTop,\n };\n\n const topLeft = {\n 'x1': elementLeft + radius,\n 'y1': elementTop,\n 'x2': elementLeft,\n 'y2': elementTop + radius,\n };\n\n const bottomLeft = {\n 'x1': elementLeft,\n 'y1': elementTop + elementHeight - radius,\n 'x2': elementLeft + radius,\n 'y2': elementTop + elementHeight,\n };\n\n // L = line.\n // C = Bezier curve.\n // Z = Close path.\n backdropPath.style.clipPath = `path('M 0 0 \\\n L ${viewportWidth} 0 \\\n L ${viewportWidth} ${viewportHeight} \\\n L 0 ${viewportHeight} \\\n L 0 ${elementTop + elementHeight} \\\n L ${bottomRight.x1} ${bottomRight.y1} \\\n C ${bottomRight.x1} ${bottomRight.y1} ${bottomRight.x2} ${bottomRight.y1} ${bottomRight.x2} ${bottomRight.y2} \\\n L ${topRight.x1} ${topRight.y1} \\\n C ${topRight.x1} ${topRight.y1} ${topRight.x1} ${topRight.y2} ${topRight.x2} ${topRight.y2} \\\n L ${topLeft.x1} ${topLeft.y1} \\\n C ${topLeft.x1} ${topLeft.y1} ${topLeft.x2} ${topLeft.y1} ${topLeft.x2} ${topLeft.y2} \\\n L ${bottomLeft.x1} ${bottomLeft.y1} \\\n C ${bottomLeft.x1} ${bottomLeft.y1} ${bottomLeft.x1} ${bottomLeft.y2} ${bottomLeft.x2} ${bottomLeft.y2} \\\n L 0 ${elementTop + elementHeight} \\\n Z'\n )`;\n }\n }\n return this;\n }\n\n /**\n * Calculate the inheritted position.\n *\n * @method calculatePosition\n * @param {jQuery} elem The element to calculate position for\n * @return {String} Calculated position\n */\n calculatePosition(elem) {\n elem = $(elem);\n while (elem.length && elem[0] !== document) {\n let position = elem.css('position');\n if (position !== 'static') {\n return position;\n }\n elem = elem.parent();\n }\n\n return null;\n }\n\n /**\n * Perform accessibility changes for step shown.\n *\n * This will add aria-hidden=\"true\" to all siblings and parent siblings.\n *\n * @method accessibilityShow\n */\n accessibilityShow() {\n let stateHolder = 'data-has-hidden';\n let attrName = 'aria-hidden';\n let hideFunction = function(child) {\n let flexitourRole = child.data('flexitour');\n if (flexitourRole) {\n switch (flexitourRole) {\n case 'container':\n case 'target':\n return;\n }\n }\n\n let hidden = child.attr(attrName);\n if (!hidden) {\n child.attr(stateHolder, true);\n Aria.hide(child);\n }\n };\n\n this.currentStepNode.siblings().each(function(index, node) {\n hideFunction($(node));\n });\n this.currentStepNode.parentsUntil('body').siblings().each(function(index, node) {\n hideFunction($(node));\n });\n }\n\n /**\n * Perform accessibility changes for step hidden.\n *\n * This will remove any newly added aria-hidden=\"true\".\n *\n * @method accessibilityHide\n */\n accessibilityHide() {\n let stateHolder = 'data-has-hidden';\n let showFunction = function(child) {\n let hidden = child.attr(stateHolder);\n if (typeof hidden !== 'undefined') {\n child.removeAttr(stateHolder);\n Aria.unhide(child);\n }\n };\n\n $('[' + stateHolder + ']').each(function(index, node) {\n showFunction($(node));\n });\n }\n};\n\nexport default Tour;\n"],"names":["constructor","config","targetNode","currentElement","window","getComputedStyle","position","parentElement","init","eventHandlers","reset","originalConfiguration","configure","apply","this","arguments","possitionNeedToBeRecalculated","recalculatedNo","storage","sessionStorage","storageKey","tourName","e","hide","resetStepListeners","steps","currentStepNumber","eventName","forEach","handler","addEventHandler","resetStepDefaults","template","templateContent","checkMinimumRequirements","Error","length","loadOriginalConfiguration","stepDefaults","setStepDefaults","extend","element","placement","delay","moveOnClick","moveAfterTime","orphan","direction","getCurrentStepNumber","parseInt","setCurrentStepNumber","stepNumber","setItem","code","DOMException","QUOTA_EXCEEDED_ERR","removeItem","getNextStepNumber","nextStepNumber","isStepPotentiallyVisible","getStepConfig","getPreviousStepNumber","previousStepNumber","isLastStep","stepConfig","isStepActuallyVisible","getPotentiallyVisibleSteps","result","stepId","stepid","isCSSAllowed","target","getStepTarget","is","testCSSElement","document","createElement","classList","add","body","appendChild","isAllowed","display","remove","next","gotoStep","previous","endTour","_gotoStep","pendingPromise","PendingPromise","delayed","setTimeout","resolve","fn","dispatchEvent","eventTypes","stepRender","defaultPrevented","renderStep","stepRendered","normalizeStepConfig","$","reflex","moveAfterClick","content","attachTo","attachPoint","first","detail","cancelable","tour","push","processStepListeners","listeners","node","currentStepNode","args","proxy","handleKeyDown","parents","listener","on","off","currentStepConfig","getTemplateContent","find","html","title","nextBtn","endBtn","removeClass","addClass","prop","then","value","catch","attr","displaystepnumbers","stepsPotentiallyVisible","totalStepsPotentiallyVisible","total","addStepToPage","clone","animationTarget","stop","data","append","css","top","left","animate","scrollTop","calculateScrollTop","promise","positionBackdrop","positionStep","revealStep","bind","isOrphan","currentStepPopper","Popper","removeOnDestroy","arrowElement","modifiers","enabled","applyStyle","onLoad","onCreate","images","calculateStepPositionInPage","fadeIn","announceStep","focus","bodyRegion","headerRegion","accessibilityShow","tabbableSelector","keyCode","hasBackdrop","currentIndex","nextIndex","nextNode","focusRelevant","activeElement","stepTarget","tabbableNodes","dialogContainer","filter","index","has","each","shiftKey","closest","last","preventDefault","call","startTour","startAt","storageStartValue","getItem","storageStartAt","tourStart","tourRunning","tourStarted","restartTour","tourEnd","previousTarget","tourEnded","transition","stepHide","destroy","removeAttr","backdrop","backdropRemovalPromise","fadeOut","currentStepElement","accessibilityHide","stepHidden","show","getStepContainer","viewportHeight","height","scrollParent","hasFixedPosition","offset","Math","max","min","ceil","stepHeight","viewportWidth","width","stepWidth","MINSPACING","maxHeight","outerHeight","flipBehavior","thisT","recalculatePlacement","flip","behaviour","arrow","recalculateArrowPosition","recalculateStepPosition","onUpdate","recalculateBackdropPosition","split","isVertical","indexOf","instance","popper","querySelector","stepElement","arrowHeight","parseFloat","arrowOffset","popperHeight","popperOffset","popperBorderWidth","popperBorderRadiusWidth","arrowPos","maxPos","minPos","newArrowPos","arrowWidth","popperWidth","popperElement","targetElement","reference","targetHeight","outerWidth","targetWidth","options","leftSpace","rightSpace","remainingSpace","maxWidth","topSpace","bottomSpace","currentStepBody","headerEle","footerEle","update","background","widthContent","targetOffsetLeft","targetOffsetRight","documentElement","clientWidth","distanceFromTop","getBoundingClientRect","relativeTop","elementWidth","BUFFER","elementHeight","elementLeft","elementTop","navbarOverlap","navbarHeight","xPlacement","getAttribute","style","radius","bottomRight","topRight","topLeft","bottomLeft","clipPath","x1","y1","x2","y2","calculatePosition","elem","parent","hideFunction","child","flexitourRole","Aria","siblings","parentsUntil","unhide"],"mappings":"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"} \ No newline at end of file diff --git a/public/admin/tool/usertours/amd/src/tour.js b/public/admin/tool/usertours/amd/src/tour.js index 2bee5296661a0..5a3abbb377d59 100644 --- a/public/admin/tool/usertours/amd/src/tour.js +++ b/public/admin/tool/usertours/amd/src/tour.js @@ -808,9 +808,6 @@ const Tour = class { targetNode.data('flexitour', 'target'); - // Add the backdrop. - this.positionBackdrop(stepConfig); - $(document.body).append(currentStepNode); this.currentStepNode = currentStepNode; @@ -826,6 +823,7 @@ const Tour = class { .animate({ scrollTop: this.calculateScrollTop(stepConfig), }).promise().then(function() { + this.positionBackdrop(stepConfig); this.positionStep(stepConfig); this.revealStep(stepConfig); pendingPromise.resolve(); diff --git a/public/admin/tool/usertours/db/upgrade.php b/public/admin/tool/usertours/db/upgrade.php index 2eda4e62a430c..cab38bf4e218d 100644 --- a/public/admin/tool/usertours/db/upgrade.php +++ b/public/admin/tool/usertours/db/upgrade.php @@ -55,5 +55,8 @@ function xmldb_tool_usertours_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/admin/tool/xmldb/actions/XMLDBCheckAction.class.php b/public/admin/tool/xmldb/actions/XMLDBCheckAction.class.php index 9d4470dbd5238..848c2812db361 100644 --- a/public/admin/tool/xmldb/actions/XMLDBCheckAction.class.php +++ b/public/admin/tool/xmldb/actions/XMLDBCheckAction.class.php @@ -90,7 +90,7 @@ function invoke() { // If not confirmed, show confirmation box if (!$confirmed) { - $o = '
'.$header.'
'; + $o = '
'; $o.= '
'; $o.= '

' . $this->str[$this->introstr] . '

'; $o .= '
'; diff --git a/public/admin/tool/xmldb/actions/check_bigints/check_bigints.class.php b/public/admin/tool/xmldb/actions/check_bigints/check_bigints.class.php index 011d923f4e5fb..d42b1a41c02e3 100644 --- a/public/admin/tool/xmldb/actions/check_bigints/check_bigints.class.php +++ b/public/admin/tool/xmldb/actions/check_bigints/check_bigints.class.php @@ -99,7 +99,7 @@ protected function display_results(array $wrong_fields) { $dbman = $DB->get_manager(); $s = ''; - $r = ''; + $r = '
'; $r.= '
'; $r.= '

' . $this->str['searchresults'] . '

'; $r.= '

' . $this->str['wrongints'] . ': ' . count($wrong_fields) . '

'; diff --git a/public/admin/tool/xmldb/actions/check_defaults/check_defaults.class.php b/public/admin/tool/xmldb/actions/check_defaults/check_defaults.class.php index c09a767905184..8b30d1a35ed94 100644 --- a/public/admin/tool/xmldb/actions/check_defaults/check_defaults.class.php +++ b/public/admin/tool/xmldb/actions/check_defaults/check_defaults.class.php @@ -147,7 +147,7 @@ protected function display_results(array $wrongfields) { $dbman = $DB->get_manager(); $s = ''; - $r = ''; + $r = '
'; $r .= '
'; $r .= '

' . $this->str['searchresults'] . '

'; $r .= '

' . $this->str['wrongdefaults'] . ': ' . count($wrongfields) . '

'; diff --git a/public/admin/tool/xmldb/actions/check_foreign_keys/check_foreign_keys.class.php b/public/admin/tool/xmldb/actions/check_foreign_keys/check_foreign_keys.class.php index 2a92afcd982f9..3e2bed26e725c 100644 --- a/public/admin/tool/xmldb/actions/check_foreign_keys/check_foreign_keys.class.php +++ b/public/admin/tool/xmldb/actions/check_foreign_keys/check_foreign_keys.class.php @@ -159,7 +159,7 @@ protected function check_table(xmldb_table $xmldb_table, array $metacolumns) { } protected function display_results(array $violatedkeys) { - $r = ''; + $r = '
'; $r.= '
'; $r.= '

' . $this->str['searchresults'] . '

'; $r.= '

' . $this->str['violatedforeignkeys'] . ': ' . count($violatedkeys) . '

'; diff --git a/public/admin/tool/xmldb/actions/check_indexes/check_indexes.class.php b/public/admin/tool/xmldb/actions/check_indexes/check_indexes.class.php index bbbc47e9bd5f3..01bfc4a4d8754 100644 --- a/public/admin/tool/xmldb/actions/check_indexes/check_indexes.class.php +++ b/public/admin/tool/xmldb/actions/check_indexes/check_indexes.class.php @@ -154,7 +154,7 @@ protected function display_results(array $missing_indexes) { } $s = ''; - $r = ''; + $r = '
'; $r.= '
'; $r.= '

' . $this->str['searchresults'] . '

'; $r .= '

' . $this->str['missingindexes'] . ': ' . count($missingindexes) . '

'; diff --git a/public/admin/tool/xmldb/actions/delete_field/delete_field.class.php b/public/admin/tool/xmldb/actions/delete_field/delete_field.class.php index 922fef987c083..b1d768abbc09f 100644 --- a/public/admin/tool/xmldb/actions/delete_field/delete_field.class.php +++ b/public/admin/tool/xmldb/actions/delete_field/delete_field.class.php @@ -73,7 +73,7 @@ function invoke() { // If not confirmed, show confirmation box if (!$confirmed) { - $o = ''; + $o = '
'; $o.= '
'; $o.= '

' . $this->str['confirmdeletefield'] . '

' . $fieldparam . '

'; $o .= '
'; diff --git a/public/admin/tool/xmldb/actions/delete_index/delete_index.class.php b/public/admin/tool/xmldb/actions/delete_index/delete_index.class.php index 51e006a88bc8a..d30ec88e597d8 100644 --- a/public/admin/tool/xmldb/actions/delete_index/delete_index.class.php +++ b/public/admin/tool/xmldb/actions/delete_index/delete_index.class.php @@ -73,7 +73,7 @@ function invoke() { // If not confirmed, show confirmation box if (!$confirmed) { - $o = ''; + $o = '
'; $o.= '
'; $o.= '

' . $this->str['confirmdeleteindex'] . '

' . $indexparam . '

'; $o .= '
'; diff --git a/public/admin/tool/xmldb/actions/delete_key/delete_key.class.php b/public/admin/tool/xmldb/actions/delete_key/delete_key.class.php index bb824c5167649..11c4651834a92 100644 --- a/public/admin/tool/xmldb/actions/delete_key/delete_key.class.php +++ b/public/admin/tool/xmldb/actions/delete_key/delete_key.class.php @@ -73,7 +73,7 @@ function invoke() { // If not confirmed, show confirmation box if (!$confirmed) { - $o = ''; + $o = '
'; $o.= '
'; $o.= '

' . $this->str['confirmdeletekey'] . '

' . $keyparam . '

'; $o .= '
'; diff --git a/public/admin/tool/xmldb/actions/delete_table/delete_table.class.php b/public/admin/tool/xmldb/actions/delete_table/delete_table.class.php index b53c4d2a186a3..4fe689d17299d 100644 --- a/public/admin/tool/xmldb/actions/delete_table/delete_table.class.php +++ b/public/admin/tool/xmldb/actions/delete_table/delete_table.class.php @@ -72,7 +72,7 @@ function invoke() { // If not confirmed, show confirmation box if (!$confirmed) { - $o = ''; + $o = '
'; $o.= '
'; $o.= '

' . $this->str['confirmdeletetable'] . '

' . $tableparam . '

'; $o .= '
'; diff --git a/public/admin/tool/xmldb/actions/delete_xml_file/delete_xml_file.class.php b/public/admin/tool/xmldb/actions/delete_xml_file/delete_xml_file.class.php index 87cf29ad4d617..52d4d5cb5733c 100644 --- a/public/admin/tool/xmldb/actions/delete_xml_file/delete_xml_file.class.php +++ b/public/admin/tool/xmldb/actions/delete_xml_file/delete_xml_file.class.php @@ -71,7 +71,7 @@ function invoke() { // If not confirmed, show confirmation box if (!$confirmed) { - $o = ''; + $o = '
'; $o.= '
'; $o.= '

' . $this->str['confirmdeletexmlfile'] . '

' . $dirpath . '/install.php

'; $o .= '
'; diff --git a/public/admin/tool/xmldb/actions/main_view/main_view.class.php b/public/admin/tool/xmldb/actions/main_view/main_view.class.php index d8578473f779b..94d9e536d600c 100644 --- a/public/admin/tool/xmldb/actions/main_view/main_view.class.php +++ b/public/admin/tool/xmldb/actions/main_view/main_view.class.php @@ -119,7 +119,7 @@ function invoke() { // Display list of DB directories if everything is ok if ($result && !empty($XMLDB->dbdirs)) { $o .= ''; + ' class="table table-striped table-sm admintable generaltable table-hover">'; $row = 0; foreach ($XMLDB->dbdirs as $key => $dbdir) { // Detect if this is the lastused dir diff --git a/public/admin/tool/xmldb/actions/reconcile_files/reconcile_files.class.php b/public/admin/tool/xmldb/actions/reconcile_files/reconcile_files.class.php index 7536ad2b6ce80..5901bc5a7b069 100644 --- a/public/admin/tool/xmldb/actions/reconcile_files/reconcile_files.class.php +++ b/public/admin/tool/xmldb/actions/reconcile_files/reconcile_files.class.php @@ -90,8 +90,15 @@ public function invoke() { // Generate the XML contents from the loaded structure. $xmlcontents = $xmldb->getStructure()->xmlOutput(); + $correctdom = new \DOMDocument(); + $correctdom->loadXML($xmlcontents); + $correct = $correctdom->saveXML(); - if ($rawcontents != $xmlcontents) { + $currentdom = new \DOMDocument(); + $currentdom->loadXML($rawcontents); + $current = $currentdom->saveXML(); + + if ($current !== $correct) { $relpath = str_replace($CFG->dirroot . '/', '', $key) . '/install.xml'; $needfix[] = $relpath; // Left here on purpose, as a quick way to fix problems. To be diff --git a/public/admin/tool/xmldb/actions/revert_changes/revert_changes.class.php b/public/admin/tool/xmldb/actions/revert_changes/revert_changes.class.php index fa96bda039972..079ff94e55f8f 100644 --- a/public/admin/tool/xmldb/actions/revert_changes/revert_changes.class.php +++ b/public/admin/tool/xmldb/actions/revert_changes/revert_changes.class.php @@ -71,7 +71,7 @@ function invoke() { // If not confirmed, show confirmation box if (!$confirmed) { - $o = '
'; + $o = '
'; $o.= '
'; $o.= '

' . $this->str['confirmrevertchanges'] . '

' . $dirpath . '

'; $o .= '
'; diff --git a/public/admin/tool/xmldb/index.php b/public/admin/tool/xmldb/index.php index 5a52dc961efa3..ab1508eff75ca 100644 --- a/public/admin/tool/xmldb/index.php +++ b/public/admin/tool/xmldb/index.php @@ -47,6 +47,7 @@ // Some previous checks $site = get_site(); +$PAGE->set_primary_active_tab('siteadminnode'); // Body of the script, based on action, we delegate the work $action = optional_param ('action', 'main_view', PARAM_ALPHAEXT); diff --git a/public/admin/webservice/forms.php b/public/admin/webservice/forms.php index 4f15173f35c2f..4f5d837c380c5 100644 --- a/public/admin/webservice/forms.php +++ b/public/admin/webservice/forms.php @@ -196,8 +196,12 @@ function definition() { //we add the descriptions to the functions foreach ($functions as $functionid => $functionname) { //retrieve full function information (including the description) - $function = \core_external\external_api::external_function_info($functionname); - if (empty($function->deprecated)) { + try { + $function = \core_external\external_api::external_function_info($functionname); + } catch (Throwable $exception) { + $function = null; + } + if ($function !== null && empty($function->deprecated)) { $functions[$functionid] = $function->name . ':' . $function->description; } else { // Exclude the deprecated ones. diff --git a/public/ai/configure.php b/public/ai/configure.php index 50b787a6f761c..9f9e8141f7ec3 100644 --- a/public/ai/configure.php +++ b/public/ai/configure.php @@ -72,6 +72,12 @@ $PAGE->set_title($title); $PAGE->set_heading($title); +// Explode if there are no provider plugins installed. +$plugins = core_plugin_manager::instance()->get_plugins_of_type('aiprovider'); +if (empty($plugins)) { + throw new moodle_exception('noproviderplugins', 'core_ai'); +} + // Provider instance form processing. $mform = new \core_ai\form\ai_provider_form(customdata: $data); if ($mform->is_cancelled()) { diff --git a/public/ai/placement/courseassist/classes/external/explain_text.php b/public/ai/placement/courseassist/classes/external/explain_text.php index f2856ca82f16b..36532fe998534 100644 --- a/public/ai/placement/courseassist/classes/external/explain_text.php +++ b/public/ai/placement/courseassist/classes/external/explain_text.php @@ -94,10 +94,11 @@ public static function execute( // Send the action to the AI manager. $manager = \core\di::get(\core_ai\manager::class); $response = $manager->process_action($action); + $generatedcontent = $response->get_response_data()['generatedcontent'] ?? ''; // Return the response. return [ 'success' => $response->get_success(), - 'generatedcontent' => $response->get_response_data()['generatedcontent'] ?? '', + 'generatedcontent' => \core_external\util::format_text($generatedcontent, FORMAT_PLAIN, $contextid)[0], 'finishreason' => $response->get_response_data()['finishreason'] ?? '', 'errorcode' => $response->get_errorcode(), 'error' => $response->get_error(), diff --git a/public/ai/placement/courseassist/classes/external/summarise_text.php b/public/ai/placement/courseassist/classes/external/summarise_text.php index 0d6ca4b95e8a1..da1d82c655733 100644 --- a/public/ai/placement/courseassist/classes/external/summarise_text.php +++ b/public/ai/placement/courseassist/classes/external/summarise_text.php @@ -94,10 +94,11 @@ public static function execute( // Send the action to the AI manager. $manager = \core\di::get(\core_ai\manager::class); $response = $manager->process_action($action); + $generatedcontent = $response->get_response_data()['generatedcontent'] ?? ''; // Return the response. return [ 'success' => $response->get_success(), - 'generatedcontent' => $response->get_response_data()['generatedcontent'] ?? '', + 'generatedcontent' => \core_external\util::format_text($generatedcontent, FORMAT_PLAIN, $contextid)[0], 'finishreason' => $response->get_response_data()['finishreason'] ?? '', 'errorcode' => $response->get_errorcode(), 'error' => $response->get_error(), diff --git a/public/ai/provider/ollama/classes/provider.php b/public/ai/provider/ollama/classes/provider.php index ef17f88282930..4284681cca740 100644 --- a/public/ai/provider/ollama/classes/provider.php +++ b/public/ai/provider/ollama/classes/provider.php @@ -55,7 +55,7 @@ public static function get_action_settings( #[\Override] public function add_authentication_headers(RequestInterface $request): RequestInterface { - if (empty($this->config['basicauthenabled'])) { + if (empty($this->config['enablebasicauth'])) { return $request; } else { // Add the Authorization header for basic auth. diff --git a/public/ai/tests/behat/admin.feature b/public/ai/tests/behat/admin.feature index 60d5cc002baf2..af7ecdf975e8c 100644 --- a/public/ai/tests/behat/admin.feature +++ b/public/ai/tests/behat/admin.feature @@ -40,15 +40,23 @@ Feature: An administrator can manage AI subsystem settings And I navigate to "AI > AI providers" in site administration And I should see "OpenAI API test" And I should see "Azure AI API test" + And I hover "Enable OpenAI API test" "field" + And "Enable OpenAI API test" "text" should be visible And I toggle the "Enable OpenAI API test" admin switch "on" And I should see "OpenAI API test enabled." + And I hover "Enable Azure AI API test" "field" + And "Enable Azure AI API test" "text" should be visible And I toggle the "Enable Azure AI API test" admin switch "on" And I should see "Azure AI API test enabled." And I reload the page And I should see "Disable OpenAI API test" And I should see "Disable Azure AI API test" + And I hover "Disable OpenAI API test" "field" + And "Disable OpenAI API test" "text" should be visible And I toggle the "Disable OpenAI API test" admin switch "off" And I should see "OpenAI API test disabled." + And I hover "Disable Azure AI API test" "field" + And "Disable Azure AI API test" "text" should be visible And I toggle the "Disable Azure AI API test" admin switch "off" Then I should see "Azure AI API test disabled." diff --git a/public/analytics/classes/local/analyser/by_course.php b/public/analytics/classes/local/analyser/by_course.php index 59b66273605d7..2264971af5e20 100644 --- a/public/analytics/classes/local/analyser/by_course.php +++ b/public/analytics/classes/local/analyser/by_course.php @@ -53,6 +53,7 @@ public function get_analysables_iterator(?string $action = null, array $contexts if (!$recordset->valid()) { $this->add_log(get_string('nocourses', 'analytics')); + $recordset->close(); return new \ArrayIterator([]); } diff --git a/public/analytics/tests/model_test.php b/public/analytics/tests/model_test.php index 3912a8d11c5c6..02b841b3d860f 100644 --- a/public/analytics/tests/model_test.php +++ b/public/analytics/tests/model_test.php @@ -54,7 +54,7 @@ final class model_test extends \advanced_testcase { public function setUp(): void { parent::setUp(); - + $this->resetAfterTest(); $this->setAdminUser(); $target = \core_analytics\manager::get_target('test_target_shortname'); @@ -68,8 +68,6 @@ public function setUp(): void { } public function test_enable(): void { - $this->resetAfterTest(true); - $this->assertEquals(0, $this->model->get_model_obj()->enabled); $this->assertEquals(0, $this->model->get_model_obj()->trained); $this->assertEquals('', $this->model->get_model_obj()->timesplitting); @@ -81,8 +79,6 @@ public function test_enable(): void { } public function test_create(): void { - $this->resetAfterTest(true); - $target = \core_analytics\manager::get_target('\core_course\analytics\target\course_dropout'); $indicators = array( \core_analytics\manager::get_indicator('\core\analytics\indicator\any_write_action'), @@ -194,7 +190,6 @@ public function test_clear(): void { */ public function test_clear_static(): void { global $DB; - $this->resetAfterTest(); $statictarget = new \test_static_target_shortname(); $indicators['test_indicator_max'] = \core_analytics\manager::get_indicator('test_indicator_max'); @@ -211,8 +206,6 @@ public function test_clear_static(): void { } public function test_model_manager(): void { - $this->resetAfterTest(true); - $this->assertCount(3, $this->model->get_indicators()); $this->assertInstanceOf('\core_analytics\local\target\binary', $this->model->get_target()); @@ -225,8 +218,6 @@ public function test_model_manager(): void { } public function test_output_dir(): void { - $this->resetAfterTest(true); - $dir = make_request_directory(); set_config('modeloutputdir', $dir, 'analytics'); @@ -237,9 +228,6 @@ public function test_output_dir(): void { public function test_unique_id(): void { global $DB; - - $this->resetAfterTest(true); - $originaluniqueid = $this->model->get_unique_id(); // Same id across instances. @@ -283,8 +271,6 @@ public function test_unique_id(): void { * @return void */ public function test_exists(): void { - $this->resetAfterTest(true); - $target = \core_analytics\manager::get_target('\core_course\analytics\target\no_teaching'); $this->assertTrue(\core_analytics\model::exists($target)); @@ -302,9 +288,6 @@ public function test_exists(): void { */ public function test_model_timelimit(): void { global $DB; - - $this->resetAfterTest(true); - set_config('modeltimelimit', 2, 'analytics'); $courses = array(); @@ -365,8 +348,6 @@ public function test_model_timelimit(): void { * Test model_config::get_class_component. */ public function test_model_config_get_class_component(): void { - $this->resetAfterTest(true); - $this->assertEquals('core', \core_analytics\model_config::get_class_component('\\core\\analytics\\indicator\\read_actions')); $this->assertEquals('core', @@ -387,8 +368,6 @@ public function test_import_model_config(): void { $this->markTestSkipped('mlbackend_python is not configured.'); } - $this->resetAfterTest(true); - $this->model->enable('\\core\\analytics\\time_splitting\\quarters'); $zipfilepath = $this->model->export_model('yeah-config.zip'); @@ -408,8 +387,6 @@ public function test_import_model_config(): void { * Test can export configuration */ public function test_can_export_configuration(): void { - $this->resetAfterTest(true); - // No time splitting method. $this->assertFalse($this->model->can_export_configuration()); @@ -433,8 +410,6 @@ public function test_export_config(): void { $this->markTestSkipped('mlbackend_python is not configured.'); } - $this->resetAfterTest(true); - $this->model->enable('\\core\\analytics\\time_splitting\\quarters'); $modelconfig = new \core_analytics\model_config($this->model); @@ -463,8 +438,6 @@ public function test_export_config(): void { public function test_inplace_editable_name(): void { global $PAGE; - $this->resetAfterTest(); - $output = new \core_renderer($PAGE, RENDERER_TARGET_GENERAL); // Check as a user with permission to edit the name. @@ -489,8 +462,6 @@ public function test_inplace_editable_name(): void { public function test_get_name_and_rename(): void { global $PAGE; - $this->resetAfterTest(); - $output = new \core_renderer($PAGE, RENDERER_TARGET_GENERAL); // By default, the model exported for template uses its target's name in the name inplace editable element. @@ -518,8 +489,6 @@ public function test_get_name_and_rename(): void { * Tests model::get_potential_timesplittings() */ public function test_potential_timesplittings(): void { - $this->resetAfterTest(); - $this->assertArrayNotHasKey('\core\analytics\time_splitting\no_splitting', $this->model->get_potential_timesplittings()); $this->assertArrayHasKey('\core\analytics\time_splitting\single_range', $this->model->get_potential_timesplittings()); $this->assertArrayHasKey('\core\analytics\time_splitting\quarters', $this->model->get_potential_timesplittings()); @@ -531,8 +500,6 @@ public function test_potential_timesplittings(): void { * @return null */ public function test_get_samples(): void { - $this->resetAfterTest(); - if (!PHPUNIT_LONGTEST) { $this->markTestSkipped('PHPUNIT_LONGTEST is not defined'); } diff --git a/public/analytics/tests/stats_test.php b/public/analytics/tests/stats_test.php index a28ec7fe5f0b1..9fb99d7b44489 100644 --- a/public/analytics/tests/stats_test.php +++ b/public/analytics/tests/stats_test.php @@ -39,7 +39,7 @@ final class stats_test extends \advanced_testcase { */ public function setUp(): void { parent::setUp(); - + $this->resetAfterTest(); $this->setAdminUser(); } @@ -47,9 +47,6 @@ public function setUp(): void { * Test the {@link \core_analytics\stats::enabled_models()} implementation. */ public function test_enabled_models(): void { - - $this->resetAfterTest(true); - // By default, sites have {@link \core_course\analytics\target\no_teaching} and // {@link \core_user\analytics\target\upcoming_activities_due} enabled. $this->assertEquals(4, \core_analytics\stats::enabled_models()); @@ -77,8 +74,6 @@ public function test_predictions(): void { $this->markTestSkipped('mlbackend_python is not configured.'); } - $this->resetAfterTest(true); - $model = \core_analytics\model::create( \core_analytics\manager::get_target('test_target_shortname'), [ @@ -127,8 +122,6 @@ public function test_actions(): void { $this->markTestSkipped('mlbackend_python is not configured.'); } - $this->resetAfterTest(true); - $model = \core_analytics\model::create( \core_analytics\manager::get_target('test_target_shortname'), [ diff --git a/public/auth/classes/external.php b/public/auth/classes/external.php index 498c1879607a1..d6b44fdd1119a 100644 --- a/public/auth/classes/external.php +++ b/public/auth/classes/external.php @@ -370,7 +370,7 @@ public static function resend_confirmation_email_parameters() { * @throws moodle_exception */ public static function resend_confirmation_email($username, $password, $redirect = '') { - global $PAGE; + global $PAGE, $CFG; $warnings = array(); $params = self::validate_parameters( @@ -387,20 +387,27 @@ public static function resend_confirmation_email($username, $password, $redirect $username = trim(core_text::strtolower($params['username'])); $password = $params['password']; + $user = core_user::get_user_by_username($username); + + if (!empty($user) && $user->confirmed) { + if (!empty($CFG->protectusernames)) { + throw new moodle_exception('invalidlogin'); + } + throw new moodle_exception('alreadyconfirmed'); + } + if (is_restored_user($username)) { + if (!empty($CFG->protectusernames)) { + throw new moodle_exception('invalidlogin'); + } throw new moodle_exception('restoredaccountresetpassword', 'webservice'); } $user = authenticate_user_login($username, $password); - if (empty($user)) { throw new moodle_exception('invalidlogin'); } - if ($user->confirmed) { - throw new moodle_exception('alreadyconfirmed'); - } - // Check if we should redirect the user once the user is confirmed. $confirmationurl = null; if (!empty($params['redirect'])) { diff --git a/public/auth/db/classes/task/sync_users.php b/public/auth/db/classes/task/sync_users.php index a7ac128425eee..ca998d2bc868f 100644 --- a/public/auth/db/classes/task/sync_users.php +++ b/public/auth/db/classes/task/sync_users.php @@ -14,18 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Sync users task - * @package auth_db - * @author Guy Thomas - * @copyright Copyright (c) 2017 Blackboard Inc. - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace auth_db\task; -defined('MOODLE_INTERNAL') || die(); - /** * Sync users task class * @package auth_db @@ -49,7 +39,7 @@ public function get_name() { */ public function execute() { if (!is_enabled_auth('db')) { - mtrace('auth_db plugin is disabled, synchronisation stopped', 2); + mtrace('auth_db plugin is disabled, synchronisation stopped'); return; } diff --git a/public/auth/db/db/upgrade.php b/public/auth/db/db/upgrade.php index d33a501978208..2c72183457298 100644 --- a/public/auth/db/db/upgrade.php +++ b/public/auth/db/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_auth_db_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/auth/email/db/upgrade.php b/public/auth/email/db/upgrade.php index c56ddfb089ddf..5a02dbb7678ff 100644 --- a/public/auth/email/db/upgrade.php +++ b/public/auth/email/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_auth_email_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/auth/ldap/db/upgrade.php b/public/auth/ldap/db/upgrade.php index b13a72639af3f..389928bcebb20 100644 --- a/public/auth/ldap/db/upgrade.php +++ b/public/auth/ldap/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_auth_ldap_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/auth/lti/auth.php b/public/auth/lti/auth.php index c5e13fd5b459d..c621eda98f15a 100644 --- a/public/auth/lti/auth.php +++ b/public/auth/lti/auth.php @@ -104,6 +104,7 @@ public function user_login($username, $password) { * @param int $provisioningmode the desired account provisioning mode, which controls the auth flow for unbound users. * @param array $legacyconsumersecrets an array of secrets used by the legacy consumer if a migration claim exists. * @throws coding_exception if the specified provisioning mode is invalid. + * @throws \core\exception\moodle_exception if user authentication fails. */ public function complete_login(array $launchdata, moodle_url $returnurl, int $provisioningmode, array $legacyconsumersecrets = []): void { @@ -112,6 +113,19 @@ public function complete_login(array $launchdata, moodle_url $returnurl, int $pr if ($this->get_user_binding($launchdata['iss'], $launchdata['sub'])) { $user = $this->find_or_create_user_from_launch($launchdata); + if ($user->suspended) { + $failurereason = AUTH_LOGIN_SUSPENDED; + $event = \core\event\user_login_failed::create([ + 'userid' => $user->id, + 'other' => [ + 'username' => $user->username, + 'reason' => $failurereason + ] + ]); + $event->trigger(); + throw new \core\exception\moodle_exception('invalidlogin', 'core'); + } + if (isloggedin()) { // If a different user is currently logged in, authenticate the linked user instead. global $USER; diff --git a/public/auth/lti/db/upgrade.php b/public/auth/lti/db/upgrade.php index f00cbb1dd7926..43f1c0de93c6d 100644 --- a/public/auth/lti/db/upgrade.php +++ b/public/auth/lti/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_auth_lti_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/auth/manual/db/upgrade.php b/public/auth/manual/db/upgrade.php index a8e350b3e8201..f5f56e53519d8 100644 --- a/public/auth/manual/db/upgrade.php +++ b/public/auth/manual/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_auth_manual_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/auth/none/db/upgrade.php b/public/auth/none/db/upgrade.php index aa841ff89f65f..bee69aa11d70e 100644 --- a/public/auth/none/db/upgrade.php +++ b/public/auth/none/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_auth_none_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/auth/oauth2/classes/api.php b/public/auth/oauth2/classes/api.php index 1192b5f0bf9f1..c05768cedee68 100644 --- a/public/auth/oauth2/classes/api.php +++ b/public/auth/oauth2/classes/api.php @@ -197,9 +197,7 @@ public static function send_confirm_link_login_email($userinfo, $issuer, $userid $data->link = $confirmationurl->out(false); $message = get_string('confirmlinkedloginemail', 'auth_oauth2', $data); - - $data->link = $confirmationurl->out(); - $messagehtml = text_to_html(get_string('confirmlinkedloginemail', 'auth_oauth2', $data), false, false, true); + $messagehtml = text_to_html(get_string('confirmlinkedloginemail', 'auth_oauth2', $data), false, false); $user->mailformat = 1; // Always send HTML version as well. @@ -339,9 +337,7 @@ public static function send_confirm_account_email($userinfo, $issuer) { $data->link = $confirmationurl->out(false); $message = get_string('confirmaccountemail', 'auth_oauth2', $data); - - $data->link = $confirmationurl->out(); - $messagehtml = text_to_html(get_string('confirmaccountemail', 'auth_oauth2', $data), false, false, true); + $messagehtml = text_to_html(get_string('confirmaccountemail', 'auth_oauth2', $data), false, false); $user->mailformat = 1; // Always send HTML version as well. diff --git a/public/auth/oauth2/classes/auth.php b/public/auth/oauth2/classes/auth.php index c591d6528af1d..f2637e5aca6ab 100644 --- a/public/auth/oauth2/classes/auth.php +++ b/public/auth/oauth2/classes/auth.php @@ -34,9 +34,11 @@ use core\oauth2\issuer; use core\oauth2\client; +global $CFG; require_once($CFG->libdir.'/authlib.php'); require_once($CFG->dirroot.'/user/lib.php'); require_once($CFG->dirroot.'/user/profile/lib.php'); +require_once($CFG->dirroot.'/login/lib.php'); /** * Plugin for oauth2 authentication. @@ -175,12 +177,12 @@ public function get_userinfo($username) { public function loginpage_idp_list($wantsurl) { $providers = \core\oauth2\api::get_all_issuers(true); $result = []; - if (empty($wantsurl)) { - $wantsurl = '/'; - } foreach ($providers as $idp) { if ($idp->is_available_for_login()) { - $params = ['id' => $idp->get('id'), 'wantsurl' => $wantsurl, 'sesskey' => sesskey()]; + $params = ['id' => $idp->get('id'), 'sesskey' => sesskey()]; + if (!empty($wantsurl)) { + $params['wantsurl'] = $wantsurl; + } $url = new moodle_url('/auth/oauth2/login.php', $params); $icon = $idp->get('image'); $result[] = ['url' => $url, 'iconurl' => $icon, 'name' => $idp->get_display_name()]; @@ -465,18 +467,31 @@ public function complete_login(client $client, $redirecturl) { $mappeduser = get_complete_user_data('id', $linkedlogin->get('userid')); if ($mappeduser && $mappeduser->suspended) { - $failurereason = AUTH_LOGIN_SUSPENDED; - $event = \core\event\user_login_failed::create([ - 'userid' => $mappeduser->id, - 'other' => [ - 'username' => $userinfo['username'], - 'reason' => $failurereason - ] - ]); - $event->trigger(); - $SESSION->loginerrormsg = get_string('invalidlogin'); - $client->log_out(); - redirect(new moodle_url('/login/index.php')); + // Check if there's another user with the same email that is not suspended. + $moodleuser = \core_user::get_user_by_email($userinfo['email'], '*', null, IGNORE_MULTIPLE); + if ($moodleuser->id == $mappeduser->id) { + $failurereason = AUTH_LOGIN_SUSPENDED; + $event = \core\event\user_login_failed::create([ + 'userid' => $mappeduser->id, + 'other' => [ + 'username' => $userinfo['username'], + 'reason' => $failurereason, + ], + ]); + $event->trigger(); + $SESSION->loginerrormsg = get_string('invalidlogin'); + $client->log_out(); + redirect(new moodle_url('/login/index.php')); + } else if ($moodleuser && !$moodleuser->suspended) { + // Update the OAuth2 linked login to point to the active user account. + $linkedlogin->set('userid', $moodleuser->id); + $linkedlogin->set('timemodified', time()); + $linkedlogin->update(); + + // Update user fields and continue with login. + $userinfo = $this->update_user($userinfo, $moodleuser); + $userwasmapped = true; + } } else if ($mappeduser && ($mappeduser->confirmed || !$issuer->get('requireconfirmation'))) { // Update user fields. $userinfo = $this->update_user($userinfo, $mappeduser); @@ -506,7 +521,6 @@ public function complete_login(client $client, $redirecturl) { redirect(new moodle_url('/login/index.php')); } - if (!$issuer->is_valid_login_domain($oauthemail)) { // Trigger login failed event. $failurereason = AUTH_LOGIN_UNAUTHORISED; @@ -522,8 +536,24 @@ public function complete_login(client $client, $redirecturl) { if (!$userwasmapped) { // No defined mapping - we need to see if there is an existing account with the same email. + $moodleuser = \core_user::get_user_by_email($userinfo['email'], '*', null, IGNORE_MULTIPLE); + + // Ensure we don't link a login for a suspended user. + if (!empty($moodleuser) && $moodleuser->suspended) { + $failurereason = AUTH_LOGIN_SUSPENDED; + $event = \core\event\user_login_failed::create([ + 'userid' => $moodleuser->id, + 'other' => [ + 'username' => $userinfo['email'], + 'reason' => $failurereason, + ], + ]); + $event->trigger(); + $SESSION->loginerrormsg = get_string('invalidlogin'); + $client->log_out(); + redirect(new moodle_url('/login/index.php')); + } - $moodleuser = \core_user::get_user_by_email($userinfo['email']); if (!empty($moodleuser)) { if ($issuer->get('requireconfirmation')) { $PAGE->set_url('/auth/oauth2/confirm-link-login.php'); @@ -620,7 +650,11 @@ public function complete_login(client $client, $redirecturl) { complete_user_login($user, $this->get_extrauserinfo()); $this->update_picture($user); - redirect($redirecturl); + + if (empty($redirecturl)) { + $redirecturl = core_login_get_return_url(); + } + redirect(new moodle_url($redirecturl)); } /** diff --git a/public/auth/oauth2/classes/output/renderer.php b/public/auth/oauth2/classes/output/renderer.php index edede9cc927f0..1366f1780898e 100644 --- a/public/auth/oauth2/classes/output/renderer.php +++ b/public/auth/oauth2/classes/output/renderer.php @@ -55,7 +55,7 @@ public function linked_logins_table($linkedlogins) { get_string('info', 'auth_oauth2'), get_string('edit'), ]; - $table->attributes['class'] = 'admintable table generaltable'; + $table->attributes['class'] = 'admintable table generaltable table-hover'; $data = []; $index = 0; diff --git a/public/auth/oauth2/db/upgrade.php b/public/auth/oauth2/db/upgrade.php index 47dc0d9574337..e20c864a6f991 100644 --- a/public/auth/oauth2/db/upgrade.php +++ b/public/auth/oauth2/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_auth_oauth2_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/auth/oauth2/lang/en/auth_oauth2.php b/public/auth/oauth2/lang/en/auth_oauth2.php index 166cea78df4a0..4e942ac1e2b40 100644 --- a/public/auth/oauth2/lang/en/auth_oauth2.php +++ b/public/auth/oauth2/lang/en/auth_oauth2.php @@ -27,19 +27,14 @@ $string['auth_oauth2settings'] = 'OAuth 2 authentication settings.'; $string['confirmaccountemail'] = 'Hi {$a->firstname}, -A new account has been requested at \'{$a->sitename}\' -using your email address. +A new account has been requested at \'{$a->sitename}\' using your email address. -To confirm your new account, please go to this web address: +To confirm your new account, please click the link below: -{$a->link} +Confirm your account -In most mail programs, this should appear as a blue link -which you can just click on. If that doesn\'t work, -then cut and paste the address into the address -line at the top of your web browser window. -If you need help, please contact the site administrator, +If you need help, please contact the site administrator. {$a->admin} If you did not do this, someone else could be trying to compromise your account. @@ -53,16 +48,12 @@ {$a->linkedemail} to your account at \'{$a->sitename}\' using your email address. -To confirm this request and link these logins, please go to this web address: +To confirm this request and link these logins, please click the link below: -{$a->link} +Link your accounts -In most mail programs, this should appear as a blue link -which you can just click on. If that doesn\'t work, -then cut and paste the address into the address -line at the top of your web browser window. -If you need help, please contact the site administrator, +If you need help, please contact the site administrator. {$a->admin} If you did not do this, someone else could be trying to compromise your account. diff --git a/public/auth/oauth2/login.php b/public/auth/oauth2/login.php index 949b5a10e4c5d..c2d39169d3088 100644 --- a/public/auth/oauth2/login.php +++ b/public/auth/oauth2/login.php @@ -25,7 +25,7 @@ require_once('../../config.php'); $issuerid = required_param('id', PARAM_INT); -$wantsurl = new moodle_url(optional_param('wantsurl', '', PARAM_URL)); +$wantsurl = optional_param('wantsurl', '', PARAM_LOCALURL); $PAGE->set_context(context_system::instance()); $PAGE->set_url(new moodle_url('/auth/oauth2/login.php', ['id' => $issuerid])); diff --git a/public/auth/shibboleth/db/upgrade.php b/public/auth/shibboleth/db/upgrade.php index 3e6a7562ffa53..99b332cd35f9a 100644 --- a/public/auth/shibboleth/db/upgrade.php +++ b/public/auth/shibboleth/db/upgrade.php @@ -43,5 +43,8 @@ function xmldb_auth_shibboleth_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/auth/tests/behat/loginform.feature b/public/auth/tests/behat/loginform.feature index 7942678268dbe..2f6c37d7e2b3e 100644 --- a/public/auth/tests/behat/loginform.feature +++ b/public/auth/tests/behat/loginform.feature @@ -96,13 +96,15 @@ Feature: Test if the login form provides the correct feedback And I follow "Log in" Then the focused element is "Password" "field" + @accessibility Scenario: Test the login page focus after error feature Given I follow "Log in" And I set the field "Username" to "admin" And I set the field "Password" to "wrongpassword" And I press "Log in" - And I press the tab key + And I wait until the page is ready Then the focused element is "Username" "field" + And the page should meet accessibility standards with "best-practice" extra tests Scenario: Display the password visibility toggle icon Given the following config values are set as admin: diff --git a/public/auth/tests/external/external_test.php b/public/auth/tests/external/external_test.php index d6f36576c66d8..bd34f001f33b2 100644 --- a/public/auth/tests/external/external_test.php +++ b/public/auth/tests/external/external_test.php @@ -164,6 +164,7 @@ public function test_resend_confirmation_email_invalid_username(): void { $this->assertTrue($result['success']); $this->assertEmpty($result['warnings']); + set_config('protectusernames', 0); $_SERVER['HTTP_USER_AGENT'] = 'no browser'; // Hack around missing user agent in CLI scripts. $this->expectException('\moodle_exception'); $this->expectExceptionMessage('error/invalidlogin'); @@ -187,6 +188,7 @@ public function test_resend_confirmation_email_invalid_password(): void { $this->assertTrue($result['success']); $this->assertEmpty($result['warnings']); + set_config('protectusernames', 0); $_SERVER['HTTP_USER_AGENT'] = 'no browser'; // Hack around missing user agent in CLI scripts. $this->expectException('\moodle_exception'); $this->expectExceptionMessage('error/invalidlogin'); @@ -217,6 +219,13 @@ public function test_resend_confirmation_email_already_confirmed_user(): void { $result = external_api::clean_returnvalue(core_auth_external::confirm_user_returns(), $result); $this->assertTrue($result['success']); + // Keep protectusernames enabled so the call returns invalidlogin exception. + $this->expectException('\moodle_exception'); + $this->expectExceptionMessage('error/invalidlogin'); + core_auth_external::resend_confirmation_email($username, $password); + + // Now disable protectusernames and expect an exception. + set_config('protectusernames', 0); $this->expectException('\moodle_exception'); $this->expectExceptionMessage('error/alreadyconfirmed'); core_auth_external::resend_confirmation_email($username, $password); diff --git a/public/availability/classes/info.php b/public/availability/classes/info.php index 5f3fdff380225..a573374bfeaaa 100644 --- a/public/availability/classes/info.php +++ b/public/availability/classes/info.php @@ -746,7 +746,11 @@ function($matches) use($modinfo, $context) { $modulename = format_string($cm->get_name(), true, ['context' => $context]); // We make sure that we add a data attribute to the name so we can change it later if the // original module name changes. - if ($cm->has_view() && $cm->get_user_visible()) { + if ( + \course_modinfo::is_mod_type_visible_on_course($cm->modname) + && $cm->has_view() + && $cm->get_user_visible() + ) { // Help student by providing a link to the module which is preventing availability. return \html_writer::link($cm->get_url(), $modulename, ['data-cm-name-for' => $cm->id]); } else { diff --git a/public/availability/condition/completion/tests/behat/availability_completion_previous.feature b/public/availability/condition/completion/tests/behat/availability_completion_previous.feature index 8dff21f5336f1..5e0880de12334 100644 --- a/public/availability/condition/completion/tests/behat/availability_completion_previous.feature +++ b/public/availability/condition/completion/tests/behat/availability_completion_previous.feature @@ -32,7 +32,7 @@ Feature: Confirm that availability_completion works with previous activity setti And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" - And I click on "Displayed if student doesn't meet this condition • Click to hide" "link" + And I click on "Item name displayed with access restriction information if student doesn't meet this condition • Click to hide" "link" And I set the field "Activity or resource" to "Previous activity with completion" And I press "Save and return to course" Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region" @@ -58,7 +58,7 @@ Feature: Confirm that availability_completion works with previous activity setti And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" - And I click on "Displayed if student doesn't meet this condition • Click to hide" "link" + And I click on "Item name displayed with access restriction information if student doesn't meet this condition • Click to hide" "link" And I set the field "Activity or resource" to "Previous activity with completion" And I press "Save and return to course" Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region" @@ -83,7 +83,7 @@ Feature: Confirm that availability_completion works with previous activity setti And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" - And I click on "Displayed if student doesn't meet this condition • Click to hide" "link" + And I click on "Item name displayed with access restriction information if student doesn't meet this condition • Click to hide" "link" And I set the field "Activity or resource" to "Previous activity with completion" And I press "Save and return to course" Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region" @@ -112,7 +112,7 @@ Feature: Confirm that availability_completion works with previous activity setti And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" - And I click on "Displayed if student doesn't meet this condition • Click to hide" "link" + And I click on "Item name displayed with access restriction information if student doesn't meet this condition • Click to hide" "link" And I set the field "Activity or resource" to "Previous activity with completion" And I press "Save changes" Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region" @@ -138,7 +138,7 @@ Feature: Confirm that availability_completion works with previous activity setti And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" - And I click on "Displayed if student doesn't meet this condition • Click to hide" "link" + And I click on "Item name displayed with access restriction information if student doesn't meet this condition • Click to hide" "link" Then the "Activity or resource" select box should not contain "Previous activity with completion" # Set Page2 restriction to Previous Activity with completion and delete Page1. @@ -148,7 +148,7 @@ Feature: Confirm that availability_completion works with previous activity setti And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" - And I click on "Displayed if student doesn't meet this condition • Click to hide" "link" + And I click on "Item name displayed with access restriction information if student doesn't meet this condition • Click to hide" "link" And I set the field "Activity or resource" to "Previous activity with completion" And I press "Save and return to course" Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region" @@ -170,7 +170,7 @@ Feature: Confirm that availability_completion works with previous activity setti And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" - And I click on "Displayed if student doesn't meet this condition • Click to hide" "link" + And I click on "Item name displayed with access restriction information if student doesn't meet this condition • Click to hide" "link" And I set the field "Activity or resource" to "Previous activity with completion" And I press "Save changes" Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region" @@ -184,7 +184,7 @@ Feature: Confirm that availability_completion works with previous activity setti And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" - And I click on "Displayed if student doesn't meet this condition • Click to hide" "link" + And I click on "Item name displayed with access restriction information if student doesn't meet this condition • Click to hide" "link" And I set the field "Activity or resource" to "Previous activity with completion" And I press "Save changes" And I should see "Not available unless: The previous activity with completion" in the "region-main" "region" diff --git a/public/availability/condition/grade/classes/condition.php b/public/availability/condition/grade/classes/condition.php index 39e6c33f2a7cf..42a44c700234f 100644 --- a/public/availability/condition/grade/classes/condition.php +++ b/public/availability/condition/grade/classes/condition.php @@ -184,7 +184,8 @@ protected function get_debug_string() { */ private static function get_cached_grade_name($courseid, $gradeitemid) { global $DB, $CFG; - require_once($CFG->libdir . '/gradelib.php'); + + require_once("{$CFG->dirroot}/grade/lib.php"); // Get all grade item names from cache, or using db query. $cache = \cache::make('availability_grade', 'items'); diff --git a/public/availability/tests/behat/display_availability.feature b/public/availability/tests/behat/display_availability.feature index 9b6d943aade72..c0ce6dcd9f491 100644 --- a/public/availability/tests/behat/display_availability.feature +++ b/public/availability/tests/behat/display_availability.feature @@ -180,4 +180,4 @@ Feature: Display availability for activities and sections And I should not see "Date" in the "Restrict access" "fieldset" And I press "Add restriction..." And I click on "Grade" "button" in the "Add restriction..." "dialogue" - And the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Displayed if student" + And the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Item name displayed" diff --git a/public/availability/tests/behat/edit_availability.feature b/public/availability/tests/behat/edit_availability.feature index b7d18668ce2ba..243a445edc060 100644 --- a/public/availability/tests/behat/edit_availability.feature +++ b/public/availability/tests/behat/edit_availability.feature @@ -89,13 +89,13 @@ Feature: edit_availability And I should see "Date" in the "Restrict access" "fieldset" And ".availability-item .availability-eye img" "css_element" should be visible And ".availability-item .availability-delete img" "css_element" should be visible - And the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Displayed if student" + And the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Item name displayed" # Toggle the eye icon. When I click on ".availability-item .availability-eye img" "css_element" Then the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Hidden entirely" When I click on ".availability-item .availability-eye img" "css_element" - Then the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Displayed if student" + Then the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Item name displayed" # Click the delete button. When I click on ".availability-item .availability-delete img" "css_element" diff --git a/public/backup/moodle2/backup_stepslib.php b/public/backup/moodle2/backup_stepslib.php index 400a45eca5e5c..aec50f520e9ca 100644 --- a/public/backup/moodle2/backup_stepslib.php +++ b/public/backup/moodle2/backup_stepslib.php @@ -310,7 +310,10 @@ protected function annotate_set_reference_bank_entries( foreach ($setreferenceconditions as $setreferencecondition) { $conditions = json_decode($setreferencecondition, true); $conditions = question_reference_manager::convert_legacy_set_reference_filter_condition($conditions); - $setreferencequestionids += array_keys($randomloader->get_filtered_questions($conditions['filter'], 0)); + $setreferencequestionids = array_merge( + $setreferencequestionids, + array_keys($randomloader->get_filtered_questions($conditions['filter'], 0)), + ); } if (empty($setreferencequestionids)) { return; @@ -2646,6 +2649,7 @@ protected function define_structure() { 'questioncategoryid', 'idnumber', 'ownerid', + 'nextversion', ]); $questionversions = new backup_nested_element('question_version'); @@ -2722,7 +2726,7 @@ protected function define_structure() { WHERE bi.backupid = ? AND bi.itemname = 'question_categoryfinal'", [backup::VAR_BACKUPID]); - // Add all question bank entries from "complete" categories, plus annotated question bank entires + // Add all question bank entries from "complete" categories, plus annotated question bank entires and their children // from "partial" categories. $questionbankentry->set_source_sql( " @@ -2734,8 +2738,11 @@ protected function define_structure() { UNION SELECT qbe.* FROM {question_bank_entries} qbe + JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id + JOIN {question} q ON q.id = qv.questionid + LEFT JOIN {question_versions} parentqv ON parentqv.questionid = q.parent JOIN {question_category_partial_temp} qcp ON qcp.itemid = qbe.questioncategoryid - JOIN {backup_ids_temp} biq ON biq.itemid = qbe.id + JOIN {backup_ids_temp} biq ON biq.itemid = qbe.id OR biq.itemid = parentqv.questionbankentryid WHERE qcp.itemid = ? AND qcp.backupid = ? AND biq.backupid = ? diff --git a/public/backup/moodle2/restore_qtype_plugin.class.php b/public/backup/moodle2/restore_qtype_plugin.class.php index 52b10c6a5488c..4b80a13a02233 100644 --- a/public/backup/moodle2/restore_qtype_plugin.class.php +++ b/public/backup/moodle2/restore_qtype_plugin.class.php @@ -568,8 +568,7 @@ public static function remove_excluded_question_data(stdClass $questiondata, arr foreach ($excludefields as $excludefield) { $pathparts = explode('/', ltrim($excludefield, '/')); - $data = $questiondata; - self::unset_excluded_fields($data, $pathparts); + $questiondata = self::unset_excluded_fields($questiondata, $pathparts); } return $questiondata; @@ -581,25 +580,60 @@ public static function remove_excluded_question_data(stdClass $questiondata, arr * If any of the elements in the path is an array, this is called recursively on each element in the array to unset fields * in each child of the array. * - * @param stdClass|array $data The questiondata object, or a subsection of it. + * @param stdClass|array $data The questiondata structure, or a subsection of it. * @param array $pathparts The remaining elements in the path to the excluded field. - * @return void + * @return stdClass|array The $data structure with excluded fields removed. */ - private static function unset_excluded_fields(stdClass|array $data, array $pathparts): void { + private static function unset_excluded_fields(stdClass|array $data, array $pathparts): stdClass|array { $element = array_shift($pathparts); - if (!isset($data->{$element})) { - // This element is not present in the data structure, nothing to unset. - return; + $unset = false; + // Get the current element from the data structure. + if (is_object($data)) { + if (!property_exists($data, $element)) { + // This element is not present in the data structure, nothing to unset. + return $data; + } + $dataelement = $data->{$element}; + } else { // It's an array. + if (!array_key_exists($element, $data)) { + return $data; + } + $dataelement = $data[$element]; } - if (is_object($data->{$element})) { - self::unset_excluded_fields($data->{$element}, $pathparts); - } else if (is_array($data->{$element})) { - foreach ($data->{$element} as $item) { - self::unset_excluded_fields($item, $pathparts); + // Check if we need to recur, or unset this element. + if (is_object($dataelement)) { + $dataelement = self::unset_excluded_fields($dataelement, $pathparts); + } else if (is_array($dataelement)) { + foreach ($dataelement as $key => $item) { + if (is_object($item) || is_array($item)) { + // This is an array of objects or arrays, recur. + $dataelement[$key] = self::unset_excluded_fields($item, $pathparts); + } else { + // This is an associative array of values, check if they should be removed. + $subelement = reset($pathparts); + if ($key == $subelement) { + unset($dataelement[$key]); + } + } } } else if (empty($pathparts)) { // This is the last element of the path and it's a scalar value, unset it. - unset($data->{$element}); + $unset = true; + } + // Write the modified element back to the data structure, or unset it. + if (is_object($data)) { + if ($unset) { + unset($data->{$element}); + } else { + $data->{$element} = $dataelement; + } + } else { + if ($unset) { + unset($data[$element]); + } else { + $data[$element] = $dataelement; + } } + return $data; } } diff --git a/public/backup/moodle2/restore_root_task.class.php b/public/backup/moodle2/restore_root_task.class.php index b3c31cf246bd3..132a710ae86b7 100644 --- a/public/backup/moodle2/restore_root_task.class.php +++ b/public/backup/moodle2/restore_root_task.class.php @@ -78,6 +78,9 @@ public function build() { // Unconditionally, load create all the needed outcomes $this->add_step(new restore_outcomes_structure_step('create_scales', 'outcomes.xml')); + // If we haven't preloaded information, load all the question banks to temp_ids_table. + $this->add_step(new \core\backup\restore_load_questionbanks('load_questionbanks')); + // If we haven't preloaded information, load all the needed categories and questions (reduced) to temp_ids_table $this->add_step(new restore_load_categories_and_questions('load_categories_and_questions')); diff --git a/public/backup/moodle2/restore_stepslib.php b/public/backup/moodle2/restore_stepslib.php index 09b2d036fbce2..d1691ac3f461f 100644 --- a/public/backup/moodle2/restore_stepslib.php +++ b/public/backup/moodle2/restore_stepslib.php @@ -27,6 +27,9 @@ defined('MOODLE_INTERNAL') || die(); +use core_question\local\bank\question_version_status; +use core_question\versions; + /** * delete old directories and conditionally create backup_temp_ids table */ @@ -5289,6 +5292,15 @@ protected function process_question($data) { $this->set_mapping('question_bank_entry', $this->latestqbe->oldid, $this->latestqbe->newid); } + if ( + ($data->qtype === 'random') + && ($this->latestversion->status == \core_question\local\bank\question_version_status::QUESTION_STATUS_HIDDEN) + ) { + // Ensure that this newly created question is considered by + // \qtype_random\task\remove_unused_questions. + $this->latestversion->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_DRAFT; + } + // Now store the question. $newitemid = $DB->insert_record('question', $data); $this->set_mapping('question', $oldid, $newitemid); @@ -5301,9 +5313,55 @@ protected function process_question($data) { $oldqvid = $this->latestversion->id; $this->latestversion->questionbankentryid = $this->latestqbe->newid; $this->latestversion->questionid = $newitemid; + // In case the backed up version was deleted and a new one created in its place, increase the version numbers of + // conflicting versions to make room for this one. + $transaction = $DB->start_delegated_transaction(); + if ( + $DB->record_exists( + 'question_versions', + [ + 'questionbankentryid' => $this->latestversion->questionbankentryid, + 'version' => $this->latestversion->version, + ], + ) + ) { + // We'll update each higher version and any references one-at-a-time, starting with the highest, to avoid + // creating a duplicate questionbankentryid-version combination in question_versions. + $moveversions = $DB->get_records_select( + 'question_versions', + 'questionbankentryid = :questionbankentryid AND version >= :oldversion', + [ + 'questionbankentryid' => $this->latestversion->questionbankentryid, + 'oldversion' => $this->latestversion->version, + ], + 'version DESC', + ); + foreach ($moveversions as $moveversion) { + $DB->set_field( + 'question_versions', + 'version', + $moveversion->version + 1, + [ + 'questionbankentryid' => $moveversion->questionbankentryid, + 'version' => $moveversion->version, + ] + ); + $DB->set_field( + 'question_references', + 'version', + $moveversion->version + 1, + [ + 'questionbankentryid' => $moveversion->questionbankentryid, + 'version' => $moveversion->version, + ] + ); + } + // Ensure the nextversion value has been initialised, and increment it to account for the additional version. + versions::get_next_version($this->latestversion->questionbankentryid); + } $newqvid = $DB->insert_record('question_versions', $this->latestversion); $this->set_mapping('question_versions', $oldqvid, $newqvid); - + $transaction->allow_commit(); } else { // By performing this set_mapping() we make get_old/new_parentid() to work for all the // children elements of the 'question' one (so qtype plugins will know the question they belong to). @@ -5311,6 +5369,18 @@ protected function process_question($data) { // Also create the question_bank_entry and version mappings, if required. $newquestionversion = $DB->get_record('question_versions', ['questionid' => $questionmapping->newitemid]); + // Restore the version to ready state if it has been hidden. + if ( + $newquestionversion->status == question_version_status::QUESTION_STATUS_HIDDEN + && $this->latestversion->status == question_version_status::QUESTION_STATUS_READY + ) { + $DB->set_field( + 'question_versions', + 'status', + question_version_status::QUESTION_STATUS_READY, + ['questionid' => $questionmapping->newitemid], + ); + } $this->set_mapping('question_versions', $this->latestversion->id, $newquestionversion->id); if (empty($this->latestqbe->newid)) { $this->latestqbe->oldid = $this->latestqbe->id; @@ -5402,12 +5472,18 @@ protected function process_tag($data) { } $tagcontextid = $this->cachedcategory->contextid; // Add the tag to the question. - core_tag_tag::add_item_tag('core_question', + $taginstanceid = core_tag_tag::add_item_tag( + 'core_question', 'question', $newquestion, context::instance_by_id($tagcontextid), - $tagname + $tagname, ); + $tagid = $DB->get_field('tag_instance', 'tagid', ['id' => $taginstanceid]); + if ($tagid != $data->id) { + // The tag didn't exist already, map the new ID. + $this->set_mapping('tag', $data->id, $tagid); + } } } @@ -5485,7 +5561,11 @@ protected function define_execution() { // but if that context still exists on the site and the user has access then point question references // to the originals. $originalcontext = context::instance_by_id($contextid, IGNORE_MISSING); - if ($originalcontext && has_capability('mod/qbank:view', $originalcontext)) { + if ( + $this->task->is_samesite() + && $originalcontext + && has_capability('mod/qbank:view', $originalcontext) + ) { $originalquestions = get_questions_category(question_get_top_category($contextid), false); $targetcoursecontext = context_course::instance($this->get_courseid()); foreach ($originalquestions as $originalquestion) { @@ -5553,7 +5633,7 @@ protected function define_execution() { // From 3.5 onwards, all question categories should be a child of a special category called the "top" category. $info = backup_controller_dbops::decode_backup_temp_info($modulecat->info); if ($after35 && empty($info->parent)) { - $oldtopid = $modulecat->newitemid; + $oldtopid = $modulecat->itemid; $modulecat->newitemid = $top->id; } else { $cat = new stdClass(); @@ -5608,13 +5688,19 @@ protected function define_execution() { // We need to check all the question_set_references belonging to this context_module. $references = $DB->get_records('question_set_references', ['usingcontextid' => $newcontext->newitemid]); foreach ($references as $reference) { - $filtercondition = json_decode($reference->filtercondition); - if (!empty($filtercondition->questioncategoryid) && - in_array($filtercondition->questioncategoryid, $categoryids)) { - // This is one of ours, update the questionscontextid. - $DB->set_field('question_set_references', - 'questionscontextid', $newcontext->newitemid, - ['id' => $reference->id]); + $filtercondition = json_decode($reference->filtercondition, true); + if (!array_key_exists('filter', $filtercondition)) { + $filtercondition = \core_question\question_reference_manager::convert_legacy_set_reference_filter_condition( + $filtercondition, + ); + } + $questioncategoryid = $filtercondition['filter']['category']['values'][0]; + if (in_array($questioncategoryid, $categoryids)) { + // This is one of ours, update the questionscontextid and filtercondition fields. + $reference->questionscontextid = $newcontext->newitemid; + $filtercondition['cat'] = "{$questioncategoryid},{$newcontext->newitemid}"; + $reference->filtercondition = json_encode($filtercondition); + $DB->update_record('question_set_references', $reference); } } } @@ -5629,20 +5715,20 @@ protected function define_execution() { ); } } - // Remove any remaining course-level question categories from the restored course. + // Remove any remaining course-level question categories and their questions from the restored course. $coursecatsql = " - SELECT qc.id AS categoryid + SELECT qc.id AS id, qc.contextid AS contextid FROM {question_categories} qc JOIN {context} c ON c.id = qc.contextid WHERE c.contextlevel = :courselevel AND c.instanceid = :courseid "; - $DB->delete_records_subquery( - 'question_categories', - 'id', - 'categoryid', + $categories = $DB->get_records_sql( $coursecatsql, - ['courselevel' => context_course::LEVEL, 'courseid' => $this->task->get_courseid()] + ['courselevel' => context_course::LEVEL, 'courseid' => $this->task->get_courseid()], ); + foreach ($categories as $category) { + question_category_delete_safe($category); + } } } @@ -6421,49 +6507,46 @@ protected function add_question_set_references($element, &$paths) { public function process_question_set_reference($data) { global $DB; $data = (object) $data; - $owncontext = $data->usingcontextid == $data->questionscontextid; $data->usingcontextid = $this->get_mappingid('context', $data->usingcontextid); $data->itemid = $this->get_new_parentid('quiz_question_instance'); + + $originalbankinbackup = (bool) restore_dbops::get_backup_ids_record( + $this->get_restoreid(), + 'questionbank', + $data->questionscontextid, + ); + + if ($context = $this->get_mappingid('context', $data->questionscontextid)) { + $data->questionscontextid = $context; + } else { + $this->log( + "question_set_reference with old id {$data->id} referenced question context " + . "{$data->questionscontextid} which was not included in the backup. Therefore, this has been " + . "restored with the old questionscontextid.", + backup::LOG_WARNING, + ); + } + $filtercondition = json_decode($data->filtercondition, true); if (!isset($filtercondition['filter'])) { // Pre-4.3, convert the old filtercondition format to the new format. + // Don't map tags to new IDs, the plugin will do that below. $filtercondition = \core_question\question_reference_manager::convert_legacy_set_reference_filter_condition( - $filtercondition); - } - - // Map category id used for category filter condition and corresponding context id. - $oldcategoryid = $filtercondition['filter']['category']['values'][0]; - // Decide if we're going to refer back to the original category, or to the new category. - // Are we restoring to a different site? - // Has the original context or category been deleted? - // Did the old category belong to the same context as the original set reference? - // Are we allowed to use its questions? - $questionscontext = context::instance_by_id($data->questionscontextid, IGNORE_MISSING); - if ( - !$this->get_task()->is_samesite() - || !$questionscontext - || !$DB->record_exists('question_categories', ['id' => $oldcategoryid]) - || $owncontext - || !has_capability('moodle/question:useall', $questionscontext) - ) { - $newcategoryid = $this->get_mappingid('question_category', $oldcategoryid); - $filtercondition['filter']['category']['values'][0] = $newcategoryid; + $filtercondition, + false, + ); } - if ($context = $this->get_mappingid('context', $data->questionscontextid)) { - $data->questionscontextid = $context; - } else { - $this->log('question_set_reference with old id ' . $data->id . - ' referenced question context ' . $data->questionscontextid . - ' which was not included in the backup. Therefore, this has been ' . - ' restored with the old questionscontextid.', backup::LOG_WARNING); - } + $qbankfeatureclasses = \core\component::get_plugin_list_with_class('qbank', 'plugin_feature'); - $filtercondition['cat'] = implode(',', [ - $filtercondition['filter']['category']['values'][0], - $data->questionscontextid, - ]); + foreach ($qbankfeatureclasses as $qbankfeatureclass) { + $qbankfeature = new $qbankfeatureclass(); + $filters = $qbankfeature->get_question_filters(); + foreach ($filters as $filter) { + $filtercondition = $filter->restore_filtercondition($filtercondition, $data, $this, $originalbankinbackup); + } + } $data->filtercondition = json_encode($filtercondition); diff --git a/public/backup/moodle2/tests/moodle2_test.php b/public/backup/moodle2/tests/moodle2_test.php index 6b37f92605872..a015c83e06a54 100644 --- a/public/backup/moodle2/tests/moodle2_test.php +++ b/public/backup/moodle2/tests/moodle2_test.php @@ -1088,14 +1088,78 @@ public function test_restore_question_category_34_35(): void { } } - // Make sure there is a single top level category in this context. + // Make sure there is a single top level category in this context and that the parents are set correctly. if ($cats) { $this->assertEquals(1, $topcategorycount[$context->id]); + $topcat = array_values($cats)[0]; + $this->assertEquals(0, $topcat->parent); + $othercat = array_values($cats)[1]; + $this->assertEquals($topcat->id, $othercat->parent); } } } } + /** + * Check that the backup/restore process correctly wires the question categories, see MDL-86300. + * @covers \restore_move_module_questions_categories::define_execution + */ + public function test_restore_question_categories_from_500(): void { + global $DB, $CFG, $USER; + + $this->resetAfterTest(true); + $this->setAdminUser(); + + // Create a course. + $generator = $this->getDataGenerator(); + $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $course = $generator->create_course(); + + // Add a quiz with question categories. + $quiz = $generator->create_module('quiz', ['course' => $course->id]); + $quizcontext = \context_module::instance($quiz->cmid); + $questiongenerator->create_question_category(['contextid' => $quizcontext->id]); + $quizquestioncats = $DB->get_records('question_categories', ['contextid' => $quizcontext->id]); + $this->assertCount(3, $quizquestioncats); + + // Add a question bank with question categories. + $qbank = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id]); + $qbankcontext = \context_module::instance($qbank->cmid); + $questiongenerator->create_question_category(['contextid' => $qbankcontext->id]); + $qbankquestioncats = $DB->get_records('question_categories', ['contextid' => $qbankcontext->id]); + $this->assertCount(3, $qbankquestioncats); + + $targetcourseid = $this->backup_and_restore($course); + + // Check the quiz and qbank question categories in the target course, in particular the parent relationship. + $modinfo = get_fast_modinfo($targetcourseid); + + $targetquizzes = $modinfo->get_instances_of('quiz'); + $this->assertCount(1, $targetquizzes); + $targetquiz = reset($targetquizzes); + $targetquizcontext = \context_module::instance($targetquiz->id); + $targetquizcats = array_values( + $DB->get_records('question_categories', ['contextid' => $targetquizcontext->id], 'parent', 'id, name, parent') + ); + $this->assertCount(3, $targetquizcats); + $quiztop = $targetquizcats[0]; + $this->assertEquals(0, $quiztop->parent); + $quiznontop = $targetquizcats[1]; + $this->assertEquals($quiztop->id, $quiznontop->parent); + + $targetqbanks = $modinfo->get_instances_of('qbank'); + $this->assertCount(1, $targetqbanks); + $targetqbankcontext = \context_module::instance(reset($targetqbanks)->id); + $targetqbankcats = array_values( + $DB->get_records('question_categories', ['contextid' => $targetqbankcontext->id], 'parent', 'id, name, parent') + ); + $this->assertCount(3, $targetqbankcats); + $qbanktop = $targetqbankcats[0]; + $this->assertEquals(0, $qbanktop->parent); + $qbanknontop = $targetqbankcats[1]; + $this->assertEquals($qbanktop->id, $qbanknontop->parent); + } + /** * Test the content bank content through a backup and restore. */ diff --git a/public/backup/moodle2/tests/restore_qtype_plugin_test.php b/public/backup/moodle2/tests/restore_qtype_plugin_test.php new file mode 100644 index 0000000000000..527763281fec7 --- /dev/null +++ b/public/backup/moodle2/tests/restore_qtype_plugin_test.php @@ -0,0 +1,147 @@ +. + +namespace core; + +/** + * Tests for question type restore methods + * + * @package core + * @copyright 2025 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \restore_qtype_plugin + */ +final class restore_qtype_plugin_test extends \basic_testcase { + /** + * All default and specified fields should be removed from the provided data structure. + */ + public function test_remove_excluded_question_data(): void { + global $CFG; + require_once($CFG->dirroot . '/backup/moodle2/restore_plugin.class.php'); + require_once($CFG->dirroot . '/backup/moodle2/restore_qtype_plugin.class.php'); + $data = (object) [ + // Default excluded fields should be removed. + 'id' => 1, + 'createdby' => 2, + 'modifiedby' => 3, + // This field is not specified for removal, it should remain. + 'questiontext' => 'Some question text', + // Excluded paths that address an array should operate on all items in the array. + 'hints' => [ + (object) [ + 'id' => 4, + 'questionid' => 1, + // This field is not specified for removal. + 'text' => 'Lorem ipsum', + ], + (object) [ + 'id' => 5, + 'questionid' => 1, + 'text' => 'Lorem ipsum', + ], + ], + 'options' => [ // This is an array of arrays, rather than an array of objects. It should be handled the same. + [ + 'id' => 6, + 'questionid' => 1, + // This field is not specified for removal. + 'option' => true, + ], + [ + 'id' => 7, + 'questionid' => 1, + 'option' => false, + ], + [ + 'id' => 8, + 'questionid' => 1, + 'option' => false, + ], + ], + 'custom1' => 'Some custom text', + // This field is not specified for removal. + 'custom2' => 'Some custom text2', + // Fields specified for removal should be removed even if they contain null values. + 'custom3' => null, + 'customarray' => [ + (object) [ + // Null values should also be removed. + 'id' => null, + // This field is not specified for removal. + 'text' => 'Custom item text', + ], + (object) [ + 'id' => null, + 'text' => 'Custom item text2', + ], + ], + 'customstructure' => [ // This array contains scalar values, not a list of objects/arrays. + 'id' => null, + 'text' => 'Custom structure text', + 'number' => 1, + 'bool' => true, + ], + ]; + + $expecteddata = (object) [ + 'questiontext' => 'Some question text', + 'hints' => [ + (object) [ + 'text' => 'Lorem ipsum', + ], + (object) [ + 'text' => 'Lorem ipsum', + ], + ], + 'options' => [ + [ + 'option' => true, + ], + [ + 'option' => false, + ], + [ + 'option' => false, + ], + ], + 'custom2' => 'Some custom text2', + 'customarray' => [ + (object) [ + 'text' => 'Custom item text', + ], + (object) [ + 'text' => 'Custom item text2', + ], + ], + 'customstructure' => [ + 'text' => 'Custom structure text', + 'number' => 1, + ], + ]; + + $excludedfields = [ + '/custom1', + '/custom3', + '/customarray/id', + '/customstructure/id', + '/customstructure/bool', + // A field that is not in the data structure will be ignored. + '/custom4', + ]; + $this->assertEquals($expecteddata, \restore_qtype_plugin::remove_excluded_question_data($data, $excludedfields)); + } +} diff --git a/public/backup/tests/hook/copy_course_hook_test.php b/public/backup/tests/hook/copy_course_hook_test.php index 8af997cb0e92c..c5cc32ede9fc2 100644 --- a/public/backup/tests/hook/copy_course_hook_test.php +++ b/public/backup/tests/hook/copy_course_hook_test.php @@ -32,6 +32,14 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class copy_course_hook_test extends advanced_testcase { + /** + * Load required test libraries + */ + public static function setUpBeforeClass(): void { + global $CFG; + require_once("{$CFG->dirroot}/backup/util/includes/backup_includes.php"); + parent::setUpBeforeClass(); + } /** * Test the hook. diff --git a/public/backup/util/dbops/restore_dbops.class.php b/public/backup/util/dbops/restore_dbops.class.php index 60872fb42c319..cd68126ff9f41 100644 --- a/public/backup/util/dbops/restore_dbops.class.php +++ b/public/backup/util/dbops/restore_dbops.class.php @@ -464,6 +464,39 @@ public static function load_categories_and_questions_to_tempids($restoreid, $que $xmlparser->process(); } + /** + * Store ids associated with any activity in the backup that supports FEATURE_PUBLISHES_QUESTIONS. + * + * @param string $restoreid The restore ID. + * @param string $activitiespath The path to the `activities` folder in the backup being restored. + */ + public static function load_questionbanks_to_tempids(string $restoreid, string $activitiespath): void { + if (!is_dir($activitiespath)) { + return; + } + // Get modules that publish questions. + $qmodules = array_filter( + array_keys(core\component::get_all_plugins_list('mod')), + fn($module) => plugin_supports('mod', $module, FEATURE_PUBLISHES_QUESTIONS), + ); + foreach (scandir($activitiespath) as $activitydir) { + [$modname] = explode('_', $activitydir); + if (!in_array($modname, $qmodules)) { + continue; + } + $activityfile = "{$activitiespath}/{$activitydir}/{$modname}.xml"; + if (!file_exists($activityfile)) { // Shouldn't happen ever, but... + throw new backup_helper_exception('missing_moodle_backup_xml_file', $activityfile); + } + // Parse each activity's file, storing the relevant data in the database. + $xmlparser = new progressive_parser(); + $xmlparser->set_file($activityfile); + $xmlprocessor = new restore_questionbanks_parser_processor($restoreid); + $xmlparser->set_processor($xmlprocessor); + $xmlparser->process(); + } + } + /** * Check all the included categories and questions, deciding the action to perform * for each one (mapping / creation) and returning one array of problems in case @@ -609,9 +642,18 @@ public static function prechek_precheck_qbanks_by_level($restoreid, $courseid, $ $topcats = 0; // get categories in context (bank) $categories = self::restore_get_question_categories($restoreid, $contextid, $contextlevel); - - // cache permissions if $targetcontext is found - if ($targetcontext = self::restore_find_best_target_context($categories, $courseid, $contextlevel)) { + if ( + $contextlevel == \core\context\module::LEVEL + && self::get_backup_ids_record($restoreid, 'questionbank', $contextid) + ) { + // Don't look for an existing module context, we have the original context in the backup, + // so we'll put the categories in the course context for now and move them once the activity is restored. + $targetcontext = core\context\course::instance($courseid); + } else { + $targetcontext = self::restore_find_best_target_context($categories, $courseid, $contextlevel); + } + if ($targetcontext) { + // Cache permissions if $targetcontext is found. $canmanagecategory = has_capability('moodle/question:managecategory', $targetcontext, $userid); $canadd = has_capability('moodle/question:add', $targetcontext, $userid); } diff --git a/public/backup/util/helper/restore_prechecks_helper.class.php b/public/backup/util/helper/restore_prechecks_helper.class.php index 803cd77903df5..4dac448851c90 100644 --- a/public/backup/util/helper/restore_prechecks_helper.class.php +++ b/public/backup/util/helper/restore_prechecks_helper.class.php @@ -173,6 +173,7 @@ public static function execute_prechecks(restore_controller $controller, $dropte $progress->progress($majorstep++); // Check we are able to restore and the categories and questions + restore_dbops::load_questionbanks_to_tempids($restoreid, $controller->get_plan()->get_basepath() . '/activities'); $file = $controller->get_plan()->get_basepath() . '/questions.xml'; restore_dbops::load_categories_and_questions_to_tempids($restoreid, $file); if ($problems = restore_dbops::precheck_categories_and_questions($restoreid, $courseid, $userid, $samesite)) { diff --git a/public/backup/util/helper/restore_questionbanks_parser_processor.php b/public/backup/util/helper/restore_questionbanks_parser_processor.php new file mode 100644 index 0000000000000..10a76750dbbdc --- /dev/null +++ b/public/backup/util/helper/restore_questionbanks_parser_processor.php @@ -0,0 +1,59 @@ +. + +defined('MOODLE_INTERNAL' || die()); + +require_once($CFG->dirroot . '/backup/util/xml/parser/processors/grouped_parser_processor.class.php'); + +/** + * Parse and store activity data for activities that publish questions. + * + * @package core + * @copyright 2025 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class restore_questionbanks_parser_processor extends grouped_parser_processor { + /** + * Store the restore ID and register paths. + * + * @param string $restoreid ID of the backup being restored. + */ + public function __construct( + /** @var string ID of the backup being restored */ + protected string $restoreid, + ) { + parent::__construct(); + $this->add_path('/activity'); + } + + #[\Override] + protected function dispatch_chunk($data): void { + // Recieved one chunk, store the context ID as that's what we will match question categories against. + $itemid = $data['tags']['contextid']; + restore_dbops::set_backup_ids_record($this->restoreid, 'questionbank', $itemid); + } + + #[\Override] + protected function notify_path_start($path) { + // Nothing to do. + } + + #[\Override] + protected function notify_path_end($path) { + // Nothing to do. + } +} diff --git a/public/backup/util/includes/restore_includes.php b/public/backup/util/includes/restore_includes.php index 9b8ab15e84da7..94b5a38eb63e9 100644 --- a/public/backup/util/includes/restore_includes.php +++ b/public/backup/util/includes/restore_includes.php @@ -39,6 +39,7 @@ require_once($CFG->dirroot . '/backup/util/helper/backup_file_manager.class.php'); require_once($CFG->dirroot . '/backup/util/helper/copy_helper.class.php'); require_once($CFG->dirroot . '/backup/util/helper/restore_prechecks_helper.class.php'); +require_once($CFG->dirroot . '/backup/util/helper/restore_questionbanks_parser_processor.php'); require_once($CFG->dirroot . '/backup/util/helper/restore_moodlexml_parser_processor.class.php'); require_once($CFG->dirroot . '/backup/util/helper/restore_inforef_parser_processor.class.php'); require_once($CFG->dirroot . '/backup/util/helper/restore_users_parser_processor.class.php'); diff --git a/public/backup/util/ui/renderer.php b/public/backup/util/ui/renderer.php index 2f69eb828f0dc..937439d037e84 100644 --- a/public/backup/util/ui/renderer.php +++ b/public/backup/util/ui/renderer.php @@ -690,7 +690,7 @@ public function render_backup_files_viewer(backup_files_viewer $viewer) { } $table = new html_table(); - $table->attributes['class'] = 'backup-files-table table generaltable'; + $table->attributes['class'] = 'backup-files-table table generaltable table-hover'; $table->head = $tablehead; $table->width = '100%'; $table->data = []; @@ -1051,7 +1051,7 @@ public function restore_progress_viewer($userid, $context) { $tablehead = array(get_string('course'), get_string('time'), get_string('status', 'backup')); $table = new html_table(); - $table->attributes['class'] = 'backup-files-table table generaltable'; + $table->attributes['class'] = 'backup-files-table table generaltable table-hover'; $table->head = $tablehead; $tabledata = array(); @@ -1092,7 +1092,7 @@ public function copy_progress_viewer(int $userid, int $courseid): string { ); $table = new html_table(); - $table->attributes['class'] = 'backup-files-table table generaltable'; + $table->attributes['class'] = 'backup-files-table table generaltable table-hover'; $table->head = $tablehead; $tabledata = array(); diff --git a/public/backup/util/xml/tests/writer_test.php b/public/backup/util/xml/tests/writer_test.php index 947d825fac367..f75337446438a 100644 --- a/public/backup/util/xml/tests/writer_test.php +++ b/public/backup/util/xml/tests/writer_test.php @@ -282,6 +282,24 @@ function test_xml_writer_public_api(): void { $result = $xo->get_allcontents(); $this->assertEquals($result, 'testsomecontent'); + // Test nullcontent reset. + $xo = new memory_xml_output(); + $xw = new mock_xml_writer($xo); + $xw->set_prologue(''); + $xw->start(); + $xw->full_tag('tagname', null); + $xw->begin_tag('tagname2'); + $xw->full_tag('tagname3', 'somecontent'); + $xw->end_tag('tagname2'); + $xw->stop(); + $result = $xo->get_allcontents(); + $expected = << + somecontent + + XML; + $this->assertEquals($expected, $result); + // Build a complex XML file and test results against stored file in fixtures $xo = new memory_xml_output(); $xw = new mock_xml_writer($xo); diff --git a/public/backup/util/xml/xml_writer.class.php b/public/backup/util/xml/xml_writer.class.php index da4ed9ebbf2e7..35be10db1fd79 100644 --- a/public/backup/util/xml/xml_writer.class.php +++ b/public/backup/util/xml/xml_writer.class.php @@ -216,6 +216,7 @@ public function full_tag($tag, $content = null, $attributes = null) { $this->lastwastext = true; $this->end_tag($tag); } + $this->nullcontent = false; // Reset nullcontent flag. } diff --git a/public/badges/award.php b/public/badges/award.php index 7b71559c2b237..bf6855c525f90 100644 --- a/public/badges/award.php +++ b/public/badges/award.php @@ -82,6 +82,16 @@ die(); } +if (!empty($role)) { + if (!user_has_role_assignment($USER->id, $role, $context->id) && !$isadmin) { + // User does not have the role passed by the parameter. + echo $OUTPUT->header(); + echo $OUTPUT->notification(get_string('wrongrole', 'badges')); + echo $OUTPUT->footer(); + die(); + } +} + $returnurl = new moodle_url('recipients.php', array('id' => $badge->id)); $returnlink = html_writer::link($returnurl, $strrecipients); $actionbar = new \core_badges\output\standard_action_bar( @@ -207,7 +217,7 @@ $users = $existingselector->get_selected_users(); foreach ($users as $user) { - if (!process_manual_revoke($user->id, $USER->id, $issuerrole->roleid, $badgeid)) { + if (!process_manual_revoke($user->id, 0, $issuerrole->roleid, $badgeid)) { echo $OUTPUT->error_text(get_string('error:cannotrevokebadge', 'badges')); } } diff --git a/public/badges/badge.php b/public/badges/badge.php index a6e6f28a8ac83..1d46520e0b281 100644 --- a/public/badges/badge.php +++ b/public/badges/badge.php @@ -80,7 +80,7 @@ $eventparams = array('context' => $PAGE->context, 'other' => $other); // If the badge does not belong to this user, log it appropriately. -if (($badge->recipient->id != $USER->id)) { +if ($badge->recipient && $badge->recipient->id != $USER->id) { $eventparams['relateduserid'] = $badge->recipient->id; } diff --git a/public/badges/classes/output/issued_badge.php b/public/badges/classes/output/issued_badge.php index 3022c99778d25..4bc58c9511234 100644 --- a/public/badges/classes/output/issued_badge.php +++ b/public/badges/classes/output/issued_badge.php @@ -74,7 +74,7 @@ public function __construct($hash) { $this->hash = $hash; $assertion = new \core_badges_assertion($hash, badges_open_badges_backpack_api()); $this->issued = $assertion->get_badge_assertion(); - if (!is_numeric($this->issued['issuedOn'])) { + if (array_key_exists('issuedOn', $this->issued) && !is_numeric($this->issued['issuedOn'])) { $this->issued['issuedOn'] = strtotime($this->issued['issuedOn']); } $this->badgeclass = $assertion->get_badge_class(); diff --git a/public/badges/lib/awardlib.php b/public/badges/lib/awardlib.php index f085c56cd01e4..89d16ad03374c 100644 --- a/public/badges/lib/awardlib.php +++ b/public/badges/lib/awardlib.php @@ -290,27 +290,27 @@ function process_manual_award($recipientid, $issuerid, $issuerrole, $badgeid) { /** * Manually revoke awarded badges. * - * @param int $recipientid - * @param int $issuerid - * @param int $issuerrole - * @param int $badgeid + * @param int $recipientid User ID of the recipient + * @param int $issuerid User ID of the issuer (if 0, issuer will be ignored) + * @param int $issuerrole Role of the issuer + * @param int $badgeid ID of the badge * @return bool */ function process_manual_revoke($recipientid, $issuerid, $issuerrole, $badgeid) { global $DB; - $params = array( - 'badgeid' => $badgeid, - 'issuerid' => $issuerid, - 'issuerrole' => $issuerrole, - 'recipientid' => $recipientid - ); + $params = [ + 'badgeid' => $badgeid, + 'issuerrole' => $issuerrole, + 'recipientid' => $recipientid, + ]; + if (!empty($issuerid)) { + $params['issuerid'] = $issuerid; + } if ($DB->record_exists('badge_manual_award', $params)) { - if ($DB->delete_records('badge_manual_award', array('badgeid' => $badgeid, - 'issuerid' => $issuerid, - 'recipientid' => $recipientid)) - && $DB->delete_records('badge_issued', array('badgeid' => $badgeid, - 'userid' => $recipientid))) { - + if ( + $DB->delete_records('badge_manual_award', $params) && + $DB->delete_records('badge_issued', ['badgeid' => $badgeid, 'userid' => $recipientid]) + ) { // Trigger event, badge revoked. $badge = new \badge($badgeid); $eventparams = array( diff --git a/public/badges/renderer.php b/public/badges/renderer.php index 684024d8582db..bd549eb5ab93a 100644 --- a/public/badges/renderer.php +++ b/public/badges/renderer.php @@ -781,7 +781,7 @@ protected function render_badge_related(\core_badges\output\badge_related $relat $paging = new paging_bar($related->totalcount, $related->page, $related->perpage, $this->page->url, 'page'); $htmlpagingbar = $this->render($paging); $table = new html_table(); - $table->attributes['class'] = 'table generaltable'; + $table->attributes['class'] = 'table generaltable table-hover'; $table->head = array( get_string('name'), get_string('version', 'badges'), @@ -840,7 +840,7 @@ protected function render_badge_alignments(\core_badges\output\badge_alignments $paging = new paging_bar($alignments->totalcount, $alignments->page, $alignments->perpage, $this->page->url, 'page'); $htmlpagingbar = $this->render($paging); $table = new html_table(); - $table->attributes['class'] = 'table generaltable'; + $table->attributes['class'] = 'table generaltable table-hover'; $table->head = array('Name', 'URL', ''); foreach ($alignments->alignments as $item) { diff --git a/public/badges/tests/behat/award_badge.feature b/public/badges/tests/behat/award_badge.feature index f5487fefe95a5..9ef45afffb6a8 100644 --- a/public/badges/tests/behat/award_badge.feature +++ b/public/badges/tests/behat/award_badge.feature @@ -11,11 +11,13 @@ Feature: Award badges And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | + | teacher2 | Teacher | 2 | teacher2@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | + | teacher2 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | And the following "activity" exists: @@ -371,14 +373,24 @@ Feature: Award badges And I am on "Course 1" course homepage And I navigate to "Badges" in current page administration And I follow "Course Badge" - Then I should see "Recipients (2)" And I select "Recipients (2)" from the "jump" singleselect And I press "Award badge" And I set the field "existingrecipients[]" to "Student 2 (student2@example.com)" And I press "Revoke badge" + And I am on "Course 1" course homepage + And I navigate to "Badges" in current page administration + And I follow "Course Badge" + Then I should see "Recipients (1)" + And I log out + # Now attempt to revoke a badge as another teacher. + And I am on the "Course 1" "course" page logged in as "teacher2" + And I navigate to "Badges" in current page administration + And I follow "Course Badge" + And I select "Recipients (1)" from the "jump" singleselect + And I press "Award badge" And I set the field "existingrecipients[]" to "Student 1 (student1@example.com)" - When I press "Revoke badge" + And I press "Revoke badge" And I am on "Course 1" course homepage And I navigate to "Badges" in current page administration And I follow "Course Badge" - Then I should see "Recipients (0)" + And I should see "Recipients (0)" diff --git a/public/blocks/accessreview/block_accessreview.php b/public/blocks/accessreview/block_accessreview.php index 7f81e0f182d3b..ea07e6f5d7e42 100644 --- a/public/blocks/accessreview/block_accessreview.php +++ b/public/blocks/accessreview/block_accessreview.php @@ -113,7 +113,7 @@ public function get_content() { return $this->content; } $table->data = $tabledata; - $table->attributes['class'] = 'generaltable table table-sm block_accessreview_table'; + $table->attributes['class'] = 'generaltable table table-sm block_accessreview_table table-hover'; $this->content->text .= html_writer::table($table, true); // Check for compatible course formats for highlighting. diff --git a/public/blocks/activity_modules/block_activity_modules.php b/public/blocks/activity_modules/block_activity_modules.php index cbf6c804fc121..2f4eee6a21c2f 100644 --- a/public/blocks/activity_modules/block_activity_modules.php +++ b/public/blocks/activity_modules/block_activity_modules.php @@ -53,7 +53,11 @@ function get_content() { foreach($modinfo->cms as $cm) { // Exclude activities that aren't visible or have no view link (e.g. label). Account for folder being displayed inline. - if (!$cm->uservisible || (!$cm->has_view() && strcmp($cm->modname, 'folder') !== 0)) { + if ( + !\course_modinfo::is_mod_type_visible_on_course($cm->modname) + || !$cm->uservisible + || (!$cm->has_view() && strcmp($cm->modname, 'folder') !== 0) + ) { continue; } if (array_key_exists($cm->modname, $modfullnames)) { diff --git a/public/blocks/activity_modules/tests/behat/block_activity_modules.feature b/public/blocks/activity_modules/tests/behat/block_activity_modules.feature index d925524cafd5f..d99a7d0e8e5fb 100644 --- a/public/blocks/activity_modules/tests/behat/block_activity_modules.feature +++ b/public/blocks/activity_modules/tests/behat/block_activity_modules.feature @@ -26,6 +26,7 @@ Feature: Block activity modules | url | Frontpage url name | Frontpage url description | Acceptance test site | url0 | | wiki | Frontpage wiki name | Frontpage wiki description | Acceptance test site | wiki0 | | workshop | Frontpage workshop name | Frontpage workshop description | Acceptance test site | workshop0 | + | qbank | Frontpage qbank name | Frontpage qbank description | Acceptance test site | qbank0 | When I log in as "admin" And I am on site homepage @@ -74,6 +75,8 @@ Feature: Block activity modules And I should see "Frontpage imscp name" And I should see "Frontpage folder name" And I should see "Frontpage url name" + And I am on site homepage + And "Question banks" "link" should not exist in the "Activities" "block" Scenario: Add activities block in a course Given the following "courses" exist: @@ -100,6 +103,7 @@ Feature: Block activity modules | url | Test url name | Test url description | C1 | url1 | | wiki | Test wiki name | Test wiki description | C1 | wiki1 | | workshop | Test workshop name | Test workshop description | C1 | workshop1 | + | qbank | Test qbank name | Test qbank description | C1 | qbank1 | When I log in as "admin" And I am on "Course 1" course homepage with editing mode on @@ -147,3 +151,5 @@ Feature: Block activity modules And I should see "Test imscp name" And I should see "Test folder name" And I should see "Test url name" + And I am on "Course 1" course homepage + And "Question banks" "link" should not exist in the "Activities" "block" diff --git a/public/blocks/badges/db/upgrade.php b/public/blocks/badges/db/upgrade.php index 964d4f150a2fe..1fbdc897e0a53 100644 --- a/public/blocks/badges/db/upgrade.php +++ b/public/blocks/badges/db/upgrade.php @@ -58,5 +58,8 @@ function xmldb_block_badges_upgrade($oldversion, $block) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/blocks/calendar_month/db/upgrade.php b/public/blocks/calendar_month/db/upgrade.php index ff795d2ee0148..4f9aa43097c74 100644 --- a/public/blocks/calendar_month/db/upgrade.php +++ b/public/blocks/calendar_month/db/upgrade.php @@ -58,5 +58,8 @@ function xmldb_block_calendar_month_upgrade($oldversion, $block) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/blocks/calendar_upcoming/db/upgrade.php b/public/blocks/calendar_upcoming/db/upgrade.php index a25b014ad903e..3c640bec21feb 100644 --- a/public/blocks/calendar_upcoming/db/upgrade.php +++ b/public/blocks/calendar_upcoming/db/upgrade.php @@ -58,5 +58,8 @@ function xmldb_block_calendar_upcoming_upgrade($oldversion, $block) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/blocks/completionstatus/block_completionstatus.php b/public/blocks/completionstatus/block_completionstatus.php index 4735e7600622f..9bd4c6fadb489 100644 --- a/public/blocks/completionstatus/block_completionstatus.php +++ b/public/blocks/completionstatus/block_completionstatus.php @@ -104,6 +104,9 @@ public function get_content() { // Flag to set if current completion data is inconsistent with what is stored in the database. $pending_update = false; + // Get activities visible to the user that have completion enabled. + $visibleactivities = $info->get_user_activities_with_completion($USER->id); + // Loop through course criteria. foreach ($completions as $completion) { $criteria = $completion->get_criteria(); @@ -115,6 +118,11 @@ public function get_content() { // Activities are a special case, so cache them and leave them till last. if ($criteria->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) { + // Only include activities that are visible to the user. + if (!isset($visibleactivities[$criteria->moduleinstance])) { + continue; + } + $activities[$criteria->moduleinstance] = $complete; if ($complete) { diff --git a/public/blocks/completionstatus/db/upgrade.php b/public/blocks/completionstatus/db/upgrade.php index d4a7ca0e00e73..aea2e0e2e70a0 100644 --- a/public/blocks/completionstatus/db/upgrade.php +++ b/public/blocks/completionstatus/db/upgrade.php @@ -59,5 +59,8 @@ function xmldb_block_completionstatus_upgrade($oldversion, $block) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/blocks/completionstatus/details.php b/public/blocks/completionstatus/details.php index e00892babe2c6..eada453ce2c14 100644 --- a/public/blocks/completionstatus/details.php +++ b/public/blocks/completionstatus/details.php @@ -119,10 +119,21 @@ // Load criteria to display. $completions = $info->get_completions($user->id); +// Get activities visible to the user that have completion enabled. +$visibleactivities = $info->get_user_activities_with_completion($user->id); + // Loop through course criteria. foreach ($completions as $completion) { $criteria = $completion->get_criteria(); + // Skip display of activity completion criteria for activities the user cannot see. + if ( + $criteria->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY && + !isset($visibleactivities[$criteria->moduleinstance]) + ) { + continue; + } + if (!$pendingupdate && $criteria->is_pending($completion)) { $pendingupdate = true; } diff --git a/public/blocks/completionstatus/tests/behat/block_completionstatus_activity_completion.feature b/public/blocks/completionstatus/tests/behat/block_completionstatus_activity_completion.feature index 941a4683529e2..c57061d7c1bd5 100644 --- a/public/blocks/completionstatus/tests/behat/block_completionstatus_activity_completion.feature +++ b/public/blocks/completionstatus/tests/behat/block_completionstatus_activity_completion.feature @@ -6,9 +6,10 @@ Feature: Enable Block Completion in a course using activity completion Background: Given the following "users" exist: - | username | firstname | lastname | email | idnumber | - | teacher1 | Teacher | 1 | teacher1@example.com | T1 | - | student1 | Student | 1 | student1@example.com | S1 | + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + | student1 | Student | 1 | student1@example.com | S1 | + | student2 | Student | 2 | student2@example.com | S2 | And the following "courses" exist: | fullname | shortname | category | enablecompletion | | Course 1 | C1 | 0 | 1 | @@ -16,6 +17,7 @@ Feature: Enable Block Completion in a course using activity completion | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | + | student2 | C1 | student | And the following "activities" exist: | activity | course | idnumber | name | gradepass | completion | completionview | completionusegrade | completionpassgrade | | page | C1 | page1 | Test page name | | 2 | 1 | 0 | 0 | @@ -23,10 +25,12 @@ Feature: Enable Block Completion in a course using activity completion And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | completionstatus | Course | C1 | course-view-* | side-pre | + And I am on the "Course 1" course page logged in as teacher1 + And I change window size to "large" + And I turn editing mode on Scenario: Completion status block when student has not started any activities - Given I am on the "Course 1" course page logged in as teacher1 - And I navigate to "Course completion" in current page administration + Given I navigate to "Course completion" in current page administration And I expand all fieldsets And I set the following fields to these values: | Test page name | 1 | @@ -36,8 +40,7 @@ Feature: Enable Block Completion in a course using activity completion And I should see "0 of 1" in the "Activity completion" "table_row" Scenario: Completion status block when student has completed a page - Given I am on the "Course 1" course page logged in as teacher1 - And I navigate to "Course completion" in current page administration + Given I navigate to "Course completion" in current page administration And I expand all fieldsets And I set the following fields to these values: | Test page name | 1 | @@ -50,8 +53,7 @@ Feature: Enable Block Completion in a course using activity completion And I should see "Yes" in the "Activity completion" "table_row" Scenario: Completion status block with items with passing grade - Given I am on the "Course 1" course page logged in as teacher1 - And I navigate to "Course completion" in current page administration + Given I navigate to "Course completion" in current page administration And I expand all fieldsets And I set the following fields to these values: | Test assign name | 1 | @@ -69,8 +71,7 @@ Feature: Enable Block Completion in a course using activity completion And I should see "Yes" in the "Activity completion" "table_row" Scenario: Completion status block with items with failing grade - Given I am on the "Course 1" course page logged in as teacher1 - And the following "grade grades" exist: + Given the following "grade grades" exist: | gradeitem | user | grade | | Test assign name | student1 | 49 | And I navigate to "Course completion" in current page administration @@ -86,3 +87,265 @@ Feature: Enable Block Completion in a course using activity completion And I follow "More details" And I should see "Achieving grade, Achieving passing grade" in the "Activity completion" "table_row" And I should see "No" in the "Activity completion" "table_row" + + @javascript + Scenario: Student visibility respects combined activity and section restrictions with progressive completion + Given the following "activities" exist: + | activity | name | intro | course | idnumber | section | visible | completion | completionview | + | page | task A | page description | C1 | page1 | 0 | 1 | 2 | 1 | + | page | task B | page description | C1 | page2 | 1 | 1 | 2 | 1 | + | assign | task C | assignment description | C1 | assign1 | 1 | 1 | 2 | 1 | + And I navigate to "Course completion" in current page administration + And I expand all fieldsets + And I set the following fields to these values: + | Page - task A | 1 | + | Page - task B | 1 | + | Assignment - task C | 1 | + And I press "Save changes" + # Add conditionally visible restriction (open eye) to section 1 requiring task A completion. + And I turn editing mode on + And I edit the section "1" + And I expand all fieldsets + And I click on "Add restriction..." "button" + And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" + And I set the field "Activity or resource" to "task A" + And I press "Save changes" + # Add conditionally hidden restriction (closed eye) to task C requiring task A completion. + And I am on the "task C" "assign activity editing" page + And I expand all fieldsets + And I click on "Add restriction..." "button" + And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" + And I click on "Item name displayed with access restriction information if student doesn't meet this condition • Click to hide" "link" + And I set the field "Activity or resource" to "task A" + And I press "Save and return to course" + And I log out + # Initial state: Only unrestricted visible activities appear. + When I am on the "Course 1" course page logged in as student1 + Then I should see "Status: Not yet started" + And I should see "0 of 1" in the "Activity completion" "table_row" + And I follow "More details" + And I should see "task A" in the "criteriastatus" "table" + And I should not see "task B" in the "criteriastatus" "table" + And I should not see "task C" in the "criteriastatus" "table" + # After completing task A: Section 2 activities become visible. + And I click on "task A" "link" + And I am on the "Course 1" course page logged in as student1 + And I should see "Status: In progress" + And I should see "1 of 3" in the "Activity completion" "table_row" + And I follow "More details" + And I should see "task A" in the "criteriastatus" "table" + And I should see "task B" in the "criteriastatus" "table" + And I should see "task C" in the "criteriastatus" "table" + + @javascript + Scenario: Student completion view shows only accessible activities considering all activity restrictions + Given the following "activities" exist: + | activity | name | intro | course | idnumber | section | visible | completion | completionview | + | page | task A | page description | C1 | page1 | 0 | 1 | 2 | 1 | + | page | task B | page description | C1 | assign1 | 1 | 1 | 2 | 1 | + | assign | task C | assignment description | C1 | assign2 | 1 | 1 | 2 | 1 | + And I navigate to "Course completion" in current page administration + And I expand all fieldsets + # Set completion of the activities. + And I set the following fields to these values: + | Page - task A | 1 | + | Page - task B | 1 | + | Assignment - task C | 1 | + And I press "Save changes" + # Add conditionally visible restriction (open eye) to "task B". + And I am on the "task B" "page activity editing" page + And I expand all fieldsets + And I click on "Add restriction..." "button" + And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" + And I set the field "Activity or resource" to "task A" + And I press "Save and return to course" + # Add conditionally hidden restriction (closed eye) to "task C". + And I am on the "task C" "assign activity editing" page + And I expand all fieldsets + And I click on "Add restriction..." "button" + And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" + And I click on "Item name displayed with access restriction information if student doesn't meet this condition • Click to hide" "link" + And I set the field "Activity or resource" to "task A" + And I press "Save and return to course" + When I am on the "Course 1" course page logged in as student1 + And I should see "Status: Not yet started" + And I should see "0 of 2" in the "Activity completion" "table_row" + And I follow "More details" + Then I should see "task A" in the "criteriastatus" "table" + And I should see "task B" in the "criteriastatus" "table" + And I should not see "task C" in the "criteriastatus" "table" + And I click on "task A" "link" + # Complete task A to make task C visible. + And I am on the "Course 1" course page logged in as student1 + And I should see "Status: In progress" + And I should see "1 of 3" in the "Activity completion" "table_row" + And I follow "More details" + And I should see "task A" in the "criteriastatus" "table" + And I should see "task B" in the "criteriastatus" "table" + And I should see "task C" in the "criteriastatus" "table" + + @javascript + Scenario: Hidden activities do not appear in the completion status block + Given the following "activities" exist: + | activity | name | intro | course | idnumber | section | visible | completion | completionview | + | page | task A | page description | C1 | page1 | 0 | 1 | 2 | 1 | + | page | task B | page description | C1 | page2 | 0 | 0 | 2 | 1 | + And I navigate to "Course completion" in current page administration + And I expand all fieldsets + # Set completion of the activities. + And I set the following fields to these values: + | Page - task A | 1 | + | Page - task B | 1 | + And I press "Save changes" + When I am on the "Course 1" course page logged in as student1 + And I should see "Status: Not yet started" + And I should see "0 of 1" in the "Activity completion" "table_row" + And I follow "More details" + And I should see "task A" in the "criteriastatus" "table" + Then I should not see "task B" in the "criteriastatus" "table" + + @javascript + Scenario: Activities in the hidden section do not appear in the completion status block + Given the following "activities" exist: + | activity | name | intro | course | idnumber | section | visible | completion | completionview | + | page | task A | page description | C1 | page1 | 0 | 1 | 2 | 1 | + | page | task B | page description | C1 | page2 | 1 | 1 | 2 | 1 | + | assign | task C | page description | C1 | page3 | 2 | 1 | 2 | 1 | + | assign | task D | page description | C1 | page4 | 3 | 1 | 2 | 1 | + And I navigate to "Course completion" in current page administration + And I expand all fieldsets + # Set completion of the activities. + And I set the following fields to these values: + | Page - task A | 1 | + | Page - task B | 1 | + | Assignment - task C | 1 | + | Assignment - task D | 1 | + And I press "Save changes" + # Hide section 1 to make sure book activities are not visible to the student. + And I hide section "1" + # Add conditionally visible restriction to section 2. + And I edit the section "2" + And I expand all fieldsets + And I click on "Add restriction..." "button" + And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" + And I set the field "Activity or resource" to "task A" + And I press "Save changes" + # Add conditionally hidden restriction to section 3. + And I am on the "Course 1" course page + And I edit the section "3" + And I expand all fieldsets + And I click on "Add restriction..." "button" + And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" + And I set the field "Activity or resource" to "task A" + And I click on "Item name displayed with access restriction information if student doesn't meet this condition • Click to hide" "link" + And I press "Save changes" + And I log out + When I am on the "Course 1" course page logged in as student1 + And I should see "Status: Not yet started" + And I should see "0 of 1" in the "Activity completion" "table_row" + And I follow "More details" + Then I should see "task A" in the "criteriastatus" "table" + And I should not see "task B" in the "criteriastatus" "table" + And I should not see "task C" in the "criteriastatus" "table" + And I should not see "task D" in the "criteriastatus" "table" + # Complete task A to make other activities visible. + And I click on "task A" "link" + And I am on the "Course 1" course page logged in as student1 + And I should see "Status: In progress" + And I should see "1 of 3" in the "Activity completion" "table_row" + And I follow "More details" + And I should see "task A" in the "criteriastatus" "table" + And I should not see "task B" in the "criteriastatus" "table" + And I should see "task C" in the "criteriastatus" "table" + And I should see "task D" in the "criteriastatus" "table" + + @javascript + Scenario: Activities with disabled completion tracking are omitted from the completion view + Given the following "activities" exist: + | activity | name | intro | course | idnumber | section | visible | completion | completionview | + | page | task A | page description | C1 | page1 | 0 | 1 | 2 | 1 | + | page | task B | page description | C1 | page2 | 1 | 1 | 2 | 1 | + And I navigate to "Course completion" in current page administration + And I expand all fieldsets + # Set completion of the activities. + And I set the following fields to these values: + | Page - task A | 1 | + | Page - task B | 1 | + And I press "Save changes" + # Disable completion tracking for "task A". + And I am on the "task A" "page activity editing" page + And I expand all fieldsets + And I set the following fields to these values: + | completion | 0 | + And I press "Save and return to course" + When I am on the "Course 1" course page logged in as student1 + And I should see "Status: Not yet started" + And I should see "0 of 1" in the "Activity completion" "table_row" + And I follow "More details" + Then I should not see "task A" in the "criteriastatus" "table" + And I should see "task B" in the "criteriastatus" "table" + + @javascript + Scenario: Activities with group or grouping restrictions are omitted from the completion view + Given the following "activities" exist: + | activity | name | intro | course | idnumber | section | visible | completion | completionview | + | page | task A | page description | C1 | page1 | 0 | 1 | 2 | 1 | + | page | task B | page description | C1 | page2 | 1 | 1 | 2 | 1 | + | page | task C | page description | C1 | page3 | 2 | 1 | 2 | 1 | + And I navigate to "Course completion" in current page administration + And I expand all fieldsets + # Set completion of the activities. + And I set the following fields to these values: + | Page - task A | 1 | + | Page - task B | 1 | + | Page - task C | 1 | + And I press "Save changes" + # Add groups and groupings. + And the following "groups" exist: + | name | course | idnumber | + | G1 | C1 | GI1 | + | G2 | C1 | GI2 | + And the following "groupings" exist: + | name | course | idnumber | + | Grouping 1 | C1 | GG1 | + | Grouping 2 | C1 | GG2 | + And the following "grouping groups" exist: + | grouping | group | + | GG1 | GI1 | + | GG2 | GI2 | + # Add students to groups. + And the following "group members" exist: + | user | group | + | student1 | GI1 | + | student2 | GI2 | + # Add group restriction to "task B". + And I am on the "task B" "page activity editing" page + And I expand all fieldsets + And I click on "Add restriction..." "button" + And I click on "Group" "button" in the "Add restriction..." "dialogue" + And I set the field "Group" to "G1" + And I click on "Save and return to course" "button" + # Add Grouping and 'Activity or resource' restriction to "task C". + And I am on the "task C" "page activity editing" page + And I expand all fieldsets + And I click on "Add restriction..." "button" + And I click on "Grouping" "button" in the "Add restriction..." "dialogue" + And I set the field "Grouping" to "Grouping 2" + And I click on "Add restriction..." "button" + And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" + And I set the field "Activity or resource" to "task A" + And I click on "Save and return to course" "button" + When I am on the "Course 1" course page logged in as student1 + And I should see "Status: Not yet started" + And I should see "0 of 2" in the "Activity completion" "table_row" + And I follow "More details" + Then I should see "task A" in the "criteriastatus" "table" + And I should see "task B" in the "criteriastatus" "table" + And I should not see "task C" in the "criteriastatus" "table" + And I am on the "Course 1" course page logged in as student2 + And I should see "Status: Not yet started" + And I should see "0 of 2" in the "Activity completion" "table_row" + And I follow "More details" + And I should see "task A" in the "criteriastatus" "table" + And I should not see "task B" in the "criteriastatus" "table" + And I should see "task C" in the "criteriastatus" "table" diff --git a/public/blocks/course_summary/db/upgrade.php b/public/blocks/course_summary/db/upgrade.php index c87cdf70e77b9..2578d5dd39aa1 100644 --- a/public/blocks/course_summary/db/upgrade.php +++ b/public/blocks/course_summary/db/upgrade.php @@ -59,5 +59,8 @@ function xmldb_block_course_summary_upgrade($oldversion, $block) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/blocks/feedback/db/upgrade.php b/public/blocks/feedback/db/upgrade.php index 6d54da15bb088..9d5f3a70aaca4 100644 --- a/public/blocks/feedback/db/upgrade.php +++ b/public/blocks/feedback/db/upgrade.php @@ -58,5 +58,8 @@ function xmldb_block_feedback_upgrade($oldversion, $block) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/blocks/html/UPGRADING.md b/public/blocks/html/UPGRADING.md new file mode 100644 index 0000000000000..2f6f7e5e40d24 --- /dev/null +++ b/public/blocks/html/UPGRADING.md @@ -0,0 +1,10 @@ +# block_html Upgrade notes + +## 5.1.3+ + +### Changed + +- Treat Dashboard (pagetype 'my-index') as trusted in web services so get_content_for_external preserves embedded HTML (e.g. iframes) on user Dashboard. + + For more information see [MDL-85322](https://tracker.moodle.org/browse/MDL-85322) + diff --git a/public/blocks/html/block_html.php b/public/blocks/html/block_html.php index a771244f8d480..dae8b6e38a85d 100644 --- a/public/blocks/html/block_html.php +++ b/public/blocks/html/block_html.php @@ -165,14 +165,24 @@ public function instance_copy($fromid) { } function content_is_trusted() { - global $SCRIPT; - + global $SCRIPT, $USER; if (!$context = context::instance_by_id($this->instance->parentcontextid, IGNORE_MISSING)) { return false; } //find out if this block is on the profile page if ($context->contextlevel == CONTEXT_USER) { - if ($SCRIPT === '/my/index.php') { + $usersubpage = my_get_page($USER->id); + $usersubpage = $usersubpage->id ?? null; + if ( + $SCRIPT === '/my/index.php' || + ( + defined('WS_SERVER') && + WS_SERVER && + !empty($this->page) && + $this->page->pagetype === 'my-index' && + $this->page->subpage === $usersubpage + ) + ) { // this is exception - page is completely private, nobody else may see content there // that is why we allow JS here return true; diff --git a/public/blocks/html/db/upgrade.php b/public/blocks/html/db/upgrade.php index cc7b9cb2f4f86..e22c811cd2669 100644 --- a/public/blocks/html/db/upgrade.php +++ b/public/blocks/html/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_block_html_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/blocks/myoverview/db/upgrade.php b/public/blocks/myoverview/db/upgrade.php index 04fbee064019e..6780e4bf44594 100644 --- a/public/blocks/myoverview/db/upgrade.php +++ b/public/blocks/myoverview/db/upgrade.php @@ -44,5 +44,8 @@ function xmldb_block_myoverview_upgrade($oldversion) { // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. + // Automatically generated Moodle v5.1.0 release upgrade line. + // Put any upgrade step following this. + return true; } diff --git a/public/blocks/myoverview/templates/main.mustache b/public/blocks/myoverview/templates/main.mustache index a1c81adfcd983..7c369dca452a0 100644 --- a/public/blocks/myoverview/templates/main.mustache +++ b/public/blocks/myoverview/templates/main.mustache @@ -23,7 +23,7 @@ {} }} -