--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Generates a secure key for the current server (presuming it does not already exist).
+ *
+ * @package core_admin
+ * @copyright 2020 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use \core\encryption;
+
+define('CLI_SCRIPT', true);
+
+require(__DIR__ . '/../../config.php');
+require_once($CFG->libdir . '/clilib.php');
+
+// Get cli options.
+[$options, $unrecognized] = cli_get_params(
+ ['help' => false, 'method' => null],
+ ['h' => 'help']);
+
+if ($unrecognized) {
+ $unrecognized = implode("\n ", $unrecognized);
+ cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
+}
+
+if ($options['help']) {
+ echo "Generate secure key
+
+This script manually creates a secure key within the secret data root folder (configured in
+config.php as \$CFG->secretdataroot). You must run it using an account with access to write
+to that folder.
+
+In normal use Moodle automatically creates the key; this script is intended when setting up
+a new Moodle system, for cases where the secure folder is not on shared storage and the key
+may be manually installed on multiple servers.
+
+Options:
+-h, --help Print out this help
+--method <method> Generate key for specified encryption method instead of default.
+ * sodium
+ * openssl-aes-256-ctr
+
+Example:
+php admin/cli/generate_key.php
+";
+ exit;
+}
+
+$method = $options['method'];
+
+if (encryption::key_exists($method)) {
+ echo 'Key already exists: ' . encryption::get_key_file($method) . "\n";
+ exit;
+}
+
+// Creates key with default permissions (no chmod).
+echo "Generating key...\n";
+encryption::create_key($method, false);
+
+echo "\nKey created: " . encryption::get_key_file($method) . "\n\n";
+echo "If the key folder is not shared storage, then key files should be copied to all servers.\n";
$settings->add(new admin_setting_configduration('analytics/modeltimelimit', new lang_string('modeltimelimit', 'analytics'),
new lang_string('modeltimelimitinfo', 'analytics'), 20 * MINSECS));
+ $options = array(
+ 0 => new lang_string('neverdelete', 'analytics'),
+ 1000 => new lang_string('numdays', '', 1000),
+ 365 => new lang_string('numdays', '', 365),
+ 180 => new lang_string('numdays', '', 180),
+ 150 => new lang_string('numdays', '', 150),
+ 120 => new lang_string('numdays', '', 120),
+ 90 => new lang_string('numdays', '', 90),
+ 60 => new lang_string('numdays', '', 60),
+ 35 => new lang_string('numdays', '', 35));
+ $settings->add(new admin_setting_configselect('analytics/calclifetime',
+ new lang_string('calclifetime', 'analytics'),
+ new lang_string('configlcalclifetime', 'analytics'), 35, $options));
+
+
}
}
--- /dev/null
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+ @template core_admin/admin_setting_encryptedpassword
+
+ Admin encrypted password template.
+
+ Context variables required for this template:
+ * name - form element name
+ * set - whether it is set or empty
+ * id - element id
+
+ Example context (json):
+ {
+ "name": "test",
+ "id": "test0",
+ "set": true
+ }
+}}
+<div class="core_admin_encryptedpassword" data-encryptedpasswordid="{{ id }}"
+ {{#novalue}}data-novalue="y"{{/novalue}}>
+ {{#set}}
+ <span>{{# str }} encryptedpassword_set, admin {{/ str }}</span>
+ {{/set}}
+ {{^set}}
+ <a href="#" title="{{# str }} encryptedpassword_edit, admin {{/ str }}">
+ <span>{{# str }} novalueclicktoset, form {{/ str }}</span>
+ {{# pix }} t/passwordunmask-edit, core, {{# str }} passwordunmaskedithint, form {{/ str }}{{/ pix }}
+ </a>
+ {{/set}}
+ <input style="display: none" type="password" name="{{name}}" disabled>
+ {{!
+ Using buttons instead of links here allows them to be connected to the label, so the button
+ works if you click the label.
+ }}
+ {{#set}}
+ <button type="button" id="{{id}}" title="{{# str }} encryptedpassword_edit, admin {{/ str }}" class="btn btn-link" data-editbutton>
+ {{# pix }} t/passwordunmask-edit, core, {{/ pix }}
+ </button>
+ {{/set}}
+ <button type="button" style="display: none" title="{{# str }} cancel {{/ str }}" class="btn btn-link" data-cancelbutton>
+ <i class="icon fa fa-times"></i>
+ </button>
+</div>
+
+{{#js}}
+require(['core_form/encryptedpassword'], function(encryptedpassword) {
+ new encryptedpassword.EncryptedPassword("{{ id }}");
+});
+{{/js}}
}
/**
- * Sets the specified site settings. A table with | config | value | (optional)plugin | is expected.
+ * Sets the specified site settings. A table with | config | value | (optional)plugin | (optional)encrypted | is expected.
*
* @Given /^the following config values are set as admin:$/
* @param TableNode $table
foreach ($data as $config => $value) {
// Default plugin value is null.
$plugin = null;
+ $encrypted = false;
if (is_array($value)) {
$plugin = $value[1];
+ if (array_key_exists(2, $value)) {
+ $encrypted = $value[2] === 'encrypted';
+ }
$value = $value[0];
}
+
+ if ($encrypted) {
+ $value = \core\encryption::encrypt($value);
+ }
+
set_config($config, $value, $plugin);
}
}
This files describes API changes in /admin/*.
+=== 3.11 ===
+
+* New admin setting admin_setting_encryptedpassword allows passwords in admin settings to be
+ encrypted (with the new \core\encryption API) so that even the admin cannot read them.
+
=== 3.9 ===
* The following functions, previously used (exclusively) by upgrade steps are not available anymore because of the upgrade cleanup performed for this version. See MDL-65809 for more info:
}
/**
- * Returns the enabled time splitting methods.
- *
- * @deprecated since Moodle 3.7
- * @todo MDL-65086 This will be deleted in Moodle 3.11
- * @see \core_analytics\manager::get_time_splitting_methods_for_evaluation
- * @return \core_analytics\local\time_splitting\base[]
+ * @deprecated since Moodle 3.7 use get_time_splitting_methods_for_evaluation instead
*/
public static function get_enabled_time_splitting_methods() {
- debugging('This function has been deprecated. You can use self::get_time_splitting_methods_for_evaluation if ' .
+ throw new coding_exception(__FUNCTION__ . '() has been removed. You can use self::get_time_splitting_methods_for_evaluation if ' .
'you want to get the default time splitting methods for evaluation, or you can use self::get_all_time_splittings if ' .
'you want to get all the time splitting methods available on this site.');
- return self::get_time_splitting_methods_for_evaluation();
}
/**
$param + $idsparams);
}
}
+
+ // Clean up calculations table.
+ $calclifetime = get_config('analytics', 'calclifetime');
+ if (!empty($calclifetime)) {
+ $lifetime = time() - ($calclifetime * DAYSECS); // Value in days.
+ $DB->delete_records_select('analytics_indicator_calc', 'timecreated < ?', [$lifetime]);
+ }
}
/**
This files describes API changes in analytics sub system,
information provided here is intended especially for developers.
+=== 3.11 ===
+* Final deprecation get_enabled_time_splitting_methods. Method has been removed. Use
+ get_time_splitting_methods_for_evaluation instead.
+
=== 3.8 ===
* "Time-splitting method" have been replaced by "Analysis interval" for the language strings that are
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This is the external method used for fetching the addable blocks in a given page.
+ *
+ * @package core_block
+ * @since Moodle 3.11
+ * @copyright 2020 Mihail Geshoski <mihail@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_block\external;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/externallib.php');
+
+use external_api;
+use external_function_parameters;
+use external_multiple_structure;
+use external_single_structure;
+use external_value;
+
+/**
+ * This is the external method used for fetching the addable blocks in a given page.
+ *
+ * @copyright 2020 Mihail Geshoski <mihail@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class fetch_addable_blocks extends external_api {
+
+ /**
+ * Describes the parameters for execute.
+ *
+ * @return external_function_parameters
+ */
+ public static function execute_parameters(): external_function_parameters {
+ return new external_function_parameters(
+ [
+ 'pagecontextid' => new external_value(PARAM_INT, 'The context ID of the page.'),
+ 'pagetype' => new external_value(PARAM_ALPHAEXT, 'The type of the page.'),
+ 'pagelayout' => new external_value(PARAM_ALPHA, 'The layout of the page.')
+ ]
+ );
+ }
+
+ /**
+ * Fetch the addable blocks in a given page.
+ *
+ * @param int $pagecontextid The context ID of the page
+ * @param string $pagetype The type of the page
+ * @param string $pagelayout The layout of the page
+ * @return array The blocks list
+ */
+ public static function execute(int $pagecontextid, string $pagetype, string $pagelayout): array {
+ global $PAGE;
+
+ $params = self::validate_parameters(self::execute_parameters(),
+ [
+ 'pagecontextid' => $pagecontextid,
+ 'pagetype' => $pagetype,
+ 'pagelayout' => $pagelayout
+ ]
+ );
+
+ $context = \context::instance_by_id($params['pagecontextid']);
+ // Validate the context. This will also set the context in $PAGE.
+ self::validate_context($context);
+
+ // We need to manually set the page layout and page type.
+ $PAGE->set_pagelayout($params['pagelayout']);
+ $PAGE->set_pagetype($params['pagetype']);
+ // Firstly, we need to load all currently existing page blocks to later determine which blocks are addable.
+ $PAGE->blocks->load_blocks(false);
+ $PAGE->blocks->create_all_block_instances();
+
+ $addableblocks = $PAGE->blocks->get_addable_blocks();
+
+ return array_map(function($block) {
+ return [
+ 'name' => $block->name,
+ 'title' => get_string('pluginname', "block_{$block->name}")
+ ];
+ }, $addableblocks);
+ }
+
+ /**
+ * Describes the execute return value.
+ *
+ * @return external_multiple_structure
+ */
+ public static function execute_returns(): external_multiple_structure {
+ return new external_multiple_structure(
+ new external_single_structure(
+ [
+ 'name' => new external_value(PARAM_PLUGIN, 'The name of the block.'),
+ 'title' => new external_value(PARAM_RAW, 'The title of the block.'),
+ ]
+ ),
+ 'List of addable blocks in a given page.'
+ );
+ }
+}
//
// $CFG->maxcoursesincategory = 10000;
//
+// Admin setting encryption
+//
+// $CFG->secretdataroot = '/var/www/my_secret_folder';
+//
+// Location to store encryption keys. By default this is $CFG->dataroot/secret; set this if
+// you want to use a different location for increased security (e.g. if too many people have access
+// to the main dataroot, or if you want to avoid using shared storage). Your web server user needs
+// read access to this location, and write access unless you manually create the keys.
+//
+// $CFG->nokeygeneration = false;
+//
+// If you change this to true then the server will give an error if keys don't exist, instead of
+// automatically generating them. This is only needed if you want to ensure that keys are consistent
+// across a cluster when not using shared storage. If you stop the server generating keys, you will
+// need to manually generate them by running 'php admin/cli/generate_key.php'.
+
//=========================================================================
// 7. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
//=========================================================================
const firstChooserOption = sectionChooserOptions.querySelector(selectors.regions.chooserOption.container);
toggleFocusableChooserOption(firstChooserOption, true);
- initTabsKeyboardNavigation(body);
initChooserOptionsKeyboardNavigation(body, mappedModules, sectionChooserOptions, modal);
return body;
.catch();
};
-/**
- * Initialise the keyboard navigation controls for the tab list items.
- *
- * @method initTabsKeyboardNavigation
- * @param {HTMLElement} body Our modal that we are working with
- */
-const initTabsKeyboardNavigation = (body) => {
- // Set up the tab handlers.
- const favTabNav = body.querySelector(selectors.regions.favouriteTabNav);
- const recommendedTabNav = body.querySelector(selectors.regions.recommendedTabNav);
- const defaultTabNav = body.querySelector(selectors.regions.defaultTabNav);
- const activityTabNav = body.querySelector(selectors.regions.activityTabNav);
- const resourceTabNav = body.querySelector(selectors.regions.resourceTabNav);
- const tabNavArray = [favTabNav, recommendedTabNav, defaultTabNav, activityTabNav, resourceTabNav];
- tabNavArray.forEach((element) => {
- return element.addEventListener('keydown', (e) => {
- // The first visible navigation tab link.
- const firstLink = e.target.parentElement.querySelector(selectors.elements.visibletabs);
- // The last navigation tab link. It would always be the default activities tab link.
- const lastLink = e.target.parentElement.lastElementChild;
-
- if (e.keyCode === arrowRight) {
- const nextLink = e.target.nextElementSibling;
- if (nextLink === null) {
- e.target.tabIndex = -1;
- firstLink.tabIndex = 0;
- firstLink.focus();
- } else if (nextLink.classList.contains('d-none')) {
- e.target.tabIndex = -1;
- lastLink.tabIndex = 0;
- lastLink.focus();
- } else {
- e.target.tabIndex = -1;
- nextLink.tabIndex = 0;
- nextLink.focus();
- }
- }
- if (e.keyCode === arrowLeft) {
- const previousLink = e.target.previousElementSibling;
- if (previousLink === null) {
- e.target.tabIndex = -1;
- lastLink.tabIndex = 0;
- lastLink.focus();
- } else if (previousLink.classList.contains('d-none')) {
- e.target.tabIndex = -1;
- firstLink.tabIndex = 0;
- firstLink.focus();
- } else {
- e.target.tabIndex = -1;
- previousLink.tabIndex = 0;
- previousLink.focus();
- }
- }
- if (e.keyCode === home) {
- e.target.tabIndex = -1;
- firstLink.tabIndex = 0;
- firstLink.focus();
- }
- if (e.keyCode === end) {
- e.target.tabIndex = -1;
- lastLink.tabIndex = 0;
- lastLink.focus();
- }
- if (e.keyCode === space) {
- e.preventDefault();
- e.target.click();
- }
- });
- });
-};
-
/**
* Initialise the keyboard navigation controls for the chooser options.
*
help: getDataSelector('region', 'help'),
modules: getDataSelector('region', 'modules'),
favouriteTabNav: getDataSelector('region', 'favourite-tab-nav'),
- recommendedTabNav: getDataSelector('region', 'recommended-tab-nav'),
defaultTabNav: getDataSelector('region', 'default-tab-nav'),
activityTabNav: getDataSelector('region', 'activity-tab-nav'),
- resourceTabNav: getDataSelector('region', 'resources-tab-nav'),
favouriteTab: getDataSelector('region', 'favourites'),
recommendedTab: getDataSelector('region', 'recommended'),
defaultTab: getDataSelector('region', 'default'),
'component' => new external_value(PARAM_COMPONENT, 'component'),
'area' => new external_value(PARAM_ALPHANUMEXT, 'area'),
'itemid' => new external_value(PARAM_INT, 'itemid'),
- 'usescategories' => new external_value(PARAM_INT, 'view has categories'),
+ 'usescategories' => new external_value(PARAM_BOOL, 'view has categories'),
'categories' => new external_multiple_structure(
new external_single_structure(
array(
$url,
$config,
$this->factory,
- $this->messages
+ $this->messages,
+ $this->preventredirect
);
if ($file) {
$this->context = \context::instance_by_id($file->get_contextid());
--- /dev/null
+<?php
+// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package installer
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['thislanguage'] = 'Аԥсуа бызшәа';
--- /dev/null
+<?php
+// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package installer
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['language'] = 'اللغة';
+$string['next'] = 'التالي';
+$string['previous'] = 'السابق';
+$string['reload'] = 'إعادة تحميل';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'বাংলা';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'Català';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'Čeština';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'Dansk';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'Deutsch';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'Ελληνικά';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'English';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'Español (México)';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'eesti';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'Euskara';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'Français';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'Gaeilge';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'Galego';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'magyar';
$string['cliansweryes'] = 'այո';
$string['cliincorrectvalueerror'] = 'Սխալ, "{$a->value}"-ը ոչ ճիշտ արժեք է "{$a->option}"-ի համար';
$string['cliincorrectvalueretry'] = 'Ոչ ճիշտ արժեք, կրկին փորձեք';
+$string['clitypevalue'] = 'մուտքագրեք արժեք';
+$string['clitypevaluedefault'] = 'մուտքագրեք արժեք, սեղմեք Enter ՝ կանխադրված արժեքն օգտագործելու համար';
+$string['cliunknowoption'] = 'Չճանաչված տարբերակներ՝
+ {$a}
+Խնդրում ենք օգտագործել - help տարբերակը:';
$string['cliyesnoprompt'] = 'Մուտքագրեք y (նշանակում է այո) կամ n (նշանակում է ոչ):';
$string['environmentrequireinstall'] = 'անհրաժեշտ է, որպեսզի տեղակայված և թույլատրված լինի';
$string['environmentrequireversion'] = 'պահանջվում է {$a->needed} տարբերակը, դուք գործարկում եք՝ {$a->current}';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'Italiano';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = '日本語';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = '한국어';
defined('MOODLE_INTERNAL') || die();
+$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'Кыргызча';
--- /dev/null
+<?php
+// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package installer
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['language'] = 'Тил';
+$string['moodlelogo'] = 'Moodle логотиби';
+$string['next'] = 'Мындан ары';
+$string['previous'] = 'Артка';
+$string['reload'] = 'Жаңыртуу';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'Lietuvių';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thislanguage'] = 'Crnogorski';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'Nederlands';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'Norsk';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'Gascon';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'Occitan-Lengadocian';
$string['pathsrodataroot'] = 'A pasta de dados não tem permissões de escrita.';
$string['pathsroparentdataroot'] = 'A pasta ascendente <b>{$a->parent}</b> não tem permissões de escrita. O programa de instalação não conseguiu criar a pasta <b>{$a->dataroot}</b>.';
$string['pathssubadmindir'] = 'Alguns servidores Web utilizam a pasta <strong>admin</strong> em URLs especiais de acesso a funcionalidades especiais, como é o caso de painéis de controlo. Algumas situações podem criar conflitos com a localização normal das páginas de administração do Moodle. Estes problemas podem ser resolvidos renomeando a pasta <strong>admin</strong> na instalação do Moodle e indicando aqui o novo nome a utilizar. Por exemplo:<br /><br /><b>moodleadmin</b><br /><br />Esta ação resolverá os problemas de acesso das hiperligações para as funcionalidades de administração do Moodle.';
-$string['pathssubdataroot'] = '<p>Uma diretoria em que o Moodle irá armazenar todo o conteúdo de ficheiros enviados pelos utilizadores.</p>
+$string['pathssubdataroot'] = '<p>Diretoria onde o Moodle irá armazenar todo o conteúdo de ficheiros enviados pelos utilizadores.</p>
<p>Esta diretoria deve ser legível e gravável pelo utilizador do servidor web (geralmente \'www-data\', \'nobody\', ou \'apache\').</p>
<p>Não deve ser acessível diretamente através da web.</p>
<p>Se a diretoria não existir, o processo de instalação tentará criá-la.</p>';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'A apresentação desta página confirma a correta instalação e ativação do pacote <strong>{$a->packname} {$a->packversion}</strong> no servidor.';
$string['welcomep30'] = 'Esta versão do pacote <strong>{$a->installername}</strong> inclui as aplicações necessárias para o correto funcionamento do <strong>Moodle</strong>, nomeadamente:';
-$string['welcomep40'] = 'Este pacote inclui o lançamento <strong>{$a->moodlerelease} do Moodle ({$a->moodleversion})</strong>.';
+$string['welcomep40'] = 'Este pacote inclui o lançamento <strong>{$a->moodlerelease} ({$a->moodleversion})</strong> do Moodle .';
$string['welcomep50'] = 'A utilização de todas as aplicações incluídas neste pacote é limitada pelas respetivas licenças. O pacote completo <strong>{$a->installername}</strong> é <ahref="https://www.opensource.org/docs/definition_plain.html">código aberto</a> e é distribuído nos termos da licença <a href="http://www.gnu.org/copyleft/gpl.html">GPL</a>.';
$string['welcomep60'] = 'As páginas seguintes irão acompanhá-lo através de algumas etapas simples para configurar e definir o <strong>Moodle</strong> no seu computador. Pode aceitar as configurações predefinidas ou, opcionalmente, alterá-las para adaptar às suas próprias necessidades.';
$string['welcomep70'] = 'Clique no botão "Seguinte" para continuar a configuração do <strong>Moodle</strong>.';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'Português - Portugal';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'Português - Brasil';
$string['errorsinenvironment'] = 'Verificarea mediului eșuată!';
$string['installation'] = 'Instalare';
$string['langdownloaderror'] = 'Din păcate, limba "{$a}" nu a putut fi descărcată. Procesul de instalare va continua în limba engleză.';
+$string['memorylimithelp'] = '<p> Limita de memorie PHP pentru serverul dvs. este setată în prezent la {$ a}. </p>
+
+<p> Acest lucru poate determina Moodle să aibă probleme de memorie mai târziu, în special
+ dacă aveți o mulțime de module activate și / sau o mulțime de utilizatori. </p>
+
+<p> Vă recomandăm să configurați PHP cu o limită mai mare, dacă este posibil, cum ar fi 40M.
+ Există mai multe moduri de a face acest lucru pe care le puteți încerca: </p>
+<ol>
+<li> Dacă puteți, recompilați PHP cu <i> --enable-memory-limit </i>.
+ Aceasta va permite Moodle să stabilească singură limita de memorie. </li>
+<li> Dacă aveți acces la fișierul php.ini, puteți schimba <b> memory_limit </b>
+ stabilind acolo ceva de genul 40M. Dacă nu aveți acces, s-ar putea
+ să îi puteți cere administratorului să facă acest lucru pentru dvs. </li>
+<li> Pe unele servere PHP puteți crea un fișier .htaccess în directorul Moodle
+ care conține această linie:
+ <blockquote> <div> php_value memory_limit 40M </div> </blockquote>
+ <p> Cu toate acestea, pe unele servere, acest lucru va împiedica <b> toate </b> paginile PHP să funcționeze
+ (veți vedea erori când vă uitați la pagini), așa că va trebui să eliminați fișierul .htaccess. </p> </li>
+</ol>';
$string['paths'] = 'Căi';
$string['pathserrcreatedataroot'] = 'Data directory ({$a->dataroot}) nu poate fi creat de către installer.';
$string['pathshead'] = 'Confirmare căi';
$string['pathsrodataroot'] = 'Directorul dataroot nu poate fi scris.';
$string['pathsroparentdataroot'] = 'Directorul parent ({$a->parent}) nu poate fi scris. Directorul data ({$a->dataroot}) nu poate fi creat de persoana care îl instalează.';
+$string['pathssubadmindir'] = 'Foarte puțini webhosts folosesc / admin ca adresă URL specială pentru a accesa un
+panoul de control sau ceva de genul acesta. Din păcate, acest lucru intră în conflict cu locația standard pentru paginile de administrare Moodle. Puteți rezolva acest lucru
+redenumirea directorului de administrare din instalarea dvs. și plasarea acestui nou nume aici. De exemplu: <em> moodleadmin </em>. Aceasta va remedia linkurile de administrator din Moodle.';
$string['pathssubdataroot'] = '<p>Un director unde Moodle va stoca tot conținutul unui fișier încărcat de către utilizatori.</p>
<p>Acest director trebuie să poată fi citit și scris de către utilizatorii serverului web (de obicei \'www-data\', \'nobody\', or \'apache\').</p>
<p>Nu trebuie să fie direct accesibil de pe web.</p>
<p>Dacă directorul nu există în prezent, procesul de instalare va încerca să îl creeze.</p>';
$string['pathssubdirroot'] = '<p>Calea completă către directorul care conține codul Moodle .</p>';
+$string['pathssubwwwroot'] = '<p> Adresa completă unde va fi accesat Moodle, adică adresa pe care utilizatorii o vor introduce în bara de adrese a browserului lor pentru a accesa Moodle. </p>
+<p> Nu este posibil să accesați Moodle utilizând mai multe adrese. Dacă site-ul dvs. este accesibil prin mai multe adrese, alegeți-l pe cel mai simplu și configurați o redirecționare permanentă pentru fiecare dintre celelalte adrese. </p>
+<p> Dacă site-ul dvs. este accesibil atât de pe Internet, cât și de pe o rețea internă (uneori numită Intranet), atunci utilizați adresa publică aici. </p>
+<p> Dacă adresa curentă nu este corectă, vă rugăm să modificați adresa URL din bara de adrese a browserului dvs. și să reporniți instalarea. </p>';
$string['pathsunsecuredataroot'] = 'Locația dataroot nu este sigură';
$string['pathswrongadmindir'] = 'Directorul admin nu există';
$string['phpextension'] = 'extensie PHP {$a}';
$string['phpversion'] = 'Versiune PHP';
-$string['phpversionhelp'] = '<p>Moodle necesită o versiune PHP de cel puțin 4.3.0 sau 5.1.0 (5.0.x are un număr de probleme cunscute).</p>
-<p>Momentan rulați versiunea {$a}</p>
-<p>Trebuie să upgradați PHP sau să îl mutați pe o gazdă cu o nouă versiune de PHP!<br />
-(În cazul 5.0.x puteți, de asemenea, să downgradați la versiunea 4.4.x)</p>';
+$string['phpversionhelp'] = '<p> Moodle necesită o versiune PHP de cel puțin 5.6.5 sau 7.1 (7.0.x are unele limitări ale motorului). </p>
+<p> În prezent executați versiunea {$ a}. </p>
+<p> Trebuie să faceți upgrade PHP sau să vă mutați la o gazdă cu o versiune mai nouă de PHP. </p>';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'Vedeți această pagină deoarece ați instalat și lansat cu succes pachetul <strong>{$a->packname} {$a->packversion}</strong> în computerul dumneavoastră. Felicitări!';
$string['welcomep30'] = 'Această versiune <strong>{$a->installername}</strong> include aplicații pentru a crea un mediu în care <strong>Moodle</strong> va funcționa, și anume:';
$string['welcomep40'] = 'Pachetul include și <strong>Moodle {$a->moodlerelease} ({$a->moodleversion})</strong>.';
-$string['welcomep50'] = 'Utilizarea tuturor aplicațiilor din acest pachet este determinată de respectivele lor
- licențe. Pachetul complet <strong>{$a->installername}</strong> este
- <a href="http://www.opensource.org/docs/definition_plain.html">open source</a> și este distribuit sub licența <a href="http://www.gnu.org/copyleft/gpl.html">GPL</a>.';
+$string['welcomep50'] = 'Utilizarea tuturor aplicațiilor din acest pachet este guvernată de licențele respective. Pachetul complet <strong> {$a->installername} </strong> este <a href="https://www.opensource.org/docs/definition_plain.html"> open source </a> și este distribuit în licența <a href="https://www.gnu.org/copyleft/gpl.html"> GPL </a>.';
$string['welcomep60'] = 'Următoarele pagini vă oferă pași ușor de urmat pentru a
configura și seta <strong>Moodle</strong> în computerul dumneavoastră. Puteți accepta setările implicite
sau, opțional, să le modificați pentru a corespunde nevoilor dumneavoastră.';
defined('MOODLE_INTERNAL') || die();
$string['clianswerno'] = 'n';
-$string['cliansweryes'] = 'y';
-$string['cliincorrectvalueretry'] = 'Valoarea nu este corectă, încercați din nou';
-$string['environmentrequireinstall'] = 'trebuie să fie instalat și activat';
-$string['environmentrequireversion'] = 'versiunea {$a->needed} este necesară iar dumneavoastră rulați {$a->current}';
+$string['cliansweryes'] = 'd';
+$string['cliincorrectvalueerror'] = 'Eroare, valoare incorectă "{$a->value}" pentru "{$a->option}"';
+$string['cliincorrectvalueretry'] = 'Valoare incorectă, încercaţi din nou';
+$string['clitypevalue'] = 'introduceţi valoarea';
+$string['clitypevaluedefault'] = 'introduceţi valoarea, apăsaţi Enter pentru a folosi valoarea implicită ({$a})';
+$string['cliunknowoption'] = 'Opţiuni necunoscute:
+ {$a}
+Vă rugăm folosiţi --opţiunea Ajutor.';
+$string['cliyesnoprompt'] = 'tastați d (pentru \'da\') sau \'n\' (pentru \'nu\')';
+$string['environmentrequireinstall'] = 'trebuie instalat şi activat';
+$string['environmentrequireversion'] = 'versiuna necesară este {$a->needed} în timp ce dumneavoastră rulaţi versiunea {$a->current}';
+$string['upgradekeyset'] = 'Actualizează cheie (lăsați gol pentru a nu fi setat)';
--- /dev/null
+<?php
+// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package installer
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['cannotcreatedboninstall'] = '<p>Nu se poate crea baza de date.</p>
+<p>Baza de date specificată nu există și utilizatorul dat nu are permisiunea de a crea baza de date.</p>
+<p>Administratorul site-ului trebuie să verifice configurația bazei de date.</p>';
+$string['cannotcreatelangdir'] = 'Nu se poate crea directorul lang';
+$string['cannotcreatetempdir'] = 'Nu se poate crea directorul temporar';
+$string['cannotdownloadcomponents'] = 'Nu se pot descărca componente';
+$string['cannotdownloadzipfile'] = 'Nu se poate descărca fișierul ZIP';
+$string['cannotfindcomponent'] = 'Componenta nu poate fi găsită';
+$string['cannotsavemd5file'] = 'Nu se poate salva fișierul md5';
+$string['cannotsavezipfile'] = 'Nu se poate salva fișierul ZIP';
+$string['cannotunzipfile'] = 'Nu se poate dezarhiva fișierul';
+$string['componentisuptodate'] = 'Componenta este actualizată';
+$string['dmlexceptiononinstall'] = '<p>A apărut o eroare în baza de date [{$a->errorcode}].<br />{$a->debuginfo}</p>';
+$string['downloadedfilecheckfailed'] = 'Verificarea fișierului descărcat a eșuat';
+$string['invalidmd5'] = 'Variabila de verificare a fost greșită - încercați din nou';
+$string['missingrequiredfield'] = 'Un câmp obligatoriu lipsește';
+$string['remotedownloaderror'] = '<p>Descărcarea componentei pe serverul dvs. nu a reușit. Vă rugăm să verificați setările proxy; extensia PHP cURL este foarte recomandată.</p>
+<p>Trebuie să descărcați manual fișierul <a href="{$a->url}">{$a->url}</a>, copiați-l în „{$a->dest}" de pe server și dezarhivați-l Acolo.</p>';
+$string['wrongdestpath'] = 'Cale de destinație greșită';
+$string['wrongsourcebase'] = 'Baza de adrese URL sursă este greșită';
+$string['wrongzipfilename'] = 'Numele fișierului ZIP greșit';
--- /dev/null
+<?php
+// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package installer
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['admindirname'] = 'Director Admin';
+$string['availablelangs'] = 'Pachete de limbă disponibile';
+$string['chooselanguagehead'] = 'Selectare limbă';
+$string['chooselanguagesub'] = 'Vă rugăm selectaţi limba pentru interfaţa de instalare, limba selectată va fi folosită EXCLUSIV în cadrul procedurii de instalare. Ulterior veţi putea selecta limba în care doriţi să fie afişată interfaţa.';
+$string['clialreadyconfigured'] = 'Fișierul de configurare';
+$string['clialreadyinstalled'] = 'Fișierul de configurare config.php există deja. Vă rugăm să folosiți dmin/cli/install_database.php to pentru a upgrada Moodle pentru acest site.';
+$string['cliinstallheader'] = 'Program command line installation Moodle {$a}';
+$string['databasehost'] = 'Gazdă baza de date';
+$string['databasename'] = 'Nume baza de date';
+$string['databasetypehead'] = 'Alegere driver baza de date';
+$string['dataroot'] = 'Director date';
+$string['datarootpermission'] = 'Permisiuni directoare date';
+$string['dbprefix'] = 'Prefix tabele';
+$string['dirroot'] = 'Director Moodle';
+$string['environmenthead'] = 'Se verifică mediul...';
+$string['environmentsub2'] = 'Fiecare versiune Moodle are o anumită cerință minimă PHP și un număr de extensii PHP obligatorii.
+Verificarea completă a mediului se face înainte de fiecare instalare și upgrade. Vă rugăm să contactați administratorul serverului, dacă nu știți cum se instalează noua versiune sau dacă activați extensiile PHP.';
+$string['errorsinenvironment'] = 'Verificarea mediului eșuată!';
+$string['installation'] = 'Instalare';
+$string['langdownloaderror'] = 'Din păcate, limba "{$a}" nu a putut fi descărcată. Procesul de instalare va continua în limba engleză.';
+$string['paths'] = 'Căi';
+$string['pathserrcreatedataroot'] = 'Data directory ({$a->dataroot}) nu poate fi creat de către installer.';
+$string['pathshead'] = 'Confirmare căi';
+$string['pathsrodataroot'] = 'Directorul dataroot nu poate fi scris.';
+$string['pathsroparentdataroot'] = 'Directorul parent ({$a->parent}) nu poate fi scris. Directorul data ({$a->dataroot}) nu poate fi creat de persoana care îl instalează.';
+$string['pathssubdataroot'] = '<p>Un director unde Moodle va stoca tot conținutul unui fișier încărcat de către utilizatori.</p>
+<p>Acest director trebuie să poată fi citit și scris de către utilizatorii serverului web (de obicei \'www-data\', \'nobody\', or \'apache\').</p>
+<p>Nu trebuie să fie direct accesibil de pe web.</p>
+<p>Dacă directorul nu există în prezent, procesul de instalare va încerca să îl creeze.</p>';
+$string['pathssubdirroot'] = '<p>Calea completă către directorul care conține codul Moodle .</p>';
+$string['pathsunsecuredataroot'] = 'Locația dataroot nu este sigură';
+$string['pathswrongadmindir'] = 'Directorul admin nu există';
+$string['phpextension'] = 'extensie PHP {$a}';
+$string['phpversion'] = 'Versiune PHP';
+$string['phpversionhelp'] = '<p>Moodle necesită o versiune PHP de cel puțin 4.3.0 sau 5.1.0 (5.0.x are un număr de probleme cunscute).</p>
+<p>Momentan rulați versiunea {$a}</p>
+<p>Trebuie să upgradați PHP sau să îl mutați pe o gazdă cu o nouă versiune de PHP!<br />
+(În cazul 5.0.x puteți, de asemenea, să downgradați la versiunea 4.4.x)</p>';
+$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
+$string['welcomep20'] = 'Vedeți această pagină deoarece ați instalat și lansat cu succes pachetul <strong>{$a->packname} {$a->packversion}</strong> în computerul dumneavoastră. Felicitări!';
+$string['welcomep30'] = 'Această versiune <strong>{$a->installername}</strong> include aplicații pentru a crea un mediu în care <strong>Moodle</strong> va funcționa, și anume:';
+$string['welcomep40'] = 'Pachetul include și <strong>Moodle {$a->moodlerelease} ({$a->moodleversion})</strong>.';
+$string['welcomep50'] = 'Utilizarea tuturor aplicațiilor din acest pachet este determinată de respectivele lor
+ licențe. Pachetul complet <strong>{$a->installername}</strong> este
+ <a href="http://www.opensource.org/docs/definition_plain.html">open source</a> și este distribuit sub licența <a href="http://www.gnu.org/copyleft/gpl.html">GPL</a>.';
+$string['welcomep60'] = 'Următoarele pagini vă oferă pași ușor de urmat pentru a
+ configura și seta <strong>Moodle</strong> în computerul dumneavoastră. Puteți accepta setările implicite
+ sau, opțional, să le modificați pentru a corespunde nevoilor dumneavoastră.';
+$string['welcomep70'] = 'Click pe butonul "Next" de mai jos pentru a continua setarea <strong>Moodle</strong>.';
+$string['wwwroot'] = 'Adresă Web';
defined('MOODLE_INTERNAL') || die();
$string['parentlanguage'] = 'ro';
-$string['thislanguage'] = 'Workplace în limba română';
+$string['thisdirection'] = 'ltr';
+$string['thislanguage'] = 'Română';
$string['language'] = 'Limbă';
$string['moodlelogo'] = 'Logo Moodle';
-$string['next'] = 'Următoarea';
+$string['next'] = 'Următorul';
$string['previous'] = 'Anterior';
$string['reload'] = 'Reîncarcă';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'Slovenščina';
$string['clianswerno'] = 'n';
$string['cliansweryes'] = 'y';
$string['cliincorrectvalueerror'] = 'Fel, värdet "{$a->value}" för "{$a->option}" är inte korrekt.';
-$string['cliincorrectvalueretry'] = 'Felaktigt värde, v.g. försök igen';
+$string['cliincorrectvalueretry'] = 'Felaktigt värde, försök igen';
$string['clitypevalue'] = 'Värde för typ';
$string['clitypevaluedefault'] = 'skriv in värdet, klicka på "Enter" om du vill använda standardvärdet ({$a})';
-$string['cliunknowoption'] = 'Ej identifierade alternativ: {$a} V.g. använd alternativet Hjälp.';
+$string['cliunknowoption'] = 'Ej identifierade alternativ:
+ {$a}
+Vänligen använd --help alternativet.';
$string['cliyesnoprompt'] = 'skriv in y (betyder ja) eller n (betyder nej)';
$string['environmentrequireinstall'] = 'är nödvändig att installera/aktivera';
$string['environmentrequireversion'] = 'version {$a->needed} är nödvändig och du använder {$a->current}';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = '简体中文';
defined('MOODLE_INTERNAL') || die();
-// Warning: this parentlanguage value is not a valid language code!
-// $string['parentlanguage'] = '';
+$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = '正體中文';
$string['enableuserfeedback_desc'] = 'If enabled, a \'Give feedback about this software\' link is displayed in the footer for users to give feedback about the Moodle software to Moodle HQ. If the \'Next feedback reminder\' option is set, the user is also shown a reminder on the Dashboard at the specified interval. Setting \'Next feedback reminder\' to \'Never\' disables the Dashboard reminder, while leaving the \'Give feedback about this software\' link in the footer.';
$string['enablewebservices'] = 'Enable web services';
$string['enablewsdocumentation'] = 'Web services documentation';
+$string['encryptedpassword_set'] = '(Set and encrypted)';
+$string['encryptedpassword_edit'] = 'Enter new value';
$string['enrolinstancedefaults'] = 'Enrolment instance defaults';
$string['enrolinstancedefaults_desc'] = 'Default enrolment settings in new courses.';
$string['enrolmultipleusers'] = 'Enrol the users';
$string['analyticslogstore_help'] = 'The log store that will be used by the analytics API to read users\' activity.';
$string['analyticssettings'] = 'Analytics settings';
$string['analyticssiteinfo'] = 'Site information';
+$string['calclifetime'] = 'Keep analytics calculations for';
+$string['configlcalclifetime'] = 'This specifies the length of time you want to keep calculation data - this will not delete predictions, but deletes the data used to generate the predictions. Using the default option here is best as it keeps your disk usage under control, however if you are using calculation tables for other purposes you may want to increase this value.';
$string['defaulttimesplittingmethods'] = 'Default analysis intervals for model\'s evaluation';
$string['defaulttimesplittingmethods_help'] = 'The analysis interval defines when the system will calculate predictions and the portion of activity logs that will be considered for those predictions. The model evaluation process will iterate through these analysis intervals unless a specific analysis interval is specified.';
$string['defaultpredictionsprocessor'] = 'Default predictions processor';
$string['modeltimelimit'] = 'Analysis time limit per model';
$string['modeltimelimitinfo'] = 'This setting limits the time each model spends analysing the site contents.';
$string['neutral'] = 'Neutral';
+$string['neverdelete'] = 'Never delete calculations';
$string['noevaluationbasedassumptions'] = 'Models based on assumptions cannot be evaluated.';
$string['nodata'] = 'No data to analyse';
$string['noinsightsmodel'] = 'This model does not generate insights';
$string['duplicateroleshortname'] = 'There is already a role with this short name!';
$string['duplicateusername'] = 'Duplicate username - skipping record';
$string['emailfail'] = 'Emailing failed';
+$string['encryption_encryptfailed'] = 'Encryption failed';
+$string['encryption_decryptfailed'] = 'Decryption failed';
+$string['encryption_invalidkey'] = 'Invalid key';
+$string['encryption_keyalreadyexists'] = 'Key already exists';
+$string['encryption_nokey'] = 'Key not found';
+$string['encryption_wrongmethod'] = 'Data does not match a supported encryption method';
$string['enddatebeforestartdate'] = 'The course end date must be after the start date.';
$string['error'] = 'Error occurred';
$string['error_question_answers_missing_in_db'] = 'Failed to find an answer matching "{$a->answer}" in the question_answers database table. This occurred while restoring the question with id {$a->filequestionid} in the backup file, which has been matched to the existing question with id {$a->dbquestionid} in the database.';
}
}
+/**
+ * Admin setting class for encrypted values using secure encryption.
+ *
+ * @copyright 2019 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class admin_setting_encryptedpassword extends admin_setting {
+
+ /**
+ * Constructor. Same as parent except that the default value is always an empty string.
+ *
+ * @param string $name Internal name used in config table
+ * @param string $visiblename Name shown on form
+ * @param string $description Description that appears below field
+ */
+ public function __construct(string $name, string $visiblename, string $description) {
+ parent::__construct($name, $visiblename, $description, '');
+ }
+
+ public function get_setting() {
+ return $this->config_read($this->name);
+ }
+
+ public function write_setting($data) {
+ $data = trim($data);
+ if ($data === '') {
+ // Value can really be set to nothing.
+ $savedata = '';
+ } else {
+ // Encrypt value before saving it.
+ $savedata = \core\encryption::encrypt($data);
+ }
+ return ($this->config_write($this->name, $savedata) ? '' : get_string('errorsetting', 'admin'));
+ }
+
+ public function output_html($data, $query='') {
+ global $OUTPUT;
+
+ $default = $this->get_defaultsetting();
+ $context = (object) [
+ 'id' => $this->get_id(),
+ 'name' => $this->get_full_name(),
+ 'set' => $data !== '',
+ 'novalue' => $this->get_setting() === null
+ ];
+ $element = $OUTPUT->render_from_template('core_admin/setting_encryptedpassword', $context);
+
+ return format_admin_setting($this, $this->visiblename, $element, $this->description,
+ true, '', $default, $query);
+ }
+}
+
/**
* Empty setting used to allow flags (advanced) on settings that can have no sensible default.
* Note: Only advanced makes sense right now - locked does not.
* @copyright 2016 Damyon Wiese <damyon@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define(['jquery', 'core/modal_factory', 'core/templates', 'core/str', 'core/notification'],
- function($, ModalFactory, Templates, Str, Notification) {
+import ModalFactory from 'core/modal_factory';
+import Templates from 'core/templates';
+import {get_string as getString} from 'core/str';
+import Ajax from 'core/ajax';
- return /** @alias module:core/addblockmodal */ {
- /**
- * Global init function for this module.
- *
- * @method init
- * @param {Object} context The template context for rendering this modal body.
- */
- init: function(context) {
- var addblocklink = $('[data-key=addblock]');
+const SELECTORS = {
+ ADD_BLOCK: '[data-key="addblock"]'
+};
- // We need the fetch the names of the blocks. It was too much to send in the page.
- var titlerequests = context.blocks.map(function(blockName) {
- return {
- key: 'pluginname',
- component: 'block_' + blockName,
- };
- });
+let addBlockModal = null;
- var bodyPromise = Str.get_strings(titlerequests)
- .then(function(titles) {
- return titles.map(function(title, index) {
- return {
- name: context.blocks[index],
- title: title,
- };
- });
- })
- .then(function(blocks) {
- context.blocks = blocks;
- return Templates.render('core/add_block_body', context);
- })
- .fail(Notification.exception);
+/**
+ * Register related event listeners.
+ *
+ * @method registerListenerEvents
+ * @param {String} pageType The type of the page
+ * @param {String} pageLayout The layout of the page
+ * @param {String} addBlockUrl The add block URL
+ */
+const registerListenerEvents = (pageType, pageLayout, addBlockUrl) => {
+ document.addEventListener('click', e => {
+
+ if (e.target.closest(SELECTORS.ADD_BLOCK)) {
+ e.preventDefault();
- var titlePromise = Str.get_string('addblock')
- .fail(Notification.exception);
+ if (addBlockModal) { // The 'add block' modal has been already created.
+ // Display the 'add block' modal.
+ addBlockModal.show();
+ } else {
+ buildAddBlockModal()
+ .then(modal => {
+ addBlockModal = modal;
+ const modalBody = renderBlocks(addBlockUrl, pageType, pageLayout);
+ modal.setBody(modalBody);
+ modal.show();
- ModalFactory.create({
- title: titlePromise,
- body: bodyPromise,
- type: 'CANCEL',
- }, addblocklink);
+ return modalBody;
+ })
+ .catch(() => {
+ addBlockModal.destroy();
+ // Unset the addBlockModal in case this is a transient error and it goes away on a relaunch.
+ addBlockModal = null;
+ });
+ }
}
+ });
+};
+
+/**
+ * Method that creates the 'add block' modal.
+ *
+ * @method buildAddBlockModal
+ * @return {Promise} The modal promise (modal's body will be rendered later).
+ */
+const buildAddBlockModal = () => {
+ return ModalFactory.create({
+ type: ModalFactory.types.CANCEL,
+ title: getString('addblock')
+ });
+};
+
+/**
+ * Method that renders the list of available blocks.
+ *
+ * @method renderBlocks
+ * @param {String} addBlockUrl The add block URL
+ * @param {String} pageType The type of the page
+ * @param {String} pageLayout The layout of the page
+ * @return {Promise}
+ */
+const renderBlocks = async(addBlockUrl, pageType, pageLayout) => {
+ // Fetch all addable blocks in the given page.
+ const blocks = await getAddableBlocks(pageType, pageLayout);
+
+ return Templates.render('core/add_block_body', {
+ blocks: blocks,
+ url: addBlockUrl
+ });
+};
+
+/**
+ * Method that fetches all addable blocks in a given page.
+ *
+ * @method getAddableBlocks
+ * @param {String} pageType The type of the page
+ * @param {String} pageLayout The layout of the page
+ * @return {Promise}
+ */
+const getAddableBlocks = async(pageType, pageLayout) => {
+ const request = {
+ methodname: 'core_block_fetch_addable_blocks',
+ args: {
+ pagecontextid: M.cfg.contextid,
+ pagetype: pageType,
+ pagelayout: pageLayout
+ },
};
-});
+
+ return Ajax.call([request])[0];
+};
+
+/**
+ * Set up the actions.
+ *
+ * @method init
+ * @param {String} pageType The type of the page
+ * @param {String} pageLayout The layout of the page
+ * @param {String} addBlockUrl The add block URL
+ */
+export const init = (pageType, pageLayout, addBlockUrl) => {
+ registerListenerEvents(pageType, pageLayout, addBlockUrl);
+};
namespace core\antivirus;
defined('MOODLE_INTERNAL') || die();
-require_once(__DIR__ . '../../../../iplookup/lib.php');
+require_once($CFG->dirroot . '/iplookup/lib.php');
/**
* Base abstract antivirus scanner class.
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class used to encrypt or decrypt data.
+ *
+ * @package core
+ * @copyright 2020 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core;
+
+/**
+ * Class used to encrypt or decrypt data.
+ *
+ * @package core
+ * @copyright 2020 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class encryption {
+ /** @var string Encryption method: Sodium */
+ const METHOD_SODIUM = 'sodium';
+ /** @var string Encryption method: hand-coded OpenSSL (less safe) */
+ const METHOD_OPENSSL = 'openssl-aes-256-ctr';
+
+ /** @var string OpenSSL cipher method */
+ const OPENSSL_CIPHER = 'AES-256-CTR';
+
+ /**
+ * Checks if Sodium is installed.
+ *
+ * @return bool True if the Sodium extension is available
+ */
+ public static function is_sodium_installed(): bool {
+ return extension_loaded('sodium');
+ }
+
+ /**
+ * Gets the encryption method to use. We use the Sodium extension if it is installed, or
+ * otherwise, OpenSSL.
+ *
+ * @return string Current encryption method
+ */
+ protected static function get_encryption_method(): string {
+ if (self::is_sodium_installed()) {
+ return self::METHOD_SODIUM;
+ } else {
+ return self::METHOD_OPENSSL;
+ }
+ }
+
+ /**
+ * Creates a key for the server.
+ *
+ * @param string|null $method Encryption method (only if you want to create a non-default key)
+ * @param bool $chmod If true, restricts the file access of the key
+ * @throws \moodle_exception If the server already has a key, or there is an error
+ */
+ public static function create_key(?string $method = null, bool $chmod = true): void {
+ if ($method === null) {
+ $method = self::get_encryption_method();
+ }
+
+ if (self::key_exists($method)) {
+ throw new \moodle_exception('encryption_keyalreadyexists', 'error');
+ }
+
+ // Don't make it read-only in Behat or it will fail to clear for future runs.
+ if (defined('BEHAT_SITE_RUNNING')) {
+ $chmod = false;
+ }
+
+ // Generate the key.
+ switch ($method) {
+ case self::METHOD_SODIUM:
+ $key = sodium_crypto_secretbox_keygen();
+ break;
+ case self::METHOD_OPENSSL:
+ $key = openssl_random_pseudo_bytes(32);
+ break;
+ default:
+ throw new \coding_exception('Unknown method: ' . $method);
+ }
+
+ // Store the key, making it readable only by server.
+ $folder = self::get_key_folder();
+ check_dir_exists($folder);
+ $keyfile = self::get_key_file($method);
+ file_put_contents($keyfile, $key);
+ if ($chmod) {
+ chmod($keyfile, 0400);
+ }
+ }
+
+ /**
+ * Gets the folder used to store the secret key.
+ *
+ * @return string Folder path
+ */
+ protected static function get_key_folder(): string {
+ global $CFG;
+ return ($CFG->secretdataroot ?? $CFG->dataroot . '/secret') . '/key';
+ }
+
+ /**
+ * Gets the file path used to store the secret key. The filename contains the cipher method,
+ * so that if necessary to transition in future it would be possible to have multiple.
+ *
+ * @param string|null $method Encryption method (only if you want to get a non-default key)
+ * @return string Full path to file
+ */
+ public static function get_key_file(?string $method = null): string {
+ if ($method === null) {
+ $method = self::get_encryption_method();
+ }
+
+ return self::get_key_folder() . '/' . $method . '.key';
+ }
+
+ /**
+ * Checks if there is a key file.
+ *
+ * @param string|null $method Encryption method (only if you want to check a non-default key)
+ * @return bool True if there is a key file
+ */
+ public static function key_exists(?string $method = null): bool {
+ if ($method === null) {
+ $method = self::get_encryption_method();
+ }
+
+ return file_exists(self::get_key_file($method));
+ }
+
+ /**
+ * Gets the current key, automatically creating it if there isn't one yet.
+ *
+ * @param string|null $method Encryption method (only if you want to get a non-default key)
+ * @return string The key (binary)
+ * @throws \moodle_exception If there isn't one already (and creation is disabled)
+ */
+ protected static function get_key(?string $method = null): string {
+ global $CFG;
+
+ if ($method === null) {
+ $method = self::get_encryption_method();
+ }
+
+ $keyfile = self::get_key_file($method);
+ if (!file_exists($keyfile) && empty($CFG->nokeygeneration)) {
+ self::create_key($method);
+ }
+ $result = @file_get_contents($keyfile);
+ if ($result === false) {
+ throw new \moodle_exception('encryption_nokey', 'error');
+ }
+ return $result;
+ }
+
+ /**
+ * Gets the length in bytes of the initial values data required.
+ *
+ * @param string $method Crypto method
+ * @return int Length in bytes
+ */
+ protected static function get_iv_length(string $method): int {
+ switch ($method) {
+ case self::METHOD_SODIUM:
+ return SODIUM_CRYPTO_SECRETBOX_NONCEBYTES;
+ case self::METHOD_OPENSSL:
+ return openssl_cipher_iv_length(self::OPENSSL_CIPHER);
+ default:
+ throw new \coding_exception('Unknown method: ' . $method);
+ }
+ }
+
+ /**
+ * Encrypts data using the server's key.
+ *
+ * Note there is a special case - the empty string is not encrypted.
+ *
+ * @param string $data Data to encrypt, or empty string for no data
+ * @param string|null $method Encryption method (only if you want to use a non-default method)
+ * @return string Encrypted data, or empty string for no data
+ * @throws \moodle_exception If the key doesn't exist, or the string is too long
+ */
+ public static function encrypt(string $data, ?string $method = null): string {
+ if ($data === '') {
+ return '';
+ } else {
+ if ($method === null) {
+ $method = self::get_encryption_method();
+ }
+
+ // Create IV.
+ $iv = random_bytes(self::get_iv_length($method));
+
+ // Encrypt data.
+ switch($method) {
+ case self::METHOD_SODIUM:
+ try {
+ $encrypted = sodium_crypto_secretbox($data, $iv, self::get_key($method));
+ } catch (\SodiumException $e) {
+ throw new \moodle_exception('encryption_encryptfailed', 'error', '', null, $e->getMessage());
+ }
+ break;
+
+ case self::METHOD_OPENSSL:
+ // This may not be a secure authenticated encryption implementation;
+ // administrators should enable the Sodium extension.
+ $key = self::get_key($method);
+ if (strlen($key) !== 32) {
+ throw new \moodle_exception('encryption_invalidkey', 'error');
+ }
+ $encrypted = @openssl_encrypt($data, self::OPENSSL_CIPHER, $key, OPENSSL_RAW_DATA, $iv);
+ if ($encrypted === false) {
+ throw new \moodle_exception('encryption_encryptfailed', 'error',
+ '', null, openssl_error_string());
+ }
+ $hmac = hash_hmac('sha256', $iv . $encrypted, $key, true);
+ $encrypted .= $hmac;
+ break;
+
+ default:
+ throw new \coding_exception('Unknown method: ' . $method);
+ }
+
+ // Encrypted data is cipher method plus IV plus encrypted data.
+ return $method . ':' . base64_encode($iv . $encrypted);
+ }
+ }
+
+ /**
+ * Decrypts data using the server's key. The decryption works with either supported method.
+ *
+ * @param string $data Data to decrypt
+ * @return string Decrypted data
+ */
+ public static function decrypt(string $data): string {
+ if ($data === '') {
+ return '';
+ } else {
+ if (preg_match('~^(' . self::METHOD_OPENSSL . '|' . self::METHOD_SODIUM . '):~', $data, $matches)) {
+ $method = $matches[1];
+ } else {
+ throw new \moodle_exception('encryption_wrongmethod', 'error');
+ }
+ $realdata = base64_decode(substr($data, strlen($method) + 1), true);
+ if ($realdata === false) {
+ throw new \moodle_exception('encryption_decryptfailed', 'error',
+ '', null, 'Invalid base64 data');
+ }
+
+ $ivlength = self::get_iv_length($method);
+ if (strlen($realdata) < $ivlength + 1) {
+ throw new \moodle_exception('encryption_decryptfailed', 'error',
+ '', null, 'Insufficient data');
+ }
+ $iv = substr($realdata, 0, $ivlength);
+ $encrypted = substr($realdata, $ivlength);
+
+ switch ($method) {
+ case self::METHOD_SODIUM:
+ try {
+ $decrypted = sodium_crypto_secretbox_open($encrypted, $iv, self::get_key($method));
+ } catch (\SodiumException $e) {
+ throw new \moodle_exception('encryption_decryptfailed', 'error',
+ '', null, $e->getMessage());
+ }
+ // Sodium returns false if decryption fails because data is invalid.
+ if ($decrypted === false) {
+ throw new \moodle_exception('encryption_decryptfailed', 'error',
+ '', null, 'Integrity check failed');
+ }
+ break;
+
+ case self::METHOD_OPENSSL:
+ if (strlen($encrypted) < 33) {
+ throw new \moodle_exception('encryption_decryptfailed', 'error',
+ '', null, 'Insufficient data');
+ }
+ $hmac = substr($encrypted, -32);
+ $encrypted = substr($encrypted, 0, -32);
+ $key = self::get_key($method);
+ $expectedhmac = hash_hmac('sha256', $iv . $encrypted, $key, true);
+ if ($hmac !== $expectedhmac) {
+ throw new \moodle_exception('encryption_decryptfailed', 'error',
+ '', null, 'Integrity check failed');
+ }
+
+ $decrypted = @openssl_decrypt($encrypted, self::OPENSSL_CIPHER, $key, OPENSSL_RAW_DATA, $iv);
+ if ($decrypted === false) {
+ throw new \moodle_exception('encryption_decryptfailed', 'error',
+ '', null, openssl_error_string());
+ }
+ break;
+
+ default:
+ throw new \coding_exception('Unknown method: ' . $method);
+ }
+
+ return $decrypted;
+ }
+ }
+}
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
),
+ 'core_block_fetch_addable_blocks' => array(
+ 'classname' => 'core_block\external\fetch_addable_blocks',
+ 'methodname' => 'execute',
+ 'description' => 'Returns all addable blocks in a given page.',
+ 'type' => 'read',
+ 'capabilities' => 'moodle/site:manageblocks',
+ 'ajax' => true,
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+ ),
+
// Filters functions.
'core_filters_get_available_in_context' => array(
'classname' => 'core_filters\external',
}
/**
- * Returns list of courses, for whole site, or category
- *
- * Similar to get_courses, but allows paging
- * Important: Using c.* for fields is extremely expensive because
- * we are using distinct. You almost _NEVER_ need all the fields
- * in such a large SELECT
- *
* @deprecated since Moodle 3.7
- * @todo The final deprecation of this function will take place in Moodle 3.11 - see MDL-65319.
- *
- * @param string|int $categoryid Either a category id or 'all' for everything
- * @param string $sort A field and direction to sort by
- * @param string $fields The additional fields to return
- * @param int $totalcount Reference for the number of courses
- * @param string $limitfrom The course to start from
- * @param string $limitnum The number of courses to limit to
- * @return array Array of courses
- */
-function get_courses_page($categoryid="all", $sort="c.sortorder ASC", $fields="c.*",
- &$totalcount, $limitfrom="", $limitnum="") {
- debugging('Function get_courses_page() is deprecated. Please use core_course_category::get_courses() ' .
- 'or core_course_category::search_courses()', DEBUG_DEVELOPER);
- global $USER, $CFG, $DB;
-
- $params = array();
-
- $categoryselect = "";
- if ($categoryid !== "all" && is_numeric($categoryid)) {
- $categoryselect = "WHERE c.category = :catid";
- $params['catid'] = $categoryid;
- } else {
- $categoryselect = "";
- }
-
- $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
- $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
- $params['contextlevel'] = CONTEXT_COURSE;
-
- $totalcount = 0;
- if (!$limitfrom) {
- $limitfrom = 0;
- }
- $visiblecourses = array();
-
- $sql = "SELECT $fields $ccselect
- FROM {course} c
- $ccjoin
- $categoryselect
- ORDER BY $sort";
-
- // Pull out all course matching the cat.
- $rs = $DB->get_recordset_sql($sql, $params);
- // Iteration will have to be done inside loop to keep track of the limitfrom and limitnum.
- foreach ($rs as $course) {
- context_helper::preload_from_record($course);
- if (core_course_category::can_view_course_info($course)) {
- $totalcount++;
- if ($totalcount > $limitfrom && (!$limitnum or count($visiblecourses) < $limitnum)) {
- $visiblecourses [$course->id] = $course;
- }
- }
- }
- $rs->close();
- return $visiblecourses;
+ */
+function get_courses_page() {
+ throw new coding_exception(
+ 'Function get_courses_page() has been removed. Please use core_course_category::get_courses() ' .
+ 'or core_course_category::search_courses()'
+ );
}
/**
@javascript
Scenario: Check disable Atto editor.
When I set the field "mycontrol" to "Disable"
- Then the "disabled" attribute of "button.atto_collapse_button" "css_element" should contain "disabled"
- And the "disabled" attribute of "button.atto_title_button" "css_element" should contain "disabled"
- And the "disabled" attribute of "button.atto_bold_button" "css_element" should contain "disabled"
- And the "disabled" attribute of "button.atto_italic_button" "css_element" should contain "disabled"
- And the "disabled" attribute of "button.atto_unorderedlist_button_insertUnorderedList" "css_element" should contain "disabled"
- And the "disabled" attribute of "button.atto_orderedlist_button_insertOrderedList" "css_element" should contain "disabled"
- And the "disabled" attribute of "button.atto_link_button" "css_element" should contain "disabled"
- And the "disabled" attribute of "button.atto_link_button_unlink" "css_element" should contain "disabled"
- And the "disabled" attribute of "button.atto_image_button" "css_element" should contain "disabled"
+ Then the "disabled" attribute of "button.atto_collapse_button" "css_element" should be set
+ And the "disabled" attribute of "button.atto_title_button" "css_element" should be set
+ And the "disabled" attribute of "button.atto_bold_button" "css_element" should be set
+ And the "disabled" attribute of "button.atto_italic_button" "css_element" should be set
+ And the "disabled" attribute of "button.atto_unorderedlist_button_insertUnorderedList" "css_element" should be set
+ And the "disabled" attribute of "button.atto_orderedlist_button_insertOrderedList" "css_element" should be set
+ And the "disabled" attribute of "button.atto_link_button" "css_element" should be set
+ And the "disabled" attribute of "button.atto_link_button_unlink" "css_element" should be set
+ And the "disabled" attribute of "button.atto_image_button" "css_element" should be set
And the "contenteditable" attribute of "div#id_myeditoreditable" "css_element" should contain "false"
@javascript
Scenario: Check enable Atto editor.
When I set the field "mycontrol" to "Enable"
- Then "button.atto_collapse_button[disabled]" "css_element" should not exist
- And "button.atto_title_button[disabled]" "css_element" should not exist
- And "button.atto_bold_button[disabled]" "css_element" should not exist
- And "button.atto_italic_button[disabled]" "css_element" should not exist
- And "button.atto_unorderedlist_button_insertUnorderedList[disabled]" "css_element" should not exist
- And "button.atto_orderedlist_button_insertOrderedList[disabled]" "css_element" should not exist
- And "button.atto_link_button[disabled]" "css_element" should not exist
- And "button.atto_link_button_unlink[disabled]" "css_element" should not exist
- And "button.atto_image_button[disabled]" "css_element" should not exist
+ Then the "disabled" attribute of "button.atto_collapse_button" "css_element" should not be set
+ And the "disabled" attribute of "button.atto_title_button" "css_element" should not be set
+ And the "disabled" attribute of "button.atto_bold_button" "css_element" should not be set
+ And the "disabled" attribute of "button.atto_italic_button" "css_element" should not be set
+ And the "disabled" attribute of "button.atto_unorderedlist_button_insertUnorderedList" "css_element" should not be set
+ And the "disabled" attribute of "button.atto_orderedlist_button_insertOrderedList" "css_element" should not be set
+ And the "disabled" attribute of "button.atto_link_button" "css_element" should not be set
+ And the "disabled" attribute of "button.atto_link_button_unlink" "css_element" should not be set
+ And the "disabled" attribute of "button.atto_image_button" "css_element" should not be set
And the "contenteditable" attribute of "div#id_myeditoreditable" "css_element" should contain "true"
@javascript
Scenario: Check disable Text area editor.
When I set the field "mycontrol" to "Disable"
- Then the "readonly" attribute of "textarea#id_myeditor" "css_element" should contain "readonly"
+ Then the "readonly" attribute of "textarea#id_myeditor" "css_element" should be set
@javascript
Scenario: Check enable Text area editor.
When I set the field "mycontrol" to "Enable"
- Then "textarea#id_myeditor[readonly]" "css_element" should not exist
+ Then the "readonly" attribute of "textarea#id_myeditor" "css_element" should not be set
--- /dev/null
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Encrypted password functionality.
+ *
+ * @module core_form/encryptedpassword
+ * @package core_form
+ * @class encryptedpassword
+ * @copyright 2019 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Constructor for EncryptedPassword.
+ *
+ * @param {String} elementId The element to apply the encrypted password JS to
+ */
+export const EncryptedPassword = function(elementId) {
+ const wrapper = document.querySelector('div[data-encryptedpasswordid="' + elementId + '"]');
+ this.spanOrLink = wrapper.querySelector('span, a');
+ this.input = wrapper.querySelector('input');
+ this.editButtonOrLink = wrapper.querySelector('button[data-editbutton], a');
+ this.cancelButton = wrapper.querySelector('button[data-cancelbutton]');
+
+ // Edit button action.
+ var editHandler = (e) => {
+ e.stopImmediatePropagation();
+ e.preventDefault();
+ this.startEditing(true);
+ };
+ this.editButtonOrLink.addEventListener('click', editHandler);
+
+ // When it's a link, do some magic to make the label work as well.
+ if (this.editButtonOrLink.nodeName === 'A') {
+ wrapper.parentElement.previousElementSibling.querySelector('label').addEventListener('click', editHandler);
+ }
+
+ // Cancel button action.
+ this.cancelButton.addEventListener('click', (e) => {
+ e.stopImmediatePropagation();
+ e.preventDefault();
+ this.cancelEditing();
+ });
+
+ // If the value is not set yet, start editing and remove the cancel option - so that
+ // it saves something in the config table and doesn't keep repeat showing it as a new
+ // admin setting...
+ if (wrapper.dataset.novalue === 'y') {
+ this.startEditing(false);
+ this.cancelButton.style.display = 'none';
+ }
+};
+
+/**
+ * Starts editing.
+ *
+ * @param {Boolean} moveFocus If true, sets focus to the edit box
+ */
+EncryptedPassword.prototype.startEditing = function(moveFocus) {
+ this.input.style.display = 'inline';
+ this.input.disabled = false;
+ this.spanOrLink.style.display = 'none';
+ this.editButtonOrLink.style.display = 'none';
+ this.cancelButton.style.display = 'inline';
+
+ // Move the id around, which changes what happens when you click the label.
+ const id = this.editButtonOrLink.id;
+ this.editButtonOrLink.removeAttribute('id');
+ this.input.id = id;
+
+ if (moveFocus) {
+ this.input.focus();
+ }
+};
+
+/**
+ * Cancels editing.
+ */
+EncryptedPassword.prototype.cancelEditing = function() {
+ this.input.style.display = 'none';
+ this.input.value = '';
+ this.input.disabled = true;
+ this.spanOrLink.style.display = 'inline';
+ this.editButtonOrLink.style.display = 'inline';
+ this.cancelButton.style.display = 'none';
+
+ // Move the id around, which changes what happens when you click the label.
+ const id = this.input.id;
+ this.input.removeAttribute('id');
+ this.editButtonOrLink.id = id;
+};
}
if ($user->icq && !isset($hiddenfields['icqnumber'])) {
- $imurl = new moodle_url('http://web.icq.com/wwp', array('uin' => $user->icq) );
- $iconurl = new moodle_url('http://web.icq.com/whitepages/online', array('icq' => $user->icq, 'img' => '5'));
+ $imurl = new moodle_url('https://web.icq.com/wwp', array('uin' => $user->icq) );
+ $iconurl = new moodle_url('https://web.icq.com/whitepages/online', array('icq' => $user->icq, 'img' => '5'));
$statusicon = html_writer::tag('img', '',
array('src' => $iconurl, 'class' => 'icon icon-post', 'alt' => get_string('status')));
$node = new core_user\output\myprofile\node('contact', 'icqnumber', get_string('icqnumber'), null, null,
$tree->add_node($node);
}
if ($user->yahoo && !isset($hiddenfields['yahooid'])) {
- $imurl = new moodle_url('http://edit.yahoo.com/config/send_webmesg', array('.target' => $user->yahoo, '.src' => 'pg'));
+ $imurl = new moodle_url('https://edit.yahoo.com/config/send_webmesg', array('.target' => $user->yahoo, '.src' => 'pg'));
$iconurl = new moodle_url('http://opi.yahoo.com/online', array('u' => $user->yahoo, 'm' => 'g', 't' => '0'));
$statusicon = html_writer::tag('img', '',
array('src' => $iconurl, 'class' => 'iconsmall icon-post', 'alt' => get_string('status')));
// Add-a-block in editing mode.
if (isset($this->page->theme->addblockposition) &&
$this->page->theme->addblockposition == BLOCK_ADDBLOCK_POSITION_FLATNAV &&
- $PAGE->user_is_editing() && $PAGE->user_can_edit_blocks() &&
- ($addable = $PAGE->blocks->get_addable_blocks())) {
+ $PAGE->user_is_editing() && $PAGE->user_can_edit_blocks()) {
$url = new moodle_url($PAGE->url, ['bui_addblock' => '', 'sesskey' => sesskey()]);
$addablock = navigation_node::create(get_string('addblock'), $url);
$flat = new flat_navigation_node($addablock, 0);
$flat->key = 'addblock';
$flat->icon = new pix_icon('i/addblock', '');
$this->add($flat);
- $blocks = [];
- foreach ($addable as $block) {
- $blocks[] = $block->name;
- }
- $params = array('blocks' => $blocks, 'url' => '?' . $url->get_query_string(false));
- $PAGE->requires->js_call_amd('core/addblockmodal', 'init', array($params));
+
+ $addblockurl = "?{$url->get_query_string(false)}";
+
+ $PAGE->requires->js_call_amd('core/addblockmodal', 'init',
+ [$PAGE->pagetype, $PAGE->pagelayout, $addblockurl]);
}
}
{{#blocks}}
<a href="{{url}}&bui_addblock={{name}}" class="list-group-item list-group-item-action">{{title}}</a>
{{/blocks}}
+{{^blocks}}
+ <div class="alert alert-primary" role="alert">
+ {{#str}} noblockstoaddhere {{/str}}
+ </div>
+{{/blocks}}
</div>
<ul class="nav nav-tabs" role="tablist">
<!-- First the top most node and immediate children -->
<li class="nav-item">
- <a class="nav-link active" href="#link{{node.key}}" data-toggle="tab" role="tab">{{node.text}}</a>
+ <a class="nav-link active" href="#link{{node.key}}" data-toggle="tab" role="tab" aria-selected="true">{{node.text}}</a>
</li>
<!-- Now the first level children with sub nodes -->
{{#node.children}}
{{#display}}
{{^is_short_branch}}
<li class="nav-item">
- <a class="nav-link" href="#link{{key}}" data-toggle="tab" role="tab">{{text}}</a>
+ <a class="nav-link" href="#link{{key}}" data-toggle="tab" role="tab" aria-selected="false" tabindex="-1">{{text}}</a>
</li>
{{/is_short_branch}}
{{/display}}
* @param string $selectortype The type of element where we are looking in.
*/
public function the_element_should_be_disabled($element, $selectortype) {
-
- // Transforming from steps definitions selector/locator format to Mink format and getting the NodeElement.
- $node = $this->get_selected_node($selectortype, $element);
-
- if (!$node->hasAttribute('disabled')) {
- throw new ExpectationException('The element "' . $element . '" is not disabled', $this->getSession());
- }
+ $this->the_attribute_of_should_be_set("disabled", $element, $selectortype, false);
}
/**
* @param string $selectortype The type of where we look
*/
public function the_element_should_be_enabled($element, $selectortype) {
-
- // Transforming from steps definitions selector/locator format to mink format and getting the NodeElement.
- $node = $this->get_selected_node($selectortype, $element);
-
- if ($node->hasAttribute('disabled')) {
- throw new ExpectationException('The element "' . $element . '" is not enabled', $this->getSession());
- }
+ $this->the_attribute_of_should_be_set("disabled", $element, $selectortype, true);
}
/**
* @param string $selectortype The type of element where we are looking in.
*/
public function the_element_should_be_readonly($element, $selectortype) {
- // Transforming from steps definitions selector/locator format to Mink format and getting the NodeElement.
- $node = $this->get_selected_node($selectortype, $element);
-
- if (!$node->hasAttribute('readonly')) {
- throw new ExpectationException('The element "' . $element . '" is not readonly', $this->getSession());
- }
+ $this->the_attribute_of_should_be_set("readonly", $element, $selectortype, false);
}
/**
* @param string $selectortype The type of element where we are looking in.
*/
public function the_element_should_not_be_readonly($element, $selectortype) {
- // Transforming from steps definitions selector/locator format to Mink format and getting the NodeElement.
- $node = $this->get_selected_node($selectortype, $element);
-
- if ($node->hasAttribute('readonly')) {
- throw new ExpectationException('The element "' . $element . '" is readonly', $this->getSession());
- }
+ $this->the_attribute_of_should_be_set("readonly", $element, $selectortype, true);
}
/**
$this->resize_window($windowsize, $windowviewport === 'viewport');
}
+ /**
+ * Checks whether there the specified attribute is set or not.
+ *
+ * @Then the :attribute attribute of :element :selectortype should be set
+ * @Then the :attribute attribute of :element :selectortype should :not be set
+ *
+ * @throws ExpectationException
+ * @param string $attribute Name of attribute
+ * @param string $element The locator of the specified selector
+ * @param string $selectortype The selector type
+ * @param string $not
+ */
+ public function the_attribute_of_should_be_set($attribute, $element, $selectortype, $not = null) {
+ // Get the container node (exception if it doesn't exist).
+ $containernode = $this->get_selected_node($selectortype, $element);
+ $hasattribute = $containernode->hasAttribute($attribute);
+
+ if ($not && $hasattribute) {
+ $value = $containernode->getAttribute($attribute);
+ // Should not be set but is.
+ throw new ExpectationException(
+ "The attribute \"{$attribute}\" should not be set but has a value of '{$value}'",
+ $this->getSession()
+ );
+ } else if (!$not && !$hasattribute) {
+ // Should be set but is not.
+ throw new ExpectationException(
+ "The attribute \"{$attribute}\" should be set but is not",
+ $this->getSession()
+ );
+ }
+ }
+
/**
* Checks whether there is an attribute on the given element that contains the specified text.
*
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Test encryption.
+ *
+ * @package core
+ * @copyright 2020 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core;
+
+/**
+ * Test encryption.
+ *
+ * @package core
+ * @copyright 2020 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class encryption_testcase extends \basic_testcase {
+
+ /**
+ * Clear junk created by tests.
+ */
+ protected function tearDown(): void {
+ global $CFG;
+ $keyfile = encryption::get_key_file(encryption::METHOD_OPENSSL);
+ if (file_exists($keyfile)) {
+ chmod($keyfile, 0700);
+ }
+ $keyfile = encryption::get_key_file(encryption::METHOD_SODIUM);
+ if (file_exists($keyfile)) {
+ chmod($keyfile, 0700);
+ }
+ remove_dir($CFG->dataroot . '/secret');
+ unset($CFG->nokeygeneration);
+ }
+
+ protected function setUp(): void {
+ $this->tearDown();
+
+ require_once(__DIR__ . '/fixtures/testable_encryption.php');
+ }
+
+ /**
+ * Tests using Sodium need to check the extension is available.
+ *
+ * @param string $method Encryption method
+ */
+ protected function require_sodium(string $method) {
+ if ($method == encryption::METHOD_SODIUM) {
+ if (!encryption::is_sodium_installed()) {
+ $this->markTestSkipped('Sodium not installed');
+ }
+ }
+ }
+
+ /**
+ * Many of the tests work with both encryption methods.
+ *
+ * @return array[] Array of method options for test
+ */
+ public function encryption_method_provider(): array {
+ return ['Sodium' => [encryption::METHOD_SODIUM], 'OpenSSL' => [encryption::METHOD_OPENSSL]];
+ }
+
+ /**
+ * Tests the create_keys and get_key functions.
+ *
+ * @param string $method Encryption method
+ * @dataProvider encryption_method_provider
+ */
+ public function test_create_key(string $method): void {
+ $this->require_sodium($method);
+ encryption::create_key($method);
+ $key = testable_encryption::get_key($method);
+
+ // Conveniently, both encryption methods have the same key length.
+ $this->assertEquals(32, strlen($key));
+
+ $this->expectExceptionMessage('Key already exists');
+ encryption::create_key($method);
+ }
+
+ /**
+ * Tests encryption and decryption with empty strings.
+ *
+ * @throws \moodle_exception
+ */
+ public function test_encrypt_and_decrypt_empty(): void {
+ $this->assertEquals('', encryption::encrypt(''));
+ $this->assertEquals('', encryption::decrypt(''));
+ }
+
+ /**
+ * Tests encryption when the keys weren't created yet.
+ *
+ * @param string $method Encryption method
+ * @dataProvider encryption_method_provider
+ */
+ public function test_encrypt_nokeys(string $method): void {
+ global $CFG;
+ $this->require_sodium($method);
+
+ // Prevent automatic generation of keys.
+ $CFG->nokeygeneration = true;
+ $this->expectExceptionMessage('Key not found');
+ encryption::encrypt('frogs', $method);
+ }
+
+ /**
+ * Tests decryption when the data has a different encryption method
+ */
+ public function test_decrypt_wrongmethod(): void {
+ $this->expectExceptionMessage('Data does not match a supported encryption method');
+ encryption::decrypt('FAKE-CIPHER-METHOD:xx');
+ }
+
+ /**
+ * Tests decryption when not enough data is supplied to get the IV and some data.
+ *
+ * @dataProvider encryption_method_provider
+ * @param string $method Encryption method
+ */
+ public function test_decrypt_tooshort(string $method): void {
+ $this->require_sodium($method);
+
+ $this->expectExceptionMessage('Insufficient data');
+ switch ($method) {
+ case encryption::METHOD_OPENSSL:
+ // It needs min 49 bytes (16 bytes IV + 32 bytes HMAC + 1 byte data).
+ $justtooshort = '0123456789abcdef0123456789abcdef0123456789abcdef';
+ break;
+ case encryption::METHOD_SODIUM:
+ // Sodium needs 25 bytes at least as far as our code is concerned (24 bytes IV + 1
+ // byte data); it splits out any authentication hashes itself.
+ $justtooshort = '0123456789abcdef01234567';
+ break;
+ }
+
+ encryption::decrypt($method . ':' .base64_encode($justtooshort));
+ }
+
+ /**
+ * Tests decryption when data is not valid base64.
+ *
+ * @dataProvider encryption_method_provider
+ * @param string $method Encryption method
+ */
+ public function test_decrypt_notbase64(string $method): void {
+ $this->require_sodium($method);
+
+ $this->expectExceptionMessage('Invalid base64 data');
+ encryption::decrypt($method . ':' . chr(160));
+ }
+
+ /**
+ * Tests decryption when the keys weren't created yet.
+ *
+ * @dataProvider encryption_method_provider
+ * @param string $method Encryption method
+ */
+ public function test_decrypt_nokeys(string $method): void {
+ global $CFG;
+ $this->require_sodium($method);
+
+ // Prevent automatic generation of keys.
+ $CFG->nokeygeneration = true;
+ $this->expectExceptionMessage('Key not found');
+ encryption::decrypt($method . ':' . base64_encode(
+ '0123456789abcdef0123456789abcdef0123456789abcdef0'));
+ }
+
+ /**
+ * Test automatic generation of keys when needed.
+ *
+ * @dataProvider encryption_method_provider
+ * @param string $method Encryption method
+ */
+ public function test_auto_key_generation(string $method): void {
+ $this->require_sodium($method);
+
+ // Allow automatic generation (default).
+ $encrypted = encryption::encrypt('frogs', $method);
+ $this->assertEquals('frogs', encryption::decrypt($encrypted));
+ }
+
+ /**
+ * Checks that invalid key causes failures.
+ *
+ * @dataProvider encryption_method_provider
+ * @param string $method Encryption method
+ */
+ public function test_invalid_key(string $method): void {
+ global $CFG;
+ $this->require_sodium($method);
+
+ // Set the key to something bogus.
+ $folder = $CFG->dataroot . '/secret/key';
+ check_dir_exists($folder);
+ file_put_contents(encryption::get_key_file($method), 'silly');
+
+ switch ($method) {
+ case encryption::METHOD_SODIUM:
+ $this->expectExceptionMessage('key size should be');
+ break;
+
+ case encryption::METHOD_OPENSSL:
+ $this->expectExceptionMessage('Invalid key');
+ break;
+ }
+ encryption::encrypt('frogs', $method);
+ }
+
+ /**
+ * Checks that modified data causes failures.
+ *
+ * @dataProvider encryption_method_provider
+ * @param string $method Encryption method
+ */
+ public function test_modified_data(string $method): void {
+ $this->require_sodium($method);
+
+ $encrypted = encryption::encrypt('frogs', $method);
+ $mainbit = base64_decode(substr($encrypted, strlen($method) + 1));
+ $mainbit = substr($mainbit, 0, 16) . 'X' . substr($mainbit, 16);
+ $encrypted = $method . ':' . base64_encode($mainbit);
+ $this->expectExceptionMessage('Integrity check failed');
+ encryption::decrypt($encrypted);
+ }
+
+ /**
+ * Tests encryption and decryption for real.
+ *
+ * @dataProvider encryption_method_provider
+ * @param string $method Encryption method
+ * @throws \moodle_exception
+ */
+ public function test_encrypt_and_decrypt_realdata(string $method): void {
+ $this->require_sodium($method);
+
+ // Encrypt short string.
+ $encrypted = encryption::encrypt('frogs', $method);
+ $this->assertNotEquals('frogs', $encrypted);
+ $this->assertEquals('frogs', encryption::decrypt($encrypted));
+
+ // Encrypt really long string (1 MB).
+ $long = str_repeat('X', 1024 * 1024);
+ $this->assertEquals($long, encryption::decrypt(encryption::encrypt($long, $method)));
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+namespace core;
+
+/**
+ * Testable version of the encryption class - just makes it possible to unit-test protected
+ * function.
+ *
+ * @package core
+ * @copyright 2020 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class testable_encryption extends encryption {
+ public static function get_key(?string $method = null): string {
+ return parent::get_key($method);
+ }
+}
* New optional parameter $extracontent for print_collapsible_region_start(). This allows developers to add interactive HTML elements
(e.g. a help icon) after the collapsible region's toggle link.
* Final deprecation i_dock_block() in behat_deprecated.php
+* Final deprecation of get_courses_page. Function has been removed and core_course_category::get_courses() should be
+ used instead.
+* New encryption API in \core\encryption allows secure encryption and decryption of data. By
+ default the key is stored in moodledata but admins can configure a different, more secure
+ location in config.php if required. To get the best possible security for this feature, we
+ recommend enabling the Sodium PHP extension.
* Behat timeout constants behat_base::TIMEOUT, EXTENDED_TIMEOUT, and REDUCED_TIMEOUT, which were deprecated in 3.7, have been removed.
=== 3.10 ===
// The last known message time is earlier than the one being requested so we can
// just return an empty result set rather than having to query the DB.
if ($lastcreated && $lastcreated < $timefrom) {
- return [];
+ return helper::format_conversation_messages($userid, $convid, []);
}
}
$this->assertNotEquals($mid1, $convmessages2['messages'][0]->id);
}
+ /**
+ * Test retrieving conversation messages by providing a timefrom higher than last message timecreated. It should return no
+ * messages but keep the return structure to not break when called from the ws.
+ */
+ public function test_get_conversation_messages_timefrom_higher_than_last_timecreated() {
+ // Create some users.
+ $user1 = self::getDataGenerator()->create_user();
+ $user2 = self::getDataGenerator()->create_user();
+ $user3 = self::getDataGenerator()->create_user();
+ $user4 = self::getDataGenerator()->create_user();
+
+ // Create group conversation.
+ $conversation = \core_message\api::create_conversation(
+ \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+ [$user1->id, $user2->id, $user3->id, $user4->id]
+ );
+
+ // The person doing the search.
+ $this->setUser($user1);
+
+ // Send some messages back and forth.
+ $time = 1;
+ testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Message 1', $time + 1);
+ testhelper::send_fake_message_to_conversation($user2, $conversation->id, 'Message 2', $time + 2);
+ testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Message 3', $time + 3);
+ testhelper::send_fake_message_to_conversation($user3, $conversation->id, 'Message 4', $time + 4);
+
+ // Retrieve the messages from $time + 5, which should return no messages.
+ $convmessages = \core_message\api::get_conversation_messages($user1->id, $conversation->id, 0, 0, '', $time + 5);
+
+ // Confirm the conversation id is correct.
+ $this->assertEquals($conversation->id, $convmessages['id']);
+
+ // Confirm the message data is correct.
+ $messages = $convmessages['messages'];
+ $this->assertEquals(0, count($messages));
+
+ // Confirm that members key is present.
+ $this->assertArrayHasKey('members', $convmessages);
+ }
+
/**
* Helper to seed the database with initial state with data.
*/
core_message_external::delete_message_for_all_users($messageid, $user1->id);
}
+ /**
+ * Test retrieving conversation messages by providing a timefrom higher than last message timecreated. It should return no
+ * messages but keep the return structure to not break when called from the ws.
+ */
+ public function test_get_conversation_messages_timefrom_higher_than_last_timecreated() {
+ $this->resetAfterTest(true);
+
+ // Create some users.
+ $user1 = self::getDataGenerator()->create_user();
+ $user2 = self::getDataGenerator()->create_user();
+ $user3 = self::getDataGenerator()->create_user();
+ $user4 = self::getDataGenerator()->create_user();
+
+ // Create group conversation.
+ $conversation = \core_message\api::create_conversation(
+ \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+ [$user1->id, $user2->id, $user3->id, $user4->id]
+ );
+
+ // The person asking for the messages for another user.
+ $this->setUser($user1);
+
+ // Send some messages back and forth.
+ $time = 1;
+ testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Message 1', $time + 1);
+ testhelper::send_fake_message_to_conversation($user2, $conversation->id, 'Message 2', $time + 2);
+ testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Message 3', $time + 3);
+ testhelper::send_fake_message_to_conversation($user3, $conversation->id, 'Message 4', $time + 4);
+
+ // Retrieve the messages.
+ $result = core_message_external::get_conversation_messages($user1->id, $conversation->id, 0, 0, '', $time + 5);
+
+ // We need to execute the return values cleaning process to simulate the web service server.
+ $result = external_api::clean_returnvalue(core_message_external::get_conversation_messages_returns(), $result);
+
+ // Check the results are correct.
+ $this->assertEquals($conversation->id, $result['id']);
+
+ // Confirm the message data is correct.
+ $messages = $result['messages'];
+ $this->assertEquals(0, count($messages));
+
+ // Confirm that members key is present.
+ $this->assertArrayHasKey('members', $result);
+ }
+
/**
* Helper to seed the database with initial state with data.
*/
'null' => NULL_ALLOWED
],
'timecreated' => ['type' => PARAM_INT],
+ 'timemodified' => ['type' => PARAM_INT],
'unread' => [
'type' => PARAM_BOOL,
'optional' => true,
'hasparent' => $post->has_parent(),
'parentid' => $post->has_parent() ? $post->get_parent_id() : null,
'timecreated' => $timecreated,
+ 'timemodified' => $post->get_time_modified(),
'unread' => ($loadcontent && $readreceiptcollection) ? !$readreceiptcollection->has_user_read_post($user, $post) : null,
'isdeleted' => $isdeleted,
'isprivatereply' => $isprivatereply,
'parentid' => $discussion1reply2->parent,
'hasparent' => true,
'timecreated' => $discussion1reply2->created,
+ 'timemodified' => $discussion1reply2->modified,
'subject' => $discussion1reply2->subject,
'replysubject' => get_string('re', 'mod_forum') . " {$discussion1reply2->subject}",
'message' => $message,
'parentid' => $discussion1reply1->parent,
'hasparent' => true,
'timecreated' => $discussion1reply1->created,
+ 'timemodified' => $discussion1reply1->modified,
'subject' => $discussion1reply1->subject,
'replysubject' => get_string('re', 'mod_forum') . " {$discussion1reply1->subject}",
'message' => $message,
'parentid' => $discussion1reply1->parent,
'hasparent' => true,
'timecreated' => $discussion1reply1->created,
+ 'timemodified' => $discussion1reply1->modified,
'subject' => $discussion1reply1->subject,
'replysubject' => get_string('re', 'mod_forum') . " {$discussion1reply1->subject}",
'message' => file_rewrite_pluginfile_urls($discussion1reply1->message, 'pluginfile.php',
'parentid' => null,
'hasparent' => false,
'timecreated' => $discussion1firstpostobject->created,
+ 'timemodified' => $discussion1firstpostobject->modified,
'subject' => $discussion1firstpostobject->subject,
'replysubject' => get_string('re', 'mod_forum') . " {$discussion1firstpostobject->subject}",
'message' => file_rewrite_pluginfile_urls($discussion1firstpostobject->message, 'pluginfile.php',
'parentid' => $discussion2reply1->parent,
'hasparent' => true,
'timecreated' => $discussion2reply1->created,
+ 'timemodified' => $discussion2reply1->modified,
'subject' => $discussion2reply1->subject,
'replysubject' => get_string('re', 'mod_forum') . " {$discussion2reply1->subject}",
'message' => file_rewrite_pluginfile_urls($discussion2reply1->message, 'pluginfile.php',
'parentid' => null,
'hasparent' => false,
'timecreated' => $discussion2firstpostobject->created,
+ 'timemodified' => $discussion2firstpostobject->modified,
'subject' => $discussion2firstpostobject->subject,
'replysubject' => get_string('re', 'mod_forum') . " {$discussion2firstpostobject->subject}",
'message' => file_rewrite_pluginfile_urls($discussion2firstpostobject->message, 'pluginfile.php',
This files describes API changes in /mod/forum/*,
information provided here is intended especially for developers.
+=== 3.11 ===
+
+* The forum post exporter now includes a "timemodified" field for each post, which is included in several WS methods:
+ * mod_forum_get_discussion_posts
+ * get_discussion_posts_by_userid
+ * get_discussion_post
+ * add_discussion_post
+
=== 3.10 ===
* Changes in external function mod_forum_external::get_discussion_posts_by_userid
if ($this->can_view_all_attempts()) {
$user = core_user::get_user($userid);
} else if ($this->can_view_own_attempts()) {
- $user = $USER;
+ $user = core_user::get_user($USER->id);
if ($userid && $user->id != $userid) {
return null;
}
// Create a question.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$questioncategory = $questiongenerator->create_question_category();
- $overrides = ['category' => $questioncategory->id, 'createdby' => $user->id, 'modifiedby' => $user->id];
+ $overrides = ['name' => 'Test question', 'category' => $questioncategory->id,
+ 'createdby' => $user->id, 'modifiedby' => $user->id];
$question = $questiongenerator->create_question('truefalse', null, $overrides);
// Create a quiz and a questions.
$rc->destroy();
// Test the question author.
- $questions = $DB->get_records('question');
+ $questions = $DB->get_records('question', ['name' => 'Test question']);
$this->assertCount(1, $questions);
$question3 = array_shift($questions);
$this->assertEquals($user->id, $question3->createdby);
// Create a question.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$questioncategory = $questiongenerator->create_question_category();
- $overrides = ['category' => $questioncategory->id, 'createdby' => $user->id, 'modifiedby' => $user->id];
+ $overrides = ['name' => 'Test question', 'category' => $questioncategory->id,
+ 'createdby' => $user->id, 'modifiedby' => $user->id];
$question = $questiongenerator->create_question('truefalse', null, $overrides);
// Create a quiz and a questions.
$rc->destroy();
// Test the question author.
- $questions = $DB->get_records('question');
+ $questions = $DB->get_records('question', ['name' => 'Test question']);
$this->assertCount(1, $questions);
$question = array_shift($questions);
$this->assertEquals($user->id, $question->createdby);
// Create a question.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$questioncategory = $questiongenerator->create_question_category();
- $overrides = ['category' => $questioncategory->id, 'createdby' => $user->id, 'modifiedby' => $user->id];
+ $overrides = ['name' => 'Test question', 'category' => $questioncategory->id,
+ 'createdby' => $user->id, 'modifiedby' => $user->id];
$question = $questiongenerator->create_question('truefalse', null, $overrides);
// Create a quiz and a questions.
$rc->destroy();
// Test the question author.
- $questions = $DB->get_records('question');
+ $questions = $DB->get_records('question', ['name' => 'Test question']);
$this->assertCount(1, $questions);
$question = array_shift($questions);
$this->assertEquals($USER->id, $question->createdby);
* @copyright 2018 Damyon Wiese <damyon@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define(['jquery', 'core/pending'], function($, Pending) {
- return {
- init: function() {
- // Drop downs from bootstrap don't support keyboard accessibility by default.
- var focusEnd = false,
- setFocusEnd = function() {
- focusEnd = true;
- },
- getFocusEnd = function() {
- var result = focusEnd;
- focusEnd = false;
- return result;
- };
-
- // Special handling for "up" keyboard control.
- $('[data-toggle="dropdown"]').keydown(function(e) {
- var trigger = e.which || e.keyCode,
- expanded;
-
- // Up key opens the menu at the end.
- if (trigger == 38) {
- // Focus the end of the menu, not the beginning.
- setFocusEnd();
- }
- // Escape key only closes the menu, it doesn't open it.
- if (trigger == 27) {
- expanded = $(e.target).attr('aria-expanded');
- e.preventDefault();
- if (expanded == "false") {
- $(e.target).click();
- }
- }
+import $ from 'jquery';
+import Pending from 'core/pending';
+
+/**
+ * Drop downs from bootstrap don't support keyboard accessibility by default.
+ */
+const dropdownFix = () => {
+ let focusEnd = false;
+ const setFocusEnd = () => {
+ focusEnd = true;
+ };
+ const getFocusEnd = () => {
+ const result = focusEnd;
+ focusEnd = false;
+ return result;
+ };
- // Space key or Enter key opens the menu.
- if (trigger == 32 || trigger == 13) {
- // Cancel random scroll.
- e.preventDefault();
- // Open the menu instead.
- $(e.target).click();
+ // Special handling for "up" keyboard control.
+ document.addEventListener('keydown', e => {
+ if (e.target.matches('[data-toggle="dropdown"]')) {
+ const trigger = e.key;
+
+ // Up key opens the menu at the end.
+ if (trigger == 'ArrowUp') {
+ // Focus the end of the menu, not the beginning.
+ setFocusEnd();
+ }
+
+ // Escape key only closes the menu, it doesn't open it.
+ if (trigger == 'Escape') {
+ const expanded = e.target.getAttribute('aria-expanded');
+ e.preventDefault();
+ if (expanded == "false") {
+ e.target.click();
}
- });
+ }
+
+ // Space key or Enter key opens the menu.
+ if (trigger == ' ' || trigger == 'Enter') {
+ // Cancel random scroll.
+ e.preventDefault();
+ // Open the menu instead.
+ e.target.click();
+ }
+ }
+ });
+
+ // Special handling for navigation keys when menu is open.
+ const shiftFocus = element => {
+ const delayedFocus = pendingPromise => {
+ element.focus();
+ pendingPromise.resolve();
+ };
+ setTimeout(delayedFocus, 50, new Pending('core/aria:delayed-focus'));
+ };
+
+ $('.dropdown').on('shown.bs.dropdown', e => {
+ // We need to focus on the first menuitem.
+ const menu = e.target.querySelector('[role="menu"]');
+ let menuItems = false;
+ let foundMenuItem = false;
- // Special handling for navigation keys when menu is open.
- var shiftFocus = function(element) {
- var delayedFocus = function(pendingPromise) {
- $(this).focus();
- pendingPromise.resolve();
- }.bind(element);
- setTimeout(delayedFocus, 50, new Pending('core/aria:delayed-focus'));
- };
-
- $('.dropdown').on('shown.bs.dropdown', function(e) {
- // We need to focus on the first menuitem.
- var menu = $(e.target).find('[role="menu"]'),
- menuItems = false,
- foundMenuItem = false;
-
- if (menu) {
- menuItems = $(menu).find('[role="menuitem"]');
+ if (menu) {
+ menuItems = menu.querySelectorAll('[role="menuitem"]');
+ }
+ if (menuItems && menuItems.length > 0) {
+ if (getFocusEnd()) {
+ foundMenuItem = menuItems[menuItems.length - 1];
+ } else {
+ // The first menu entry, pretty reasonable.
+ foundMenuItem = menuItems[0];
+ }
+ }
+ if (foundMenuItem) {
+ shiftFocus(foundMenuItem);
+ }
+ });
+ // Search for menu items by finding the first item that has
+ // text starting with the typed character (case insensitive).
+ document.addEventListener('keypress', e => {
+ if (e.target.matches('.dropdown [role="menu"] [role="menuitem"]')) {
+ const menu = e.target.closest('[role="menu"]');
+ if (!menu) {
+ return;
+ }
+ const menuItems = menu.querySelectorAll('[role="menuitem"]');
+ if (!menuItems) {
+ return;
+ }
+
+ const trigger = e.key.toLowerCase();
+
+ for (let i = 0; i < menuItems.length; i++) {
+ const item = menuItems[i];
+ const itemText = item.text.trim().toLowerCase();
+ if (itemText.indexOf(trigger) == 0) {
+ shiftFocus(item);
+ break;
}
- if (menuItems && menuItems.length > 0) {
- if (getFocusEnd()) {
- foundMenuItem = menuItems[menuItems.length - 1];
- } else {
- // The first menu entry, pretty reasonable.
- foundMenuItem = menuItems[0];
+ }
+ }
+ });
+
+ // Keyboard navigation for arrow keys, home and end keys.
+ document.addEventListener('keydown', e => {
+ if (e.target.matches('.dropdown [role="menu"] [role="menuitem"]')) {
+ const trigger = e.key;
+ let next = false;
+ const menu = e.target.closest('[role="menu"]');
+
+ if (!menu) {
+ return;
+ }
+ const menuItems = menu.querySelectorAll('[role="menuitem"]');
+ if (!menuItems) {
+ return;
+ }
+ // Down key.
+ if (trigger == 'ArrowDown') {
+ for (let i = 0; i < menuItems.length - 1; i++) {
+ if (menuItems[i] == e.target) {
+ next = menuItems[i + 1];
+ break;
}
}
- if (foundMenuItem) {
- shiftFocus(foundMenuItem);
- }
- });
- // Search for menu items by finding the first item that has
- // text starting with the typed character (case insensitive).
- $('.dropdown [role="menu"] [role="menuitem"]').keypress(function(e) {
- var trigger = String.fromCharCode(e.which || e.keyCode),
- menu = $(e.target).closest('[role="menu"]'),
- i = 0,
- menuItems = false,
- item,
- itemText;
-
- if (!menu) {
- return;
- }
- menuItems = $(menu).find('[role="menuitem"]');
- if (!menuItems) {
- return;
+ if (!next) {
+ // Wrap to first item.
+ next = menuItems[0];
}
- trigger = trigger.toLowerCase();
- for (i = 0; i < menuItems.length; i++) {
- item = $(menuItems[i]);
- itemText = item.text().trim().toLowerCase();
- if (itemText.indexOf(trigger) == 0) {
- shiftFocus(item);
+ } else if (trigger == 'ArrowUp') {
+ // Up key.
+ for (let i = 1; i < menuItems.length; i++) {
+ if (menuItems[i] == e.target) {
+ next = menuItems[i - 1];
break;
}
}
- });
-
- // Keyboard navigation for arrow keys, home and end keys.
- $('.dropdown [role="menu"] [role="menuitem"]').keydown(function(e) {
- var trigger = e.which || e.keyCode,
- next = false,
- menu = $(e.target).closest('[role="menu"]'),
- i = 0,
- menuItems = false;
- if (!menu) {
- return;
- }
- menuItems = $(menu).find('[role="menuitem"]');
- if (!menuItems) {
- return;
+ if (!next) {
+ // Wrap to last item.
+ next = menuItems[menuItems.length - 1];
}
- // Down key.
- if (trigger == 40) {
- for (i = 0; i < menuItems.length - 1; i++) {
- if (menuItems[i] == e.target) {
- next = menuItems[i + 1];
- break;
- }
- }
- if (!next) {
- // Wrap to first item.
- next = menuItems[0];
- }
- } else if (trigger == 38) {
- // Up key.
- for (i = 1; i < menuItems.length; i++) {
- if (menuItems[i] == e.target) {
- next = menuItems[i - 1];
- break;
- }
- }
- if (!next) {
- // Wrap to last item.
- next = menuItems[menuItems.length - 1];
- }
+ } else if (trigger == 'Home') {
+ // Home key.
+ next = menuItems[0];
- } else if (trigger == 36) {
- // Home key.
- next = menuItems[0];
+ } else if (trigger == 'End') {
+ // End key.
+ next = menuItems[menuItems.length - 1];
+ }
+ // Variable next is set if we do want to act on the keypress.
+ if (next) {
+ e.preventDefault();
+ shiftFocus(next);
+ }
+ return;
+ }
+ });
- } else if (trigger == 35) {
- // End key.
- next = menuItems[menuItems.length - 1];
- }
- // Variable next is set if we do want to act on the keypress.
- if (next) {
- e.preventDefault();
- shiftFocus(next);
- }
- return;
- });
- $('.dropdown').on('hidden.bs.dropdown', function(e) {
- // We need to focus on the menu trigger.
- var trigger = $(e.target).find('[data-toggle="dropdown"]');
- if (trigger) {
- shiftFocus(trigger);
- }
+ $('.dropdown').on('hidden.bs.dropdown', e => {
+ // We need to focus on the menu trigger.
+ const trigger = e.target.querySelector('[data-toggle="dropdown"]');
+ if (trigger) {
+ shiftFocus(trigger);
+ }
+ });
+};
+
+/**
+ * After page load, focus on any element with special autofocus attribute.
+ */
+const autoFocus = () => {
+ window.addEventListener("load", () => {
+ const alerts = document.querySelectorAll('[data-aria-autofocus="true"][role="alert"]');
+ Array.prototype.forEach.call(alerts, autofocusElement => {
+ // According to the specification an role="alert" region is only read out on change to the content
+ // of that region.
+ autofocusElement.innerHTML += ' ';
+ autofocusElement.removeAttribute('data-aria-autofocus');
+ });
+ });
+};
+
+/**
+ * Changes the focus to the correct tab based on the key that is pressed.
+ * @param {KeyboardEvent} e
+ */
+const updateTabFocus = e => {
+ const tabList = e.target.closest('[role="tablist"]');
+ const vertical = tabList.getAttribute('aria-orientation') == 'vertical';
+ const rtl = window.right_to_left();
+ const arrowNext = vertical ? 'ArrowDown' : (rtl ? 'ArrowLeft' : 'ArrowRight');
+ const arrowPrevious = vertical ? 'ArrowUp' : (rtl ? 'ArrowRight' : 'ArrowLeft');
+ const tabs = Array.prototype.filter.call(
+ tabList.querySelectorAll('[role="tab"]'),
+ tab => getComputedStyle(tab).display !== 'none'); // We only work with the visible tabs.
+
+ for (let i = 0; i < tabs.length; i++) {
+ tabs[i].index = i;
+ }
+
+ switch (e.key) {
+ case arrowNext:
+ e.preventDefault();
+ if (e.target.index !== undefined && tabs[e.target.index + 1]) {
+ tabs[e.target.index + 1].focus();
+ } else {
+ tabs[0].focus();
+ }
+ break;
+ case arrowPrevious:
+ e.preventDefault();
+ if (e.target.index !== undefined && tabs[e.target.index - 1]) {
+ tabs[e.target.index - 1].focus();
+ } else {
+ tabs[tabs.length - 1].focus();
+ }
+ break;
+ case 'Home':
+ e.preventDefault();
+ tabs[0].focus();
+ break;
+ case 'End':
+ e.preventDefault();
+ tabs[tabs.length - 1].focus();
+ break;
+ case 'Enter':
+ case ' ':
+ e.preventDefault();
+ $(e.target).tab('show');
+ tabs.forEach(tab => {
+ tab.tabIndex = -1;
});
+ e.target.tabIndex = 0;
+ }
+};
+
+/**
+ * Fix accessibility issues regarding tab elements focus and their tab order in Bootstrap navs.
+ */
+const tabElementFix = () => {
+ document.addEventListener('keydown', e => {
+ if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'Enter', ' '].includes(e.key)) {
+ if (e.target.matches('[role="tablist"] [role="tab"]')) {
+ updateTabFocus(e);
+ }
+ }
+ });
- // After page load, focus on any element with special autofocus attribute.
- window.addEventListener("load", () => {
- const alerts = document.querySelectorAll('[data-aria-autofocus="true"][role="alert"]');
- Array.prototype.forEach.call(alerts, autofocusElement => {
- // According to the specification an role="alert" region is only read out on change to the content
- // of that region.
- autofocusElement.innerHTML += ' ';
- autofocusElement.removeAttribute('data-aria-autofocus');
- });
+ document.addEventListener('click', e => {
+ if (e.target.matches('[role="tablist"] [role="tab"]')) {
+ const tabs = e.target.closest('[role="tablist"]').querySelectorAll('[role="tab"]');
+ e.preventDefault();
+ $(e.target).tab('show');
+ tabs.forEach(tab => {
+ tab.tabIndex = -1;
});
+ e.target.tabIndex = 0;
}
- };
-});
+ });
+};
+
+export const init = () => {
+ dropdownFix();
+ autoFocus();
+ tabElementFix();
+};
*/
import $ from 'jquery';
-import Aria from './aria';
+import * as Aria from './aria';
import Bootstrap from './bootstrap/index';
import Pending from 'core/pending';
import Scroll from './scroll';
location.hash = hash;
}
});
- var hash = window.location.hash;
+ const hash = window.location.hash;
if (hash) {
- $('.nav-link[href="' + hash + '"]').tab('show');
+ const tab = document.querySelector('.nav-link[href="' + hash + '"]');
+ if (tab) {
+ tab.click();
+ }
}
};
// Add pending promise event listeners to relevant Bootstrap custom events.
setupBootstrapPendingChecks();
+// Setup Aria helpers for Bootstrap features.
+Aria.init();
+
// Remember the last visited tabs.
rememberTabs();
// Disables flipping the dropdowns up and getting hidden behind the navbar.
$.fn.dropdown.Constructor.Default.flip = false;
-// Setup Aria helpers for Bootstrap features.
-Aria.init();
-
pendingPromise.resolve();
export {
<ul class="nav nav-tabs" role="tablist">
{{#tabs}}
<li class="nav-item">
- <a href="#{{name}}" class="nav-link {{#active}}active{{/active}}" data-toggle="tab" role="tab">{{displayname}}</a>
+ <a href="#{{name}}" class="nav-link {{#active}}active{{/active}}" data-toggle="tab" role="tab"
+ {{#active}}aria-selected="true"{{/active}}
+ {{^active}}aria-selected="false" tabindex="-1"{{/active}}>{{displayname}}</a>
</li>
{{/tabs}}
</ul>
$PAGE->set_title("$course->fullname: $strpersonalprofile: $fullname");
$PAGE->set_heading($course->fullname);
-$PAGE->set_pagelayout('standard');
+$PAGE->set_pagelayout('mypublic');
// Locate the users settings in the settings navigation and force it open.
// This MUST be done after we've set up the page as it is going to cause theme and output to initialise.
defined('MOODLE_INTERNAL') || die();
-$version = 2021052500.44; // YYYYMMDD = weekly release date of this DEV branch.
+$version = 2021052500.46; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
-$release = '4.0dev (Build: 20201127)'; // Human-friendly version name
+$release = '4.0dev (Build: 20201204)'; // Human-friendly version name
$branch = '400'; // This version's branch.
$maturity = MATURITY_ALPHA; // This version's maturity level.