Merge branch 'MDL-70279' of git://github.com/paulholden/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 22 Dec 2020 23:46:42 +0000 (00:46 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 22 Dec 2020 23:46:42 +0000 (00:46 +0100)
229 files changed:
admin/cli/generate_key.php [new file with mode: 0644]
admin/settings/server.php
admin/templates/setting_encryptedpassword.mustache [new file with mode: 0644]
admin/tests/behat/behat_admin.php
admin/tool/behat/tests/behat/edit_permissions.feature
admin/tool/behat/tests/behat/keyboard.feature
admin/tool/dataprivacy/tests/behat/manage_purposes.feature
admin/tool/lp/tests/behat/plan_crud.feature
admin/upgrade.txt
analytics/classes/local/analyser/base.php
analytics/classes/manager.php
analytics/tests/behat/manage_models.feature
analytics/upgrade.txt
backup/tests/quiz_restore_decode_links_test.php
blocks/myoverview/amd/build/view.min.js
blocks/myoverview/amd/build/view.min.js.map
blocks/myoverview/amd/src/view.js
blocks/myoverview/classes/output/main.php
blocks/myoverview/tests/behat/block_myoverview_pagelimit_persistence.feature
blocks/myoverview/tests/behat/block_myoverview_pagination.feature [new file with mode: 0644]
blocks/timeline/templates/event-list-item.mustache
calendar/classes/external/calendar_event_exporter.php
config-dist.php
course/amd/build/local/activitychooser/dialogue.min.js
course/amd/build/local/activitychooser/dialogue.min.js.map
course/amd/build/local/activitychooser/selectors.min.js
course/amd/build/local/activitychooser/selectors.min.js.map
course/amd/src/local/activitychooser/dialogue.js
course/amd/src/local/activitychooser/selectors.js
course/classes/customfield/course_handler.php
course/tests/behat/coursetags.feature
course/tests/behat/rename_roles.feature
enrol/cohort/settings.php
enrol/database/settings.php
enrol/externallib.php
enrol/fee/classes/plugin.php
enrol/fee/settings.php
enrol/manual/settings.php
enrol/manual/tests/behat/quickenrolment.feature
enrol/mnet/settings.php
enrol/paypal/settings.php
enrol/self/settings.php
enrol/tests/externallib_test.php
enrol/upgrade.txt
files/classes/archive_writer.php [new file with mode: 0644]
files/classes/local/archive_writer/file_writer_interface.php [new file with mode: 0644]
files/classes/local/archive_writer/stream_writer_interface.php [new file with mode: 0644]
files/classes/local/archive_writer/zip_writer.php [new file with mode: 0644]
files/tests/archive_writer_test.php [new file with mode: 0644]
files/tests/fixtures/awesome_file.txt [new file with mode: 0644]
files/tests/local/archive_writer/zip_writer_test.php [new file with mode: 0644]
group/tests/behat/create_groups.feature
group/tests/behat/group_description.feature
install/lang/es/langconfig.php
install/lang/ky/admin.php [new file with mode: 0644]
install/lang/mk/error.php
install/lang/mk/moodle.php
install/lang/no/install.php
install/lang/ro/install.php
install/lang/ro/langconfig.php
install/lang/ro_wp/langconfig.php
install/lang/sv/admin.php
lang/en/admin.php
lang/en/error.php
lang/en/moodle.php
lib/adminlib.php
lib/amd/build/form-autocomplete.min.js
lib/amd/build/form-autocomplete.min.js.map
lib/amd/build/network.min.js
lib/amd/build/network.min.js.map
lib/amd/build/paged_content_factory.min.js
lib/amd/build/paged_content_factory.min.js.map
lib/amd/build/permissionmanager.min.js
lib/amd/build/permissionmanager.min.js.map
lib/amd/src/form-autocomplete.js
lib/amd/src/network.js
lib/amd/src/paged_content_factory.js
lib/amd/src/permissionmanager.js
lib/behat/behat_field_manager.php
lib/behat/classes/behat_session_interface.php
lib/behat/form_field/behat_form_autocomplete.php
lib/classes/encryption.php [new file with mode: 0644]
lib/classes/oauth2/client.php
lib/classes/output/notification.php
lib/classes/task/manager.php
lib/cronlib.php
lib/db/upgrade.php
lib/dml/moodle_database.php
lib/dml/mysqli_native_moodle_database.php
lib/dml/oci_native_moodle_database.php
lib/dml/pdo_moodle_database.php
lib/dml/pgsql_native_moodle_database.php
lib/dml/sqlsrv_native_moodle_database.php
lib/dml/tests/fixtures/test_moodle_database.php
lib/form/amd/build/encryptedpassword.min.js [new file with mode: 0644]
lib/form/amd/build/encryptedpassword.min.js.map [new file with mode: 0644]
lib/form/amd/src/encryptedpassword.js [new file with mode: 0644]
lib/form/templates/element-advcheckbox.mustache
lib/form/templates/element-checkbox.mustache
lib/form/templates/element-radio.mustache
lib/form/templates/element-template.mustache
lib/html2text/Html2Text.php
lib/html2text/readme_moodle.txt
lib/moodlelib.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/setup.php
lib/simplepie/LICENSE.txt
lib/simplepie/README.markdown
lib/simplepie/autoloader.php
lib/simplepie/library/SimplePie.php
lib/simplepie/library/SimplePie/Author.php
lib/simplepie/library/SimplePie/Cache.php
lib/simplepie/library/SimplePie/Cache/Base.php
lib/simplepie/library/SimplePie/Cache/DB.php
lib/simplepie/library/SimplePie/Cache/File.php
lib/simplepie/library/SimplePie/Cache/Memcache.php
lib/simplepie/library/SimplePie/Cache/Memcached.php
lib/simplepie/library/SimplePie/Cache/MySQL.php
lib/simplepie/library/SimplePie/Caption.php
lib/simplepie/library/SimplePie/Category.php
lib/simplepie/library/SimplePie/Content/Type/Sniffer.php
lib/simplepie/library/SimplePie/Copyright.php
lib/simplepie/library/SimplePie/Core.php
lib/simplepie/library/SimplePie/Credit.php
lib/simplepie/library/SimplePie/Decode/HTML/Entities.php
lib/simplepie/library/SimplePie/Enclosure.php
lib/simplepie/library/SimplePie/Exception.php
lib/simplepie/library/SimplePie/File.php
lib/simplepie/library/SimplePie/HTTP/Parser.php
lib/simplepie/library/SimplePie/IRI.php
lib/simplepie/library/SimplePie/Item.php
lib/simplepie/library/SimplePie/Locator.php
lib/simplepie/library/SimplePie/Misc.php
lib/simplepie/library/SimplePie/Net/IPv6.php
lib/simplepie/library/SimplePie/Parse/Date.php
lib/simplepie/library/SimplePie/Parser.php
lib/simplepie/library/SimplePie/Rating.php
lib/simplepie/library/SimplePie/Registry.php
lib/simplepie/library/SimplePie/Restriction.php
lib/simplepie/library/SimplePie/Sanitize.php
lib/simplepie/library/SimplePie/Source.php
lib/simplepie/library/SimplePie/XML/Declaration/Parser.php
lib/simplepie/library/SimplePie/gzdecode.php
lib/simplepie/readme_moodle.txt
lib/table/classes/local/filter/filter.php
lib/table/classes/local/filter/filterset.php
lib/templates/form_autocomplete_input.mustache
lib/templates/paged_content_paging_bar.mustache
lib/templates/permissionmanager_panelcontent.mustache
lib/templates/permissionmanager_role.mustache
lib/templates/search_input_navbar.mustache
lib/templates/settings_link_page.mustache
lib/tests/analysers_test.php
lib/tests/behat/behat_forms.php
lib/tests/behat/behat_hooks.php
lib/tests/encryption_test.php [new file with mode: 0644]
lib/tests/fixtures/deprecated_analyser.php [deleted file]
lib/tests/fixtures/testable_encryption.php [new file with mode: 0644]
lib/tests/scheduled_task_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
mod/assign/locallib.php
mod/assign/module.js
mod/chat/gui_header_js/jsupdate.php
mod/chat/gui_header_js/jsupdated.php
mod/chat/gui_sockets/chatinput.php
mod/data/export_form.php
mod/data/lang/en/data.php
mod/data/lang/en/deprecated.txt
mod/data/tests/behat/view_entries.feature
mod/feedback/tests/behat/coursemapping.feature
mod/folder/download_folder.php
mod/folder/renderer.php
mod/forum/externallib.php
mod/forum/tests/behat/advanced_search.feature
mod/forum/upgrade.txt
mod/lti/amd/build/contentitem.min.js
mod/lti/amd/build/contentitem.min.js.map
mod/lti/amd/src/contentitem.js
mod/lti/classes/local/ltiopenid/registration_helper.php
mod/lti/locallib.php
mod/lti/openid-configuration.php
mod/lti/tests/openidregistration_test.php
mod/quiz/attemptlib.php
mod/quiz/db/access.php
mod/quiz/lang/en/deprecated.txt
mod/quiz/lang/en/quiz.php
mod/quiz/lib.php
mod/quiz/locallib.php
mod/quiz/overrides.php
mod/quiz/renderer.php
mod/quiz/tests/attempt_test.php
mod/quiz/tests/attempts_test.php
mod/quiz/tests/behat/quiz_group_override.feature
mod/quiz/tests/behat/quiz_user_override.feature
mod/quiz/tests/generator/lib.php
mod/quiz/tests/locallib_test.php
mod/quiz/version.php
payment/classes/helper.php
question/engine/lib.php
question/type/ddmarker/amd/build/question.min.js
question/type/ddmarker/amd/build/question.min.js.map
question/type/ddmarker/amd/src/question.js
question/type/essay/renderer.php
report/competency/tests/behat/breakdown_by_activity.feature
search/classes/manager.php
search/tests/manager_test.php
theme/boost/amd/build/aria.min.js
theme/boost/amd/build/aria.min.js.map
theme/boost/amd/build/loader.min.js
theme/boost/amd/build/loader.min.js.map
theme/boost/amd/src/aria.js
theme/boost/amd/src/loader.js
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/question.scss
theme/boost/style/moodle.css
theme/boost/templates/admin_setting_tabs.mustache
theme/classic/style/moodle.css
user/amd/build/participantsfilter.min.js
user/amd/build/participantsfilter.min.js.map
user/amd/src/participantsfilter.js
user/templates/participantsfilter.mustache
user/tests/behat/edit_user_roles.feature
user/tests/behat/filter_participants.feature
user/tests/behat/filter_participants_showall.feature
user/tests/behat/full_name_display.feature
user/tests/behat/view_participants_groups.feature
version.php

diff --git a/admin/cli/generate_key.php b/admin/cli/generate_key.php
new file mode 100644 (file)
index 0000000..28fd0af
--- /dev/null
@@ -0,0 +1,77 @@
+<?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";
index 11541bb..beb70f3 100644 (file)
@@ -73,6 +73,22 @@ if ($hassiteconfig) {
     $temp->add(new admin_setting_configduration('sessiontimeout', new lang_string('sessiontimeout', 'admin'),
         new lang_string('configsessiontimeout', 'admin'), 8 * 60 * 60));
 
+    $sessiontimeoutwarning = new admin_setting_configduration('sessiontimeoutwarning',
+        new lang_string('sessiontimeoutwarning', 'admin'),
+        new lang_string('configsessiontimeoutwarning', 'admin'), 20 * 60);
+
+    $sessiontimeoutwarning->set_validate_function(function(int $value): string {
+        global $CFG;
+        // Check sessiontimeoutwarning is less than sessiontimeout.
+        if ($CFG->sessiontimeout <= $value) {
+            return get_string('configsessiontimeoutwarningcheck', 'admin');
+        } else {
+            return '';
+        }
+    });
+
+    $temp->add($sessiontimeoutwarning);
+
     $temp->add(new admin_setting_configtext('sessioncookie', new lang_string('sessioncookie', 'admin'),
         new lang_string('configsessioncookie', 'admin'), '', PARAM_ALPHANUM));
     $temp->add(new admin_setting_configtext('sessioncookiepath', new lang_string('sessioncookiepath', 'admin'),
diff --git a/admin/templates/setting_encryptedpassword.mustache b/admin/templates/setting_encryptedpassword.mustache
new file mode 100644 (file)
index 0000000..e2a98cf
--- /dev/null
@@ -0,0 +1,64 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <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}}
index 98f5d91..d71af45 100644 (file)
@@ -89,7 +89,7 @@ class behat_admin extends behat_base {
     }
 
     /**
-     * Sets the specified site settings. A table with | config | value | (optional)plugin | is expected.
+     * Sets the specified site settings. A table with | config | value | (optional)plugin | (optional)encrypted | is expected.
      *
      * @Given /^the following config values are set as admin:$/
      * @param TableNode $table
@@ -103,11 +103,20 @@ class behat_admin extends behat_base {
         foreach ($data as $config => $value) {
             // Default plugin value is null.
             $plugin = null;
+            $encrypted = false;
 
             if (is_array($value)) {
                 $plugin = $value[1];
+                if (array_key_exists(2, $value)) {
+                    $encrypted = $value[2] === 'encrypted';
+                }
                 $value = $value[0];
             }
+
+            if ($encrypted) {
+                $value = \core\encryption::encrypt($value);
+            }
+
             set_config($config, $value, $plugin);
         }
     }
index e3ffc92..7394512 100644 (file)
@@ -6,14 +6,18 @@ Feature: Edit capabilities
 
   Background:
     Given the following "users" exist:
-      | username | firstname | lastname | email |
-      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | username | firstname | lastname  |
+      | teacher1 | Teacher   | 1         |
+      | tutor    | Teaching  | Assistant |
+      | student  | Student   | One       |
     And the following "courses" exist:
-      | fullname | shortname | category |
-      | Course 1 | C1 | 0 |
+      | fullname | shortname |
+      | Course 1 | C1        |
     And the following "course enrolments" exist:
-      | user | course | role |
-      | teacher1 | C1 | editingteacher |
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+      | tutor    | C1     | teacher        |
+      | student  | C1     | student        |
 
   Scenario: Default system capabilities modification
     Given I log in as "admin"
@@ -60,3 +64,25 @@ Feature: Edit capabilities
     Then "mod/forum:deleteanypost" capability has "Prohibit" permission
     And "mod/forum:editanypost" capability has "Prevent" permission
     And "mod/forum:addquestion" capability has "Allow" permission
+
+  @javascript
+  Scenario: Edit permissions escapes role names correctly
+    When I am on the "C1" "Course" page logged in as "admin"
+    And I navigate to "Edit settings" in current page administration
+    And I set the following fields to these values:
+      | Your word for 'Teacher'             | Teacher >= editing  |
+      | Your word for 'Non-editing teacher' | Teacher < "editing" |
+      | Your word for 'Student'             | Studier & 'learner' |
+    And I press "Save and display"
+    And I navigate to course participants
+    Then I should see "Teacher >= editing (Teacher)" in the "Teacher 1" "table_row"
+    And I should see "Teacher < \"editing\" (Non-editing teacher)" in the "Teaching Assistant" "table_row"
+    And I should see "Studier & 'learner' (Student)" in the "Student One" "table_row"
+    And I navigate to "Users > Permissions" in current page administration
+    And I should see "Teacher >= editing" in the "mod/forum:replypost" "table_row"
+    And I should see "Teacher < \"editing\"" in the "mod/forum:replypost" "table_row"
+    And I should see "Studier & 'learner'" in the "mod/forum:replypost" "table_row"
+    And I follow "Prohibit"
+    And "Teacher >= editing" "button" in the "Prohibit role" "dialogue" should be visible
+    And "Teacher < \"editing\"" "button" in the "Prohibit role" "dialogue" should be visible
+    And "Studier & 'learner'" "button" in the "Prohibit role" "dialogue" should be visible
index ca9a74f..cdcb081 100644 (file)
@@ -28,15 +28,16 @@ Feature: Verify that keyboard steps work as expected
     And I press the shift tab key
     And the focused element is "Username" "field"
 
-  @javascript
-  Scenario: Using the arrow keys allows me to navigate through menus
-    Given the following "users" exist:
-      | username | email                        | firstname | lastname |
-      | saffronr | saffron.rutledge@example.com | Saffron   | Rutledge |
-    And I log in as "saffronr"
-    And I click on "Saffron Rutledge" "link" in the ".usermenu" "css_element"
-    When I press the up key
-    Then the focused element is "Log out" "link"
+#  TODO: Uncomment the following when MDL-66979 is integrated.
+#  @javascript
+#  Scenario: Using the arrow keys allows me to navigate through menus
+#    Given the following "users" exist:
+#      | username | email                        | firstname | lastname |
+#      | saffronr | saffron.rutledge@example.com | Saffron   | Rutledge |
+#    And I log in as "saffronr"
+#    And I click on "Saffron Rutledge" "link" in the ".usermenu" "css_element"
+#    When I press the up key
+#    Then the focused element is "Log out" "link"
 
   @javascript
   Scenario: The escape key can be used to close a dialogue
index 6619bda..ff4b208 100644 (file)
@@ -10,16 +10,12 @@ Feature: Manage data storage purposes
     And I open the action menu in "region-main" "region"
     And I choose "Purposes" in the open action menu
     And I press "Add purpose"
-    And I set the field "Name" to "Purpose 1"
-    And I set the field "Description" to "Purpose 1 description"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Lawful bases" "form_row"
-    And I click on "Contract (GDPR Art. 6.1(b))" "list_item"
-    And I click on "Legal obligation (GDPR Art 6.1(c))" "list_item"
-    And I press the escape key
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Sensitive personal data processing reasons" "form_row"
-    And I click on "Explicit consent (GDPR Art. 9.2(a))" "list_item"
-    And I press the escape key
-    And I set the field "retentionperiodnumber" to "2"
+    And I set the following fields to these values:
+      | Name                                       | Purpose 1                                                      |
+      | Description                                | Purpose 1 description                                          |
+      | Lawful bases                               | Contract (GDPR Art. 6.1(b)),Legal obligation (GDPR Art 6.1(c)) |
+      | Sensitive personal data processing reasons | Explicit consent (GDPR Art. 9.2(a))                            |
+      | retentionperiodnumber                      | 2                                                              |
     When I press "Save"
     Then I should see "Purpose 1" in the "List of data purposes" "table"
     And I should see "Contract (GDPR Art. 6.1(b))" in the "Purpose 1" "table_row"
@@ -31,14 +27,13 @@ Feature: Manage data storage purposes
   Scenario: Update a data storage purpose
     Given I open the action menu in "Purpose 1" "table_row"
     And I choose "Edit" in the open action menu
-    And I set the field "Name" to "Purpose 1 edited"
-    And I set the field "Description" to "Purpose 1 description edited"
-    And I click on "Legal obligation (GDPR Art 6.1(c))" "text" in the ".form-autocomplete-selection" "css_element"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Lawful bases" "form_row"
-    And I click on "Vital interests (GDPR Art. 6.1(d))" "list_item"
-    And I press the escape key
-    And I set the field "retentionperiodnumber" to "3"
-    And I click on "protected" "checkbox"
+    And I set the following fields to these values:
+      | Name                                       | Purpose 1 edited                                                |
+      | Description                                | Purpose 1 description edited                                    |
+      | Lawful bases                               | Contract (GDPR Art. 6.1(b)), Vital interests (GDPR Art. 6.1(d)) |
+      | Sensitive personal data processing reasons | Explicit consent (GDPR Art. 9.2(a))                             |
+      | retentionperiodnumber                      | 3                                                               |
+      | protected                                  | 1                                                               |
     When I press "Save changes"
     Then I should see "Purpose 1 edited" in the "List of data purposes" "table"
     And I should see "Purpose 1 description edited" in the "Purpose 1 edited" "table_row"
index 55c085a..9f3dc07 100644 (file)
@@ -53,9 +53,7 @@ Feature: Manage plearning plan
     And I follow "Home"
     And I navigate to "Competencies > Learning plan templates" in site administration
     And I click on ".template-cohorts" "css_element" in the "Science template cohort" "table_row"
-    And I click on ".form-autocomplete-downarrow" "css_element"
-    And I click on "cohort plan" item in the autocomplete list
-    And I press the escape key
+    And I set the field "Select cohorts to sync" to "cohort plan"
     When I click on "Add cohorts" "button"
     Then I should see "2 learning plans were created."
     And I follow "Learning plan templates"
index d3adb18..c3ab30f 100644 (file)
@@ -1,5 +1,10 @@
 This files describes API changes in /admin/*.
 
+=== 3.11 ===
+
+* New admin setting admin_setting_encryptedpassword allows passwords in admin settings to be
+  encrypted (with the new \core\encryption API) so that even the admin cannot read them.
+
 === 3.9 ===
 
 * The following functions, previously used (exclusively) by upgrade steps are not available anymore because of the upgrade cleanup performed for this version. See MDL-65809 for more info:
index 7a958f9..6908c19 100644 (file)
@@ -106,21 +106,10 @@ abstract class base {
     }
 
     /**
-     * Returns the list of analysable elements available on the site.
-     *
-     * \core_analytics\local\analyser\by_course and \core_analytics\local\analyser\sitewide are implementing
-     * this method returning site courses (by_course) and the whole system (sitewide) as analysables.
-     *
-     * @todo MDL-65284 This will be removed in Moodle 3.11
-     * @deprecated
-     * @see get_analysables_iterator
-     * @throws  \coding_exception
-     * @return \core_analytics\analysable[] Array of analysable elements using the analysable id as array key.
+     * @deprecated since Moodle 3.7
      */
     public function get_analysables() {
-        // This function should only be called from get_analysables_iterator and we keep it here until Moodle 3.11
-        // for backwards compatibility.
-        throw new \coding_exception('This method is deprecated in favour of get_analysables_iterator.');
+        throw new \coding_exception('get_analysables() method has been removed and cannot be used any more.');
     }
 
     /**
@@ -134,15 +123,7 @@ abstract class base {
      * @param \context[] $contexts Only analysables that depend on the provided contexts. All analysables in the system if empty.
      * @return \Iterator
      */
-    public function get_analysables_iterator(?string $action = null, array $contexts = []) {
-
-        debugging('Please overwrite get_analysables_iterator with your own implementation, we only keep this default
-            implementation for backwards compatibility purposes with get_analysables(). note that $action param will
-            be ignored so the analysable elements will be processed using get_analysables order, regardless of the
-            last time they were processed.');
-
-        return new \ArrayIterator($this->get_analysables());
-    }
+    abstract public function get_analysables_iterator(?string $action = null, array $contexts = []);
 
     /**
      * This function returns this analysable list of samples.
index 8bbdf4d..4216bbe 100644 (file)
@@ -603,8 +603,8 @@ class manager {
      */
     public static function add_builtin_models() {
 
-        debugging('core_analytics\manager::add_builtin_models() has been deprecated. Core models are now automatically '.
-            'updated according to their declaration in the lib/db/analytics.php file.', DEBUG_DEVELOPER);
+        throw new \coding_exception('core_analytics\manager::add_builtin_models() has been removed. Core models ' .
+                        'are now automatically updated according to their declaration in the lib/db/analytics.php file.');
     }
 
     /**
index 816a253..d68ed78 100644 (file)
@@ -55,10 +55,7 @@ Feature: Manage analytics models
     And I choose "Create model" in the open action menu
     And I set the field "Enabled" to "Enable"
     And I select "__core_course__analytics__target__course_completion" from the "target" singleselect
-    And I open the autocomplete suggestions list
-    And I click on "Read actions amount" item in the autocomplete list
-    And I open the autocomplete suggestions list
-    And I click on "Any write action in the course" item in the autocomplete list
+    And I set the field "Indicators" to "Read actions amount, Any write action in the course"
     And I select "__core__analytics__time_splitting__single_range" from the "timesplitting" singleselect
     And I press "Save changes"
     Then I should see "No predictions available yet" in the "Students at risk of not meeting the course completion conditions" "table_row"
index 9501afb..b23200c 100644 (file)
@@ -2,8 +2,15 @@ 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.
+* Final deprecation add_builtin_models. Method has been removed. The functionality
+  has been replaced with automatic update of models provided by the core moodle component.
+  There is no need to call this method explicitly any more. Instead, adding new models can be achieved
+  by updating the lib/db/analytics.php file and bumping the core version.
+* Final deprecation - get_analysables(). Please see get_analysables_interator() instead.
+  get_analysables_iterator() needs to be overridden by the child class.
 
 === 3.8 ===
 
index 19a142d..46a6436 100644 (file)
@@ -82,16 +82,17 @@ class restore_quiz_decode_testcase extends \core_privacy\tests\provider_testcase
 
         $newcm = duplicate_module($course, get_fast_modinfo($course)->get_cm($quiz->cmid));
 
-        $sql = "SELECT qa.answer
+        $sql = "SELECT qa.id, qa.answer
                   FROM {quiz} q
              LEFT JOIN {quiz_slots} qs ON qs.quizid = q.id
              LEFT JOIN {question_answers} qa ON qa.question = qs.questionid
                  WHERE q.id = :quizid";
         $params = array('quizid' => $newcm->instance);
-        $answers = $DB->get_fieldset_sql($sql, $params);
-        $this->assertEquals($CFG->wwwroot . '/course/view.php?id=' . $course->id, $answers[0]);
-        $this->assertEquals($CFG->wwwroot . '/mod/quiz/view.php?id=' . $quiz->cmid, $answers[1]);
-        $this->assertEquals($CFG->wwwroot . '/grade/report/index.php?id=' . $quiz->cmid, $answers[2]);
-        $this->assertEquals($CFG->wwwroot . '/mod/quiz/index.php?id=' . $quiz->cmid, $answers[3]);
+        $answers = $DB->get_records_sql_menu($sql, $params);
+
+        $this->assertEquals($CFG->wwwroot . '/course/view.php?id=' . $course->id, $answers[$firstanswer->id]);
+        $this->assertEquals($CFG->wwwroot . '/mod/quiz/view.php?id=' . $quiz->cmid, $answers[$secondanswer->id]);
+        $this->assertEquals($CFG->wwwroot . '/grade/report/index.php?id=' . $quiz->cmid, $answers[$thirdanswer->id]);
+        $this->assertEquals($CFG->wwwroot . '/mod/quiz/index.php?id=' . $quiz->cmid, $answers[$fourthanswer->id]);
     }
 }
index 8e18c99..17ed845 100644 (file)
Binary files a/blocks/myoverview/amd/build/view.min.js and b/blocks/myoverview/amd/build/view.min.js differ
index 723993f..e238948 100644 (file)
Binary files a/blocks/myoverview/amd/build/view.min.js.map and b/blocks/myoverview/amd/build/view.min.js.map differ
index 524c4ba..1c92361 100644 (file)
@@ -543,11 +543,9 @@ function(
 
         // Filter out all pagination options which are too large for the amount of courses user is enrolled in.
         var totalCourseCount = parseInt(root.find(Selectors.courseView.region).attr('data-totalcoursecount'), 10);
-        if (totalCourseCount) {
-            itemsPerPage = itemsPerPage.filter(function(pagingOption) {
-                return pagingOption.value < totalCourseCount;
-            });
-        }
+        itemsPerPage = itemsPerPage.filter(function(pagingOption) {
+            return pagingOption.value < totalCourseCount || pagingOption.value === 0;
+        });
 
         var filters = getFilterValues(root);
         var config = $.extend({}, DEFAULT_PAGED_CONTENT_CONFIG);
@@ -602,7 +600,8 @@ function(
                                 pageCourses = $.merge(loadedPages[currentPage].courses, courses.slice(0, nextPageStart));
                             }
                         } else {
-                            nextPageStart = pageData.limit;
+                            // When the page limit is zero, there is only one page of courses, no start for next page.
+                            nextPageStart = pageData.limit || false;
                             pageCourses = (pageData.limit > 0) ? courses.slice(0, pageData.limit) : courses;
                         }
 
@@ -611,8 +610,8 @@ function(
                             courses: pageCourses
                         };
 
-                        // Set up the next page
-                        var remainingCourses = nextPageStart ? courses.slice(nextPageStart, courses.length) : [];
+                        // Set up the next page (if there is more than one page).
+                        var remainingCourses = nextPageStart !== false ? courses.slice(nextPageStart, courses.length) : [];
                         if (remainingCourses.length) {
                             loadedPages[currentPage + 1] = {
                                 courses: remainingCourses
index c290c98..b0236fd 100644 (file)
@@ -202,8 +202,9 @@ class main implements renderable, templatable {
         // Check and remember the given view.
         $this->view = $view ? $view : BLOCK_MYOVERVIEW_VIEW_CARD;
 
-        // Check and remember the given page size.
-        if ($paging == BLOCK_MYOVERVIEW_PAGING_ALL) {
+        // Check and remember the given page size, `null` indicates no page size set
+        // while a `0` indicates a paging size of `All`.
+        if (!is_null($paging) && $paging == BLOCK_MYOVERVIEW_PAGING_ALL) {
             $this->paging = BLOCK_MYOVERVIEW_PAGING_ALL;
         } else {
             $this->paging = $paging ? $paging : BLOCK_MYOVERVIEW_PAGING_12;
index a192c3d..fef9a8b 100644 (file)
@@ -7,30 +7,30 @@ Feature: The my overview block allows users to persistence of their page limits
       | student1 | Student   | X        | student1@example.com | S1       |
     And the following "courses" exist:
       | fullname | shortname | category |
-      | Course 1 | C1        | 0        |
-      | Course 2 | C2        | 0        |
-      | Course 3 | C3        | 0        |
-      | Course 4 | C4        | 0        |
-      | Course 5 | C5        | 0        |
-      | Course 6 | C6        | 0        |
-      | Course 7 | C7        | 0        |
-      | Course 8 | C8        | 0        |
-      | Course 9 | C9        | 0        |
+      | Course 1 | C01        | 0        |
+      | Course 2 | C02        | 0        |
+      | Course 3 | C03        | 0        |
+      | Course 4 | C04        | 0        |
+      | Course 5 | C05        | 0        |
+      | Course 6 | C06        | 0        |
+      | Course 7 | C07        | 0        |
+      | Course 8 | C08        | 0        |
+      | Course 9 | C09        | 0        |
       | Course 10 | C10        | 0        |
       | Course 11 | C11        | 0        |
       | Course 12 | C12        | 0        |
       | Course 13 | C13        | 0        |
     And the following "course enrolments" exist:
       | user | course | role |
-      | student1 | C1 | student |
-      | student1 | C2 | student |
-      | student1 | C3 | student |
-      | student1 | C4 | student |
-      | student1 | C5 | student |
-      | student1 | C6 | student |
-      | student1 | C7 | student |
-      | student1 | C8 | student |
-      | student1 | C9 | student |
+      | student1 | C01 | student |
+      | student1 | C02 | student |
+      | student1 | C03 | student |
+      | student1 | C04 | student |
+      | student1 | C05 | student |
+      | student1 | C06 | student |
+      | student1 | C07 | student |
+      | student1 | C08 | student |
+      | student1 | C09 | student |
       | student1 | C10 | student |
       | student1 | C11 | student |
       | student1 | C12 | student |
@@ -38,8 +38,8 @@ Feature: The my overview block allows users to persistence of their page limits
 
   Scenario: Toggle the page limit between page reloads
     Given I log in as "student1"
-    When I click on "[data-toggle='dropdown']" "css_element" in the "Course overview" "block"
-    And I click on "All" "link"
+    When I click on "[data-action='limit-toggle']" "css_element" in the "Course overview" "block"
+    And I click on "All" "link" in the ".dropdown-menu.show" "css_element"
     Then I should see "Course 13"
     And I reload the page
     Then I should see "Course 13"
@@ -47,8 +47,8 @@ Feature: The my overview block allows users to persistence of their page limits
 
   Scenario: Toggle the page limit between grouping changes
     Given I log in as "student1"
-    When I click on "[data-toggle='dropdown']" "css_element" in the "Course overview" "block"
-    And I click on "All" "link"
+    When I click on "[data-action='limit-toggle']" "css_element" in the "Course overview" "block"
+    And I click on "All" "link" in the ".dropdown-menu.show" "css_element"
     And I click on "All (except removed from view)" "button" in the "Course overview" "block"
     And I click on "In progress" "link" in the "Course overview" "block"
     Then I should see "Course 13"
diff --git a/blocks/myoverview/tests/behat/block_myoverview_pagination.feature b/blocks/myoverview/tests/behat/block_myoverview_pagination.feature
new file mode 100644 (file)
index 0000000..9792252
--- /dev/null
@@ -0,0 +1,191 @@
+@block @block_myoverview @javascript
+Feature: My overview block pagination
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email                | idnumber |
+      | student1 | Student   | X        | student1@example.com | S1       |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 01 | C1       | 0        |
+      | Course 02 | C2       | 0        |
+      | Course 03 | C3       | 0        |
+      | Course 04 | C4       | 0        |
+      | Course 05 | C5       | 0        |
+      | Course 06 | C6       | 0        |
+      | Course 07 | C7       | 0        |
+      | Course 08 | C8       | 0        |
+      | Course 09 | C9       | 0        |
+      | Course 10 | C10      | 0        |
+      | Course 11 | C11      | 0        |
+      | Course 12 | C12      | 0        |
+      | Course 13 | C13      | 0        |
+      | Course 14 | C14      | 0        |
+      | Course 15 | C15      | 0        |
+      | Course 16 | C16      | 0        |
+      | Course 17 | C17      | 0        |
+      | Course 18 | C18      | 0        |
+      | Course 19 | C19      | 0        |
+      | Course 20 | C20      | 0        |
+      | Course 21 | C21      | 0        |
+      | Course 22 | C22      | 0        |
+      | Course 23 | C23      | 0        |
+      | Course 24 | C24      | 0        |
+      | Course 25 | C25      | 0        |
+
+  Scenario: The pagination controls should be hidden if I am not enrolled in any courses
+    When I log in as "student1"
+    Then I should see "No courses" in the "Course overview" "block"
+    And I should not see "Show" in the "Course overview" "block"
+    And ".block_myoverview .dropdown-menu.show" "css_element" should not be visible
+    And ".block_myoverview [data-control='next']" "css_element" should not be visible
+    And ".block_myoverview [data-control='previous']" "css_element" should not be visible
+    And I log out
+
+  Scenario: The pagination controls should be hidden if I am enrolled in 12 courses or less
+    Given the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+      | student1 | C2 | student |
+      | student1 | C3 | student |
+      | student1 | C4 | student |
+      | student1 | C5 | student |
+      | student1 | C6 | student |
+      | student1 | C7 | student |
+      | student1 | C8 | student |
+      | student1 | C9 | student |
+      | student1 | C10 | student |
+      | student1 | C11 | student |
+      | student1 | C12 | student |
+    When I log in as "student1"
+    Then I should not see "Show" in the "Course overview" "block"
+    And ".block_myoverview .dropdown-menu.show" "css_element" should not be visible
+    And ".block_myoverview [data-control='next']" "css_element" should not be visible
+    And ".block_myoverview [data-control='previous']" "css_element" should not be visible
+    And I log out
+
+  Scenario: The default pagination should be 12 courses
+    Given the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+      | student1 | C2 | student |
+      | student1 | C3 | student |
+      | student1 | C4 | student |
+      | student1 | C5 | student |
+      | student1 | C6 | student |
+      | student1 | C7 | student |
+      | student1 | C8 | student |
+      | student1 | C9 | student |
+      | student1 | C10 | student |
+      | student1 | C11 | student |
+      | student1 | C12 | student |
+      | student1 | C13 | student |
+    When I log in as "student1"
+    Then I should see "12" in the "[data-action='limit-toggle']" "css_element"
+    And I log out
+
+  Scenario: I should only see pagination limit options less than total number of enrolled courses
+    Given the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+      | student1 | C2 | student |
+      | student1 | C3 | student |
+      | student1 | C4 | student |
+      | student1 | C5 | student |
+      | student1 | C6 | student |
+      | student1 | C7 | student |
+      | student1 | C8 | student |
+      | student1 | C9 | student |
+      | student1 | C10 | student |
+      | student1 | C11 | student |
+      | student1 | C12 | student |
+      | student1 | C13 | student |
+    And I log in as "student1"
+    When I click on "[data-action='limit-toggle']" "css_element" in the "Course overview" "block"
+    Then I should see "All" in the ".dropdown-menu.show" "css_element"
+    And I should see "12" in the ".dropdown-menu.show" "css_element"
+    And ".block_myoverview [data-control='next']" "css_element" should be visible
+    And ".block_myoverview [data-control='previous']" "css_element" should be visible
+    But I should not see "24" in the ".block_myoverview .dropdown-menu.show" "css_element"
+    And I log out
+
+  Scenario: Previous page button should be disabled when on the first page of courses
+    Given the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+      | student1 | C2 | student |
+      | student1 | C3 | student |
+      | student1 | C4 | student |
+      | student1 | C5 | student |
+      | student1 | C6 | student |
+      | student1 | C7 | student |
+      | student1 | C8 | student |
+      | student1 | C9 | student |
+      | student1 | C10 | student |
+      | student1 | C11 | student |
+      | student1 | C12 | student |
+      | student1 | C13 | student |
+    When I log in as "student1"
+    Then the "class" attribute of ".block_myoverview [data-control='previous']" "css_element" should contain "disabled"
+    And I log out
+
+  Scenario: Next page button should be disabled when on the last page of courses
+    Given the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+      | student1 | C2 | student |
+      | student1 | C3 | student |
+      | student1 | C4 | student |
+      | student1 | C5 | student |
+      | student1 | C6 | student |
+      | student1 | C7 | student |
+      | student1 | C8 | student |
+      | student1 | C9 | student |
+      | student1 | C10 | student |
+      | student1 | C11 | student |
+      | student1 | C12 | student |
+      | student1 | C13 | student |
+    When I log in as "student1"
+    And I click on "[data-control='next']" "css_element" in the "Course overview" "block"
+    And I wait until ".block_myoverview [data-control='next']" "css_element" exists
+    Then the "class" attribute of ".block_myoverview [data-control='next']" "css_element" should contain "disabled"
+    And I log out
+
+  Scenario: Next and previous page buttons should both be enabled when not on last or first page of courses
+    Given the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+      | student1 | C2 | student |
+      | student1 | C3 | student |
+      | student1 | C4 | student |
+      | student1 | C5 | student |
+      | student1 | C6 | student |
+      | student1 | C7 | student |
+      | student1 | C8 | student |
+      | student1 | C9 | student |
+      | student1 | C10 | student |
+      | student1 | C11 | student |
+      | student1 | C12 | student |
+      | student1 | C13 | student |
+      | student1 | C14 | student |
+      | student1 | C15 | student |
+      | student1 | C16 | student |
+      | student1 | C17 | student |
+      | student1 | C18 | student |
+      | student1 | C19 | student |
+      | student1 | C20 | student |
+      | student1 | C21 | student |
+      | student1 | C22 | student |
+      | student1 | C23 | student |
+      | student1 | C24 | student |
+      | student1 | C25 | student |
+    When I log in as "student1"
+    And I click on "[data-control='next']" "css_element" in the "Course overview" "block"
+    And I wait until ".block_myoverview [data-control='next']" "css_element" exists
+    Then the "class" attribute of ".block_myoverview [data-control='next']" "css_element" should not contain "disabled"
+    And the "class" attribute of ".block_myoverview [data-control='previous']" "css_element" should not contain "disabled"
+    And I should see "Course 13" in the "Course overview" "block"
+    And I should see "Course 24" in the "Course overview" "block"
+    But I should not see "Course 12" in the "Course overview" "block"
+    And I should not see "Course 25" in the "Course overview" "block"
+    And I log out
index c483c28..a33a10e 100644 (file)
@@ -52,9 +52,9 @@
             <a href="{{url}}"
                title={{#quote}}{{{name}}}{{/quote}}
                aria-label='{{#str}} ariaeventlistitem, block_timeline, { "name": {{#quote}}{{{name}}}{{/quote}}, "course": {{#quote}}{{{course.fullnamedisplay}}}{{/quote}}, "date": "{{#userdate}} {{timesort}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}}" } {{/str}}'
-            ><h6 class="event-name text-truncate mb-0">{{#quote}}{{{name}}}{{/quote}}</h6></a>
+            ><h6 class="event-name text-truncate mb-0">{{{name}}}</h6></a>
             {{#course.fullnamedisplay}}
-            <small class="text-muted text-truncate mb-0">{{#quote}}{{{course.fullnamedisplay}}}{{/quote}}</small>
+            <small class="text-muted text-truncate mb-0">{{{course.fullnamedisplay}}}</small>
             {{/course.fullnamedisplay}}
             {{#action.actionable}}
             <h6 class="mb-0 pt-2">
index e943e9c..bfa2368 100644 (file)
@@ -116,7 +116,11 @@ class calendar_event_exporter extends event_exporter_base {
         $values['islastday'] = false;
         $today = $this->related['type']->timestamp_to_date_array($this->related['today']);
 
-        $values['popupname'] = $this->event->get_name();
+        if ($hascourse) {
+            $values['popupname'] = external_format_string($this->event->get_name(), \context_course::instance($course->id), true);
+        } else {
+            $values['popupname'] = external_format_string($this->event->get_name(), \context_system::instance(), true);
+        }
 
         $times = $this->event->get_times();
         if ($duration = $times->get_duration()) {
index b8c5d51..8933024 100644 (file)
@@ -727,6 +727,22 @@ $CFG->admin = 'admin';
 //
 // $CFG->maxcoursesincategory = 10000;
 //
+// Admin setting encryption
+//
+//      $CFG->secretdataroot = '/var/www/my_secret_folder';
+//
+// Location to store encryption keys. By default this is $CFG->dataroot/secret; set this if
+// you want to use a different location for increased security (e.g. if too many people have access
+// to the main dataroot, or if you want to avoid using shared storage). Your web server user needs
+// read access to this location, and write access unless you manually create the keys.
+//
+//      $CFG->nokeygeneration = false;
+//
+// If you change this to true then the server will give an error if keys don't exist, instead of
+// automatically generating them. This is only needed if you want to ensure that keys are consistent
+// across a cluster when not using shared storage. If you stop the server generating keys, you will
+// need to manually generate them by running 'php admin/cli/generate_key.php'.
+
 //=========================================================================
 // 7. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
 //=========================================================================
index d517d5b..8028476 100644 (file)
Binary files a/course/amd/build/local/activitychooser/dialogue.min.js and b/course/amd/build/local/activitychooser/dialogue.min.js differ
index 740fe3f..c782a84 100644 (file)
Binary files a/course/amd/build/local/activitychooser/dialogue.min.js.map and b/course/amd/build/local/activitychooser/dialogue.min.js.map differ
index 45f8f4f..ea395d6 100644 (file)
Binary files a/course/amd/build/local/activitychooser/selectors.min.js and b/course/amd/build/local/activitychooser/selectors.min.js differ
index 13f9e9f..25b6d79 100644 (file)
Binary files a/course/amd/build/local/activitychooser/selectors.min.js.map and b/course/amd/build/local/activitychooser/selectors.min.js.map differ
index a3bde76..a7c7018 100644 (file)
@@ -216,7 +216,6 @@ const registerListenerEvents = (modal, mappedModules, partialFavourite, footerDa
         const firstChooserOption = sectionChooserOptions.querySelector(selectors.regions.chooserOption.container);
 
         toggleFocusableChooserOption(firstChooserOption, true);
-        initTabsKeyboardNavigation(body);
         initChooserOptionsKeyboardNavigation(body, mappedModules, sectionChooserOptions, modal);
 
         return body;
@@ -235,77 +234,6 @@ const registerListenerEvents = (modal, mappedModules, partialFavourite, footerDa
     .catch();
 };
 
-/**
- * Initialise the keyboard navigation controls for the tab list items.
- *
- * @method initTabsKeyboardNavigation
- * @param {HTMLElement} body Our modal that we are working with
- */
-const initTabsKeyboardNavigation = (body) => {
-    // Set up the tab handlers.
-    const favTabNav = body.querySelector(selectors.regions.favouriteTabNav);
-    const recommendedTabNav = body.querySelector(selectors.regions.recommendedTabNav);
-    const defaultTabNav = body.querySelector(selectors.regions.defaultTabNav);
-    const activityTabNav = body.querySelector(selectors.regions.activityTabNav);
-    const resourceTabNav = body.querySelector(selectors.regions.resourceTabNav);
-    const tabNavArray = [favTabNav, recommendedTabNav, defaultTabNav, activityTabNav, resourceTabNav];
-    tabNavArray.forEach((element) => {
-        return element.addEventListener('keydown', (e) => {
-            // The first visible navigation tab link.
-            const firstLink = e.target.parentElement.querySelector(selectors.elements.visibletabs);
-            // The last navigation tab link. It would always be the default activities tab link.
-            const lastLink = e.target.parentElement.lastElementChild;
-
-            if (e.keyCode === arrowRight) {
-                const nextLink = e.target.nextElementSibling;
-                if (nextLink === null) {
-                    e.target.tabIndex = -1;
-                    firstLink.tabIndex = 0;
-                    firstLink.focus();
-                } else if (nextLink.classList.contains('d-none')) {
-                    e.target.tabIndex = -1;
-                    lastLink.tabIndex = 0;
-                    lastLink.focus();
-                } else {
-                    e.target.tabIndex = -1;
-                    nextLink.tabIndex = 0;
-                    nextLink.focus();
-                }
-            }
-            if (e.keyCode === arrowLeft) {
-                const previousLink = e.target.previousElementSibling;
-                if (previousLink === null) {
-                    e.target.tabIndex = -1;
-                    lastLink.tabIndex = 0;
-                    lastLink.focus();
-                } else if (previousLink.classList.contains('d-none')) {
-                    e.target.tabIndex = -1;
-                    firstLink.tabIndex = 0;
-                    firstLink.focus();
-                } else {
-                    e.target.tabIndex = -1;
-                    previousLink.tabIndex = 0;
-                    previousLink.focus();
-                }
-            }
-            if (e.keyCode === home) {
-                e.target.tabIndex = -1;
-                firstLink.tabIndex = 0;
-                firstLink.focus();
-            }
-            if (e.keyCode === end) {
-                e.target.tabIndex = -1;
-                lastLink.tabIndex = 0;
-                lastLink.focus();
-            }
-            if (e.keyCode === space) {
-                e.preventDefault();
-                e.target.click();
-            }
-        });
-    });
-};
-
 /**
  * Initialise the keyboard navigation controls for the chooser options.
  *
index dd20e75..34e9adb 100644 (file)
@@ -52,10 +52,8 @@ export default {
         help: getDataSelector('region', 'help'),
         modules: getDataSelector('region', 'modules'),
         favouriteTabNav: getDataSelector('region', 'favourite-tab-nav'),
-        recommendedTabNav: getDataSelector('region', 'recommended-tab-nav'),
         defaultTabNav: getDataSelector('region', 'default-tab-nav'),
         activityTabNav: getDataSelector('region', 'activity-tab-nav'),
-        resourceTabNav: getDataSelector('region', 'resources-tab-nav'),
         favouriteTab: getDataSelector('region', 'favourites'),
         recommendedTab: getDataSelector('region', 'recommended'),
         defaultTab: getDataSelector('region', 'default'),
index dede44d..c30da88 100644 (file)
@@ -102,8 +102,13 @@ class course_handler extends \core_customfield\handler {
                     has_capability('moodle/course:changelockedcustomfields', $context));
         } else {
             $context = $this->get_parent_context();
-            return (!$field->get_configdata_property('locked') ||
-                guess_if_creator_will_have_course_capability('moodle/course:changelockedcustomfields', $context));
+            if ($context->contextlevel == CONTEXT_SYSTEM) {
+                return (!$field->get_configdata_property('locked') ||
+                    has_capability('moodle/course:changelockedcustomfields', $context));
+            } else {
+                return (!$field->get_configdata_property('locked') ||
+                    guess_if_creator_will_have_course_capability('moodle/course:changelockedcustomfields', $context));
+            }
         }
     }
 
index c2655fc..3506893 100644 (file)
@@ -38,7 +38,7 @@ Feature: Tagging courses
     And I expand all fieldsets
     Then "Mathematics" "autocomplete_suggestions" should exist
     And I set the following fields to these values:
-      | Tags | Algebra |
+      | Tags | Mathematics, Algebra |
     And I press "Save and display"
     And I am on "Course 2" course homepage
     And I navigate to "Edit settings" in current page administration
@@ -75,7 +75,7 @@ Feature: Tagging courses
     And I navigate to "Course tags" in current page administration
     Then I should see "Mathematics" in the ".form-autocomplete-selection" "css_element"
     And I set the following fields to these values:
-      | Tags | Algebra |
+      | Tags | Mathematics, Algebra |
     And I press "Save changes"
     And I am on "Course 2" course homepage
     And I navigate to "Course tags" in current page administration
index 1b3c581..00f7c81 100644 (file)
@@ -31,9 +31,11 @@ Feature: Rename roles within a course
     And "Learner" "button" should exist
     And I navigate to course participants
     And I set the field "type" in the "Filter 1" "fieldset" to "Roles"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I should see "Tutor (Non-editing teacher)" in the ".form-autocomplete-suggestions" "css_element"
+    And I open the autocomplete suggestions list in the "Filter 1" "fieldset"
     And I should see "Learner (Student)" in the ".form-autocomplete-suggestions" "css_element"
+    And I press the escape key
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Tutor (Non-editing teacher)"
+
     And I click on "Student 1's role assignments" "link"
     And I click on ".form-autocomplete-downarrow" "css_element" in the "Student 1" "table_row"
     And "Tutor (Non-editing teacher)" "autocomplete_suggestions" should exist
@@ -53,6 +55,6 @@ Feature: Rename roles within a course
     And "Learner" "button" should not exist
     And I navigate to course participants
     And I set the field "type" in the "Filter 1" "fieldset" to "Roles"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
+    And I open the autocomplete suggestions list in the "Filter 1" "fieldset"
     And I should see "Non-editing teacher" in the ".form-autocomplete-suggestions" "css_element"
     And I should see "Student" in the ".form-autocomplete-suggestions" "css_element"
index 8c08a74..50cd5dc 100644 (file)
@@ -36,7 +36,7 @@ if ($ADMIN->fulltree) {
         $student = get_archetype_roles('student');
         $student = reset($student);
         $settings->add(new admin_setting_configselect('enrol_cohort/roleid',
-            get_string('defaultrole', 'role'), '', $student->id, $options));
+            get_string('defaultrole', 'role'), '', $student->id ?? null, $options));
 
         $options = array(
             ENROL_EXT_REMOVED_UNENROL        => get_string('extremovedunenrol', 'enrol'),
index 28c7504..92d0833 100644 (file)
@@ -86,7 +86,11 @@ if ($ADMIN->fulltree) {
         $options = get_default_enrol_roles(context_system::instance());
         $student = get_archetype_roles('student');
         $student = reset($student);
-        $settings->add(new admin_setting_configselect('enrol_database/defaultrole', get_string('defaultrole', 'enrol_database'), get_string('defaultrole_desc', 'enrol_database'), $student->id, $options));
+        $settings->add(new admin_setting_configselect('enrol_database/defaultrole',
+            get_string('defaultrole', 'enrol_database'),
+            get_string('defaultrole_desc', 'enrol_database'),
+            $student->id ?? null,
+            $options));
     }
 
     $settings->add(new admin_setting_configcheckbox('enrol_database/ignorehiddencourses', get_string('ignorehiddencourses', 'enrol_database'), get_string('ignorehiddencourses_desc', 'enrol_database'), 0));
index d0fbaa9..5189a3a 100644 (file)
@@ -679,28 +679,34 @@ class core_enrol_external extends external_api {
      */
     public static function get_enrolled_users_parameters() {
         return new external_function_parameters(
-            array(
+            [
                 'courseid' => new external_value(PARAM_INT, 'course id'),
                 'options'  => new external_multiple_structure(
                     new external_single_structure(
-                        array(
+                        [
                             'name'  => new external_value(PARAM_ALPHANUMEXT, 'option name'),
                             'value' => new external_value(PARAM_RAW, 'option value')
-                        )
+                        ]
                     ), 'Option names:
                             * withcapability (string) return only users with this capability. This option requires \'moodle/role:review\' on the course context.
                             * groupid (integer) return only users in this group id. If the course has groups enabled and this param
                                                 isn\'t defined, returns all the viewable users.
                                                 This option requires \'moodle/site:accessallgroups\' on the course context if the
                                                 user doesn\'t belong to the group.
-                            * onlyactive (integer) return only users with active enrolments and matching time restrictions. This option requires \'moodle/course:enrolreview\' on the course context.
+                            * onlyactive (integer) return only users with active enrolments and matching time restrictions.
+                                                This option requires \'moodle/course:enrolreview\' on the course context.
+                                                Please note that this option can\'t
+                                                be used together with onlysuspended (only one can be active).
+                            * onlysuspended (integer) return only suspended users. This option requires
+                                            \'moodle/course:enrolreview\' on the course context. Please note that this option can\'t
+                                                be used together with onlyactive (only one can be active).
                             * userfields (\'string, string, ...\') return only the values of these user fields.
                             * limitfrom (integer) sql limit from.
                             * limitnumber (integer) maximum number of returned users.
                             * sortby (string) sort by id, firstname or lastname. For ordering like the site does, use siteorder.
                             * sortdirection (string) ASC or DESC',
-                            VALUE_DEFAULT, array()),
-            )
+                            VALUE_DEFAULT, []),
+            ]
         );
     }
 
@@ -714,7 +720,7 @@ class core_enrol_external extends external_api {
      *                               }
      * @return array An array of users
      */
-    public static function get_enrolled_users($courseid, $options = array()) {
+    public static function get_enrolled_users($courseid, $options = []) {
         global $CFG, $USER, $DB;
 
         require_once($CFG->dirroot . '/course/lib.php');
@@ -722,67 +728,71 @@ class core_enrol_external extends external_api {
 
         $params = self::validate_parameters(
             self::get_enrolled_users_parameters(),
-            array(
+            [
                 'courseid'=>$courseid,
                 'options'=>$options
-            )
+            ]
         );
         $withcapability = '';
         $groupid        = 0;
         $onlyactive     = false;
-        $userfields     = array();
+        $onlysuspended  = false;
+        $userfields     = [];
         $limitfrom = 0;
         $limitnumber = 0;
         $sortby = 'us.id';
-        $sortparams = array();
+        $sortparams = [];
         $sortdirection = 'ASC';
         foreach ($options as $option) {
             switch ($option['name']) {
-            case 'withcapability':
-                $withcapability = $option['value'];
-                break;
-            case 'groupid':
-                $groupid = (int)$option['value'];
-                break;
-            case 'onlyactive':
-                $onlyactive = !empty($option['value']);
-                break;
-            case 'userfields':
-                $thefields = explode(',', $option['value']);
-                foreach ($thefields as $f) {
-                    $userfields[] = clean_param($f, PARAM_ALPHANUMEXT);
-                }
-                break;
-            case 'limitfrom' :
-                $limitfrom = clean_param($option['value'], PARAM_INT);
-                break;
-            case 'limitnumber' :
-                $limitnumber = clean_param($option['value'], PARAM_INT);
-                break;
-            case 'sortby':
-                $sortallowedvalues = array('id', 'firstname', 'lastname', 'siteorder');
-                if (!in_array($option['value'], $sortallowedvalues)) {
-                    throw new invalid_parameter_exception('Invalid value for sortby parameter (value: ' . $option['value'] . '),' .
-                        'allowed values are: ' . implode(',', $sortallowedvalues));
-                }
-                if ($option['value'] == 'siteorder') {
-                    list($sortby, $sortparams) = users_order_by_sql('us');
-                } else {
-                    $sortby = 'us.' . $option['value'];
-                }
-                break;
-            case 'sortdirection':
-                $sortdirection = strtoupper($option['value']);
-                $directionallowedvalues = array('ASC', 'DESC');
-                if (!in_array($sortdirection, $directionallowedvalues)) {
-                    throw new invalid_parameter_exception('Invalid value for sortdirection parameter
+                case 'withcapability':
+                    $withcapability = $option['value'];
+                    break;
+                case 'groupid':
+                    $groupid = (int)$option['value'];
+                    break;
+                case 'onlyactive':
+                    $onlyactive = !empty($option['value']);
+                    break;
+                case 'onlysuspended':
+                    $onlysuspended = !empty($option['value']);
+                    break;
+                case 'userfields':
+                    $thefields = explode(',', $option['value']);
+                    foreach ($thefields as $f) {
+                        $userfields[] = clean_param($f, PARAM_ALPHANUMEXT);
+                    }
+                    break;
+                case 'limitfrom' :
+                    $limitfrom = clean_param($option['value'], PARAM_INT);
+                    break;
+                case 'limitnumber' :
+                    $limitnumber = clean_param($option['value'], PARAM_INT);
+                    break;
+                case 'sortby':
+                    $sortallowedvalues = ['id', 'firstname', 'lastname', 'siteorder'];
+                    if (!in_array($option['value'], $sortallowedvalues)) {
+                        throw new invalid_parameter_exception('Invalid value for sortby parameter (value: ' .
+                            $option['value'] . '), allowed values are: ' . implode(',', $sortallowedvalues));
+                    }
+                    if ($option['value'] == 'siteorder') {
+                        list($sortby, $sortparams) = users_order_by_sql('us');
+                    } else {
+                        $sortby = 'us.' . $option['value'];
+                    }
+                    break;
+                case 'sortdirection':
+                    $sortdirection = strtoupper($option['value']);
+                    $directionallowedvalues = ['ASC', 'DESC'];
+                    if (!in_array($sortdirection, $directionallowedvalues)) {
+                        throw new invalid_parameter_exception('Invalid value for sortdirection parameter
                         (value: ' . $sortdirection . '),' . 'allowed values are: ' . implode(',', $directionallowedvalues));
-                }
-                break;
+                    }
+                    break;
             }
         }
 
-        $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST);
+        $course = $DB->get_record('course', ['id' => $courseid], '*', MUST_EXIST);
         $coursecontext = context_course::instance($courseid, IGNORE_MISSING);
         if ($courseid == SITEID) {
             $context = context_system::instance();
@@ -809,11 +819,12 @@ class core_enrol_external extends external_api {
             require_capability('moodle/site:accessallgroups', $coursecontext);
         }
         // to overwrite this option, you need course:enrolereview permission
-        if ($onlyactive) {
+        if ($onlyactive || $onlysuspended) {
             require_capability('moodle/course:enrolreview', $coursecontext);
         }
 
-        list($enrolledsql, $enrolledparams) = get_enrolled_sql($coursecontext, $withcapability, $groupid, $onlyactive);
+        list($enrolledsql, $enrolledparams) = get_enrolled_sql($coursecontext, $withcapability, $groupid, $onlyactive,
+        $onlysuspended);
         $ctxselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
         $ctxjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = u.id AND ctx.contextlevel = :contextlevel)";
         $enrolledparams['contextlevel'] = CONTEXT_USER;
@@ -829,7 +840,7 @@ class core_enrol_external extends external_api {
                 $enrolledparams = array_merge($enrolledparams, $groupparams);
             } else {
                 // User doesn't belong to any group, so he can't see any user. Return an empty array.
-                return array();
+                return [];
             }
         }
         $sql = "SELECT us.*, COALESCE(ul.timeaccess, 0) AS lastcourseaccess
@@ -845,7 +856,7 @@ class core_enrol_external extends external_api {
         $enrolledparams['courseid'] = $courseid;
 
         $enrolledusers = $DB->get_recordset_sql($sql, $enrolledparams, $limitfrom, $limitnumber);
-        $users = array();
+        $users = [];
         foreach ($enrolledusers as $user) {
             context_helper::preload_from_record($user);
             if ($userdetails = user_get_user_details($user, $course, $userfields)) {
@@ -865,7 +876,7 @@ class core_enrol_external extends external_api {
     public static function get_enrolled_users_returns() {
         return new external_multiple_structure(
             new external_single_structure(
-                array(
+                [
                     'id'    => new external_value(PARAM_INT, 'ID of the user'),
                     'username'    => new external_value(PARAM_RAW, 'Username policy is defined in Moodle security config', VALUE_OPTIONAL),
                     'firstname'   => new external_value(PARAM_NOTAGS, 'The first name(s) of the user', VALUE_OPTIONAL),
@@ -896,47 +907,47 @@ class core_enrol_external extends external_api {
                     'profileimageurl' => new external_value(PARAM_URL, 'User image profile URL - big version', VALUE_OPTIONAL),
                     'customfields' => new external_multiple_structure(
                         new external_single_structure(
-                            array(
+                            [
                                 'type'  => new external_value(PARAM_ALPHANUMEXT, 'The type of the custom field - text field, checkbox...'),
                                 'value' => new external_value(PARAM_RAW, 'The value of the custom field'),
                                 'name' => new external_value(PARAM_RAW, 'The name of the custom field'),
                                 'shortname' => new external_value(PARAM_RAW, 'The shortname of the custom field - to be able to build the field class in the code'),
-                            )
+                            ]
                         ), 'User custom fields (also known as user profil fields)', VALUE_OPTIONAL),
                     'groups' => new external_multiple_structure(
                         new external_single_structure(
-                            array(
+                            [
                                 'id'  => new external_value(PARAM_INT, 'group id'),
                                 'name' => new external_value(PARAM_RAW, 'group name'),
                                 'description' => new external_value(PARAM_RAW, 'group description'),
                                 'descriptionformat' => new external_format_value('description'),
-                            )
+                            ]
                         ), 'user groups', VALUE_OPTIONAL),
                     'roles' => new external_multiple_structure(
                         new external_single_structure(
-                            array(
+                            [
                                 'roleid'       => new external_value(PARAM_INT, 'role id'),
                                 'name'         => new external_value(PARAM_RAW, 'role name'),
                                 'shortname'    => new external_value(PARAM_ALPHANUMEXT, 'role shortname'),
                                 'sortorder'    => new external_value(PARAM_INT, 'role sortorder')
-                            )
+                            ]
                         ), 'user roles', VALUE_OPTIONAL),
                     'preferences' => new external_multiple_structure(
                         new external_single_structure(
-                            array(
+                            [
                                 'name'  => new external_value(PARAM_RAW, 'The name of the preferences'),
                                 'value' => new external_value(PARAM_RAW, 'The value of the custom field'),
-                            )
+                            ]
                     ), 'User preferences', VALUE_OPTIONAL),
                     'enrolledcourses' => new external_multiple_structure(
                         new external_single_structure(
-                            array(
+                            [
                                 'id'  => new external_value(PARAM_INT, 'Id of the course'),
                                 'fullname' => new external_value(PARAM_RAW, 'Fullname of the course'),
                                 'shortname' => new external_value(PARAM_RAW, 'Shortname of the course')
-                            )
+                            ]
                     ), 'Courses where the user is enrolled - limited by which courses the user is able to see', VALUE_OPTIONAL)
-                )
+                ]
             )
         );
     }
index 78cbbcd..549247c 100644 (file)
@@ -45,6 +45,10 @@ class enrol_fee_plugin extends enrol_plugin {
             $currencies[$c] = new lang_string($c, 'core_currencies');
         }
 
+        uasort($currencies, function($a, $b) {
+            return strcmp($a, $b);
+        });
+
         return $currencies;
     }
 
index 2f383b8..50d919e 100644 (file)
@@ -71,7 +71,7 @@ if ($ADMIN->fulltree) {
         $student = get_archetype_roles('student');
         $student = reset($student);
         $settings->add(new admin_setting_configselect('enrol_fee/roleid',
-            get_string('defaultrole', 'enrol_fee'), get_string('defaultrole_desc', 'enrol_fee'), $student->id, $options));
+            get_string('defaultrole', 'enrol_fee'), get_string('defaultrole_desc', 'enrol_fee'), $student->id ?? null, $options));
     }
 
     $settings->add(new admin_setting_configduration('enrol_fee/enrolperiod',
index 2056c19..8dcabcc 100644 (file)
@@ -63,7 +63,7 @@ if ($ADMIN->fulltree) {
         $student = get_archetype_roles('student');
         $student = reset($student);
         $settings->add(new admin_setting_configselect('enrol_manual/roleid',
-            get_string('defaultrole', 'role'), '', $student->id, $options));
+            get_string('defaultrole', 'role'), '', $student->id ?? null, $options));
     }
 
     $options = array(2 => get_string('coursestart'), 3 => get_string('today'), 4 => get_string('now', 'enrol_manual'));
index cd3c979..564f69f 100644 (file)
@@ -130,18 +130,17 @@ Feature: Teacher can search and enrol users one by one into the course
   Scenario: Searching for a non-existing user
     Given I navigate to course participants
     And I press "Enrol users"
-    And I set the field "Select users" to "qwertyuiop"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Select users" "form_row"
+    And I click on "Select users" "field"
+    And I type "qwertyuiop"
     Then I should see "No suggestions"
 
   @javascript
   Scenario: If there are less than 100 matching users, all are displayed for selection
     Given I navigate to course participants
     And I press "Enrol users"
-    When I set the field "Select users" to "example.com"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Select users" "form_row"
-    And I click on "Student 099" item in the autocomplete list
-    Then I should see "Student 099"
+    When I click on "Select users" "field"
+    And I type "example.com"
+    Then "Student 099" "autocomplete_suggestions" should exist
 
   @javascript
   Scenario: If there are more than 100 matching users, inform there are too many.
@@ -151,8 +150,8 @@ Feature: Teacher can search and enrol users one by one into the course
       | student101  | Student   | 101      | student101@example.com  |
     And I navigate to course participants
     And I press "Enrol users"
-    When I set the field "Select users" to "example.com"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Select users" "form_row"
+    When I click on "Select users" "field"
+    And I type "example.com"
     Then I should see "Too many users (>100) to show"
 
   @javascript
@@ -161,8 +160,8 @@ Feature: Teacher can search and enrol users one by one into the course
       | maxusersperpage | 5 |
     And I navigate to course participants
     And I press "Enrol users"
-    When I set the field "Select users" to "student00"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Select users" "form_row"
+    When I click on "Select users" "field"
+    And I type "student00"
     Then I should see "Too many users (>5) to show"
 
   @javascript
@@ -177,18 +176,18 @@ Feature: Teacher can search and enrol users one by one into the course
     When I am on "Course 001" course homepage
     Then I navigate to course participants
     And I press "Enrol users"
-    When I set the field "Select users" to "student100@example.com"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Select users" "form_row"
+    And I click on "Select users" "field"
+    And I type "student100@example.com"
     Then I should see "student100@example.com, CITY1, GB, 1234567892, 1234567893, ABC1, ABC2"
     # Remove identity field in setting User policies
     And the following config values are set as admin:
       | showuseridentity | idnumber,email,phone1,phone2,department,institution |
-    When I am on "Course 001" course homepage
+    And I am on "Course 001" course homepage
     And I navigate to course participants
     And I press "Enrol users"
-    When I set the field "Select users" to "student100@example.com"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Select users" "form_row"
-    Then I should see "student100@example.com, 1234567892, 1234567893, ABC1, ABC2"
+    And I click on "Select users" "field"
+    And I type "student100@example.com"
+    And I should see "student100@example.com, 1234567892, 1234567893, ABC1, ABC2"
 
 # The following tests are commented out as a result of MDL-66339.
 #  @javascript
index fc7517f..c74c8e1 100644 (file)
@@ -36,6 +36,6 @@ if ($ADMIN->fulltree) {
         $student = reset($student);
         $settings->add(new admin_setting_configselect_with_advanced('enrol_mnet/roleid',
             get_string('defaultrole', 'role'), '',
-            array('value'=>$student->id, 'adv'=>true), $options));
+            array('value' => $student->id ?? null, 'adv' => true), $options));
     }
 }
index 66e0ae4..d9a3a7f 100644 (file)
@@ -66,7 +66,10 @@ if ($ADMIN->fulltree) {
         $student = get_archetype_roles('student');
         $student = reset($student);
         $settings->add(new admin_setting_configselect('enrol_paypal/roleid',
-            get_string('defaultrole', 'enrol_paypal'), get_string('defaultrole_desc', 'enrol_paypal'), $student->id, $options));
+            get_string('defaultrole', 'enrol_paypal'),
+            get_string('defaultrole_desc', 'enrol_paypal'),
+            $student->id ?? null,
+            $options));
     }
 
     $settings->add(new admin_setting_configduration('enrol_paypal/enrolperiod',
index 6375ea2..cee5d7f 100644 (file)
@@ -79,7 +79,10 @@ if ($ADMIN->fulltree) {
         $student = get_archetype_roles('student');
         $student = reset($student);
         $settings->add(new admin_setting_configselect('enrol_self/roleid',
-            get_string('defaultrole', 'enrol_self'), get_string('defaultrole_desc', 'enrol_self'), $student->id, $options));
+            get_string('defaultrole', 'enrol_self'),
+            get_string('defaultrole_desc', 'enrol_self'),
+            $student->id ?? null,
+            $options));
     }
 
     $settings->add(new admin_setting_configduration('enrol_self/enrolperiod',
index a12df0b..c98df46 100644 (file)
@@ -355,6 +355,95 @@ class core_enrol_externallib_testcase extends externallib_advanced_testcase {
         }
     }
 
+    /**
+     * Verify get_enrolled_users() returned users according to their status.
+     */
+    public function test_get_enrolled_users_active_suspended() {
+        global $USER;
+
+        $this->resetAfterTest();
+
+        // Create the course and the users.
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+        $user0 = $this->getDataGenerator()->create_user(['username' => 'user0active']);
+        $user1 = $this->getDataGenerator()->create_user(['username' => 'user1active']);
+        $user2 = $this->getDataGenerator()->create_user(['username' => 'user2active']);
+        $user2su = $this->getDataGenerator()->create_user(['username' => 'user2suspended']); // Suspended user.
+        $user3 = $this->getDataGenerator()->create_user(['username' => 'user3active']);
+        $user3su = $this->getDataGenerator()->create_user(['username' => 'user3suspended']); // Suspended user.
+
+        // Enrol the users in the course.
+        $this->getDataGenerator()->enrol_user($user0->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user2su->id, $course->id, null, 'manual', 0, 0, ENROL_USER_SUSPENDED);
+        $this->getDataGenerator()->enrol_user($user3->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user3su->id, $course->id, null, 'manual', 0, 0, ENROL_USER_SUSPENDED);
+
+        // Create a role to add the allowedcaps. Users will have this role assigned.
+        $roleid = $this->getDataGenerator()->create_role();
+        // Allow the specified capabilities.
+        assign_capability('moodle/course:enrolreview', CAP_ALLOW, $roleid, $coursecontext);
+        assign_capability('moodle/user:viewalldetails', CAP_ALLOW, $roleid, $coursecontext);
+
+        // Switch to the user and assign the role.
+        $this->setUser($user0);
+        role_assign($roleid, $USER->id, $coursecontext);
+
+        // Suspended users.
+        $options = [
+            ['name' => 'onlysuspended', 'value' => true],
+            ['name' => 'userfields', 'value' => 'id,username']
+        ];
+        $suspendedusers = core_enrol_external::get_enrolled_users($course->id, $options);
+
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $suspendedusers = external_api::clean_returnvalue(core_enrol_external::get_enrolled_users_returns(), $suspendedusers);
+        $this->assertCount(2, $suspendedusers);
+
+        foreach ($suspendedusers as $suspendeduser) {
+            $this->assertStringContainsString('suspended', $suspendeduser['username']);
+        }
+
+        // Active users.
+        $options = [
+            ['name' => 'onlyactive', 'value' => true],
+            ['name' => 'userfields', 'value' => 'id,username']
+        ];
+        $activeusers = core_enrol_external::get_enrolled_users($course->id, $options);
+
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $activeusers = external_api::clean_returnvalue(core_enrol_external::get_enrolled_users_returns(), $activeusers);
+        $this->assertCount(4, $activeusers);
+
+        foreach ($activeusers as $activeuser) {
+            $this->assertStringContainsString('active', $activeuser['username']);
+        }
+
+        // All enrolled users.
+        $options = [
+            ['name' => 'userfields', 'value' => 'id,username']
+        ];
+        $allusers = core_enrol_external::get_enrolled_users($course->id, $options);
+
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $allusers = external_api::clean_returnvalue(core_enrol_external::get_enrolled_users_returns(), $allusers);
+        $this->assertCount(6, $allusers);
+
+        // Active and suspended. Test exception is thrown.
+        $options = [
+            ['name' => 'onlyactive', 'value' => true],
+            ['name' => 'onlysuspended', 'value' => true],
+            ['name' => 'userfields', 'value' => 'id,username']
+        ];
+        $this->expectException('coding_exception');
+        $message = 'Coding error detected, it must be fixed by a programmer: Both onlyactive ' .
+                        'and onlysuspended are set, this is probably not what you want!';
+        $this->expectExceptionMessage($message);
+        core_enrol_external::get_enrolled_users($course->id, $options);
+    }
+
     /**
      * Test get_users_courses
      */
index bfec2ff..a882f6e 100644 (file)
@@ -1,6 +1,10 @@
 This files describes API changes in /enrol/* - plugins,
 information provided here is intended especially for developers.
 
+=== 3.11 ===
+
+* Added onlysuspended option to core_enrol_get_enrolled_users webservice to retrieve only suspended users.
+
 === 3.8 ===
 
 * Function enrol_manual_plugin::enrol_cohort now return the number of enrolled cohort users.
diff --git a/files/classes/archive_writer.php b/files/classes/archive_writer.php
new file mode 100644 (file)
index 0000000..415fca9
--- /dev/null
@@ -0,0 +1,142 @@
+<?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/>.
+
+/**
+ * Abstraction of general file archives.
+ *
+ * @package   core_files
+ * @copyright 2020 Mark Nelson <mdjnelson@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_files;
+
+use core_files\local\archive_writer\file_writer_interface as file_writer_interface;
+use core_files\local\archive_writer\stream_writer_interface as stream_writer_interface;
+
+/**
+ * Each file archive type must extend this class.
+ *
+ * @package   core_files
+ * @copyright 2020 Mark Nelson <mdjnelson@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class archive_writer {
+
+    /**
+     * The zip writer class.
+     */
+    public const ZIP_WRITER = 'zip_writer';
+
+    /**
+     * Returns the stream writer.
+     *
+     * @param string $filename
+     * @param string $type
+     * @return stream_writer_interface
+     */
+    public static function get_stream_writer(string $filename, string $type): stream_writer_interface {
+        $classname = self::get_classname_for_type($type);
+
+        if (!is_a($classname, stream_writer_interface::class, true)) {
+            throw new \InvalidArgumentException("{$type} does not support streaming");
+        }
+
+        return $classname::stream_instance($filename);
+    }
+
+    /**
+     * Returns the file writer.
+     *
+     * @param string $filepath
+     * @param string $type
+     * @return file_writer_interface
+     */
+    public static function get_file_writer(string $filepath, string $type): file_writer_interface {
+        $classname = self::get_classname_for_type($type);
+
+        if (!is_a($classname, file_writer_interface::class, true)) {
+            throw new \InvalidArgumentException("{$type} does not support writing to files");
+        }
+
+        return $classname::file_instance($filepath);
+    }
+
+    /**
+     * Sanitise the file path, removing any unsuitable characters.
+     *
+     * @param string $filepath
+     * @return string
+     */
+    public function sanitise_filepath(string $filepath): string {
+        return clean_param($filepath, PARAM_PATH);
+    }
+
+    /**
+     * Returns the class name for the type that was provided in get_file_writer().
+     *
+     * @param string $type
+     * @return string
+     */
+    protected static function get_classname_for_type(string $type): string {
+        return "core_files\local\archive_writer\\" . $type;
+    }
+
+    /**
+     * The archive_writer Constructor.
+     */
+    protected function __construct() {
+
+    }
+
+    /**
+     * Adds a file from a file path.
+     *
+     * @param string $name The path of file in archive (including directory).
+     * @param string $path The path to file on disk (note: paths should be encoded using
+     *                     UNIX-style forward slashes -- e.g '/path/to/some/file').
+     */
+    abstract public function add_file_from_filepath(string $name, string $path): void;
+
+    /**
+     * Adds a file from a string.
+     *
+     * @param string $name The path of file in archive (including directory).
+     * @param string $data The contents of file
+     */
+    abstract public function add_file_from_string(string $name, string $data): void;
+
+    /**
+     * Adds a file from a stream.
+     *
+     * @param string $name The path of file in archive (including directory).
+     * @param resource $stream The contents of file as a stream resource
+     */
+    abstract public function add_file_from_stream(string $name, $stream): void;
+
+    /**
+     * Adds a stored_file to archive.
+     *
+     * @param string $name The path of file in archive (including directory).
+     * @param \stored_file $file
+     */
+    abstract public function add_file_from_stored_file(string $name, \stored_file $file): void;
+
+    /**
+     * Finish writing the zip footer.
+     */
+    abstract public function finish(): void;
+}
diff --git a/files/classes/local/archive_writer/file_writer_interface.php b/files/classes/local/archive_writer/file_writer_interface.php
new file mode 100644 (file)
index 0000000..6e2a175
--- /dev/null
@@ -0,0 +1,50 @@
+<?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/>.
+
+/**
+ * Interface used by archives that write to files.
+ *
+ * @package   core_files
+ * @copyright 2020 Mark Nelson <mdjnelson@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_files\local\archive_writer;
+
+/**
+ * Interface used by archives that write to files.
+ *
+ * @package   core_files
+ * @copyright 2020 Mark Nelson <mdjnelson@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+interface file_writer_interface {
+
+    /**
+     * Return the file instance.
+     *
+     * @param string $filename
+     * @return static
+     */
+    public static function file_instance(string $filename): self;
+
+    /**
+     * Get the path of the zip.
+     *
+     * @return string
+     */
+    public function get_path_to_zip(): string;
+}
diff --git a/files/classes/local/archive_writer/stream_writer_interface.php b/files/classes/local/archive_writer/stream_writer_interface.php
new file mode 100644 (file)
index 0000000..4043580
--- /dev/null
@@ -0,0 +1,43 @@
+<?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/>.
+
+/**
+ * Interface used by archives that write to streams.
+ *
+ * @package   core_files
+ * @copyright 2020 Mark Nelson <mdjnelson@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_files\local\archive_writer;
+
+/**
+ * Interface used by archives that write to streams.
+ *
+ * @package   core_files
+ * @copyright 2020 Mark Nelson <mdjnelson@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+interface stream_writer_interface {
+
+    /**
+     * Return the stream instance.
+     *
+     * @param string $filename
+     * @return static
+     */
+    public static function stream_instance(string $filename): self;
+}
diff --git a/files/classes/local/archive_writer/zip_writer.php b/files/classes/local/archive_writer/zip_writer.php
new file mode 100644 (file)
index 0000000..f6187f5
--- /dev/null
@@ -0,0 +1,135 @@
+<?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 for creating ZIP archives.
+ *
+ * @package   core_files
+ * @copyright 2020 Mark Nelson <mdjnelson@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_files\local\archive_writer;
+
+use ZipStream\Option\Archive;
+use ZipStream\ZipStream;
+use core_files\archive_writer;
+use core_files\local\archive_writer\file_writer_interface as file_writer_interface;
+use core_files\local\archive_writer\stream_writer_interface as stream_writer_interface;
+
+/**
+ * Class used for creating ZIP archives.
+ *
+ * @package   core_files
+ * @copyright 2020 Mark Nelson <mdjnelson@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class zip_writer extends archive_writer implements file_writer_interface, stream_writer_interface {
+
+    /**
+     * @var resource File resource for the file handle for a file-based zip stream
+     */
+    private $zipfilehandle = null;
+
+    /**
+     * @var String The location of the zip file.
+     */
+    private $zipfilepath = null;
+
+    /**
+     * @var ZipStream The zip stream.
+     */
+    private $archive;
+
+    /**
+     * The zip_writer constructor.
+     *
+     * @param ZipStream $archive
+     */
+    protected function __construct(ZipStream $archive) {
+        parent::__construct();
+        $this->archive = $archive;
+    }
+
+    public static function stream_instance(string $filename): stream_writer_interface {
+        $options = new Archive();
+        $options->setSendHttpHeaders(true);
+        $options->setContentDisposition('attachment');
+        $options->setContentType('application/x-zip');
+        $zipwriter = new ZipStream($filename, $options);
+
+        return new static($zipwriter);
+    }
+
+    public static function file_instance(string $filename): file_writer_interface {
+        $dir = make_request_directory();
+        $filepath = "$dir/$filename";
+        $fh = fopen($filepath, 'w');
+
+        $exportoptions = new Archive();
+        $exportoptions->setOutputStream($fh);
+        $exportoptions->setSendHttpHeaders(false);
+        $zipstream = new ZipStream($filename, $exportoptions);
+
+        $zipwriter = new static($zipstream);
+        // ZipStream only takes a file handle resource.
+        // It does not close this resource itself, and it does not know the location of this resource on disk.
+        // Store references to the filehandle, and the location of the filepath in the new class so that the `finish()`
+        // function can close the fh, and move the temporary file into place.
+        // The filehandle must be closed when finishing the archive. ZipStream does not close it automatically.
+        $zipwriter->zipfilehandle = $fh;
+        $zipwriter->zipfilepath = $filepath;
+
+        return $zipwriter;
+    }
+
+    public function add_file_from_filepath(string $name, string $path): void {
+        $this->archive->addFileFromPath($this->sanitise_filepath($name), $path);
+    }
+
+    public function add_file_from_string(string $name, string $data): void {
+        $this->archive->addFile($this->sanitise_filepath($name), $data);
+    }
+
+    public function add_file_from_stream(string $name, $stream): void {
+        $this->archive->addFileFromStream($this->sanitise_filepath($name), $stream);
+        fclose($stream);
+    }
+
+    public function add_file_from_stored_file(string $name, \stored_file $file): void {
+        $filehandle = $file->get_content_file_handle();
+        $this->archive->addFileFromStream($this->sanitise_filepath($name), $filehandle);
+        fclose($filehandle);
+    }
+
+    public function finish(): void {
+        $this->archive->finish();
+
+        if ($this->zipfilehandle) {
+            fclose($this->zipfilehandle);
+        }
+    }
+
+    public function get_path_to_zip(): string {
+        return $this->zipfilepath;
+    }
+
+    public function sanitise_filepath(string $filepath): string {
+        $filepath = parent::sanitise_filepath($filepath);
+
+        return \ZipStream\File::filterFilename($filepath);
+    }
+}
diff --git a/files/tests/archive_writer_test.php b/files/tests/archive_writer_test.php
new file mode 100644 (file)
index 0000000..544f58e
--- /dev/null
@@ -0,0 +1,54 @@
+<?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/>.
+
+/**
+ * Unit tests for core_files\local\archive_writer/zip_archive.
+ *
+ * @package core_files
+ * @category test
+ * @copyright 2020 Mark Nelson <mdjnelson@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ */
+
+namespace core_files;
+
+use advanced_testcase;
+use core_files\local\archive_writer\zip_writer;
+
+/**
+ * Unit tests for \core_files\archive_writer.
+ *
+ * @coversDefaultClass \core_files\archive_writer
+ */
+class archive_writer_testcase extends advanced_testcase {
+
+    /**
+     * Test get_file_writer().
+     */
+    public function test_get_file_writer(): void {
+        $zipwriter = archive_writer::get_file_writer('file.zip', archive_writer::ZIP_WRITER);
+        $this->assertInstanceOf(zip_writer::class, $zipwriter);
+        $this->assertTrue(file_exists($zipwriter->get_path_to_zip()));
+    }
+
+    /**
+     * Test get_stream_writer().
+     */
+    public function test_get_stream_writer(): void {
+        $zipwriter = archive_writer::get_stream_writer('path/to/file.txt', archive_writer::ZIP_WRITER);
+        $this->assertInstanceOf(zip_writer::class, $zipwriter);
+    }
+}
diff --git a/files/tests/fixtures/awesome_file.txt b/files/tests/fixtures/awesome_file.txt
new file mode 100644 (file)
index 0000000..61193b6
--- /dev/null
@@ -0,0 +1 @@
+Hey, this is an awesome text file. Hello! :)
\ No newline at end of file
diff --git a/files/tests/local/archive_writer/zip_writer_test.php b/files/tests/local/archive_writer/zip_writer_test.php
new file mode 100644 (file)
index 0000000..790d8d1
--- /dev/null
@@ -0,0 +1,187 @@
+<?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/>.
+
+/**
+ * Unit tests for \core_files\local\archive_writer\zip_writer.
+ *
+ * @package core_files
+ * @category test
+ * @copyright 2020 Mark Nelson <mdjnelson@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ */
+
+namespace core_files\local\archive_writer;
+
+use advanced_testcase;
+use context_module;
+use core_files\archive_writer;
+use ZipArchive;
+
+/**
+ * Unit tests for \core_files\local\archive_writer\zip_writer.
+ *
+ * @coversDefaultClass \core_files\local\archive_writer\zip_writer
+ */
+class zip_writer_testcase extends advanced_testcase {
+
+    /**
+     * Test add_file_from_filepath().
+     */
+    public function test_add_file_from_filepath(): void {
+        global $CFG;
+
+        $pathtofileinzip = '/some/made/up/name.txt';
+        $filetoadd = $CFG->dirroot . '/files/tests/fixtures/awesome_file.txt';
+
+        $zipwriter = archive_writer::get_file_writer('test.zip', archive_writer::ZIP_WRITER);
+        $zipwriter->add_file_from_filepath($pathtofileinzip, $filetoadd);
+        $zipwriter->finish();
+
+        $pathtozip = $zipwriter->get_path_to_zip();
+        $zip = new ZipArchive();
+        $opened = $zip->open($pathtozip);
+        $this->assertTrue($opened);
+
+        $pathtofileinzip = $zipwriter->sanitise_filepath($pathtofileinzip);
+
+        $this->assertEquals("Hey, this is an awesome text file. Hello! :)", $zip->getFromName($pathtofileinzip));
+    }
+
+    /**
+     * Test add_file_from_string().
+     */
+    public function test_add_file_from_string(): void {
+        $pathtofileinzip = "/path/to/my/awesome/file.txt";
+        $mycontent = "This is some real awesome content, ya dig?";
+
+        $zipwriter = archive_writer::get_file_writer('test.zip', archive_writer::ZIP_WRITER);
+        $zipwriter->add_file_from_string($pathtofileinzip, $mycontent);
+        $zipwriter->finish();
+
+        $pathtozip = $zipwriter->get_path_to_zip();
+        $zip = new ZipArchive();
+        $opened = $zip->open($pathtozip);
+        $this->assertTrue($opened);
+
+        $pathtofileinzip = $zipwriter->sanitise_filepath($pathtofileinzip);
+
+        $this->assertEquals($mycontent, $zip->getFromName($pathtofileinzip));
+    }
+
+    /**
+     * Test add_file_from_stream().
+     */
+    public function test_add_file_from_stream(): void {
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $course = $this->getDataGenerator()->create_course();
+        $assign = $this->getDataGenerator()->create_module('assign', ['course' => $course->id]);
+
+        // Add a file to the intro.
+        $filerecord = [
+            'contextid' => context_module::instance($assign->cmid)->id,
+            'component' => 'mod_assign',
+            'filearea'  => 'intro',
+            'itemid'    => 0,
+            'filepath'  => '/',
+            'filename'  => 'fileintro.txt',
+        ];
+        $fs = get_file_storage();
+        $storedfile = $fs->create_file_from_string($filerecord, 'Contents for the assignment, yeow!');
+
+        $pathtofileinzip = $storedfile->get_filepath() . $storedfile->get_filename();
+
+        $zipwriter = archive_writer::get_file_writer('test.zip', archive_writer::ZIP_WRITER);
+        $zipwriter->add_file_from_stream($pathtofileinzip, $storedfile->get_content_file_handle());
+        $zipwriter->finish();
+
+        $pathtozip = $zipwriter->get_path_to_zip();
+        $zip = new ZipArchive();
+        $opened = $zip->open($pathtozip);
+        $this->assertTrue($opened);
+
+        $pathtofileinzip = $zipwriter->sanitise_filepath($pathtofileinzip);
+
+        $this->assertEquals($storedfile->get_content(), $zip->getFromName($pathtofileinzip));
+    }
+
+    /**
+     * Test add_file_from_stored_file().
+     */
+    public function test_add_file_from_stored_file(): void {
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $course = $this->getDataGenerator()->create_course();
+        $assign = $this->getDataGenerator()->create_module('assign', ['course' => $course->id]);
+
+        // Add a file to the intro.
+        $filerecord = [
+            'contextid' => context_module::instance($assign->cmid)->id,
+            'component' => 'mod_assign',
+            'filearea'  => 'intro',
+            'itemid'    => 0,
+            'filepath'  => '/',
+            'filename'  => 'fileintro.txt',
+        ];
+        $fs = get_file_storage();
+        $storedfile = $fs->create_file_from_string($filerecord, 'Contents for the assignment, yeow!');
+
+        $pathtofileinzip = $storedfile->get_filepath() . $storedfile->get_filename();
+
+        $zipwriter = archive_writer::get_file_writer('test.zip', archive_writer::ZIP_WRITER);
+        $zipwriter->add_file_from_stored_file($pathtofileinzip, $storedfile);
+        $zipwriter->finish();
+
+        $pathtozip = $zipwriter->get_path_to_zip();
+        $zip = new ZipArchive();
+        $opened = $zip->open($pathtozip);
+        $this->assertTrue($opened);
+
+        $pathtofileinzip = $zipwriter->sanitise_filepath($pathtofileinzip);
+
+        $this->assertEquals($storedfile->get_content(), $zip->getFromName($pathtofileinzip));
+    }
+
+    /**
+     * Test sanitise_filepath().
+     *
+     * @param string $providedfilepath The provided file path.
+     * @param string $expectedfilepath The expected file path.
+     * @dataProvider sanitise_filepath_provider
+     */
+    public function test_sanitise_filepath(string $providedfilepath, string $expectedfilepath): void {
+        $zipwriter = archive_writer::get_stream_writer('path/to/file.txt', archive_writer::ZIP_WRITER);
+        $this->assertEquals($expectedfilepath, $zipwriter->sanitise_filepath($providedfilepath));
+    }
+
+    /**
+     * Data provider for test_sanitise_filepath.
+     *
+     * @return array
+     */
+    public function sanitise_filepath_provider(): array {
+        return [
+            ['a../../file/path', 'a../file/path'],
+            ['a./file/path', 'a./file/path'],
+            ['../file/path', 'file/path'],
+            ['foo/bar/', 'foo/bar/'],
+            ['\\\\\\a\\\\\\file\\\\\\path', 'a/file/path'],
+            ['//a//file/////path////', 'a/file/path/']
+        ];
+    }
+}
index 7e50089..2b4a65a 100644 (file)
@@ -50,15 +50,12 @@ Feature: Organize students into groups
     And the "members" select box should not contain "Student 1 (student1@example.com)"
     And I navigate to course participants
     And I set the field "type" in the "Filter 1" "fieldset" to "Groups"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I click on "Group 1" "list_item"
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Group 1"
     And I click on "Apply filters" "button"
     And I should see "Student 0"
     And I should see "Student 1"
     And I should not see "Student 2"
-    And I click on "Group 1" "autocomplete_selection"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I click on "Group 2" "list_item"
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Group 2"
     And I click on "Apply filters" "button"
     And I should see "Student 2"
     And I should see "Student 3"
index e63ee05..087fecb 100644 (file)
@@ -46,8 +46,7 @@ Feature: The description of a group can be viewed by students and teachers
     And I should see "Description for Group A"
     And ".groupinfobox" "css_element" should exist
     And I set the field "type" in the "Filter 1" "fieldset" to "Groups"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I click on "Group B" "list_item"
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Group B"
     And I click on "Apply filters" "button"
     And I click on "Student 2" "link" in the "participants" "table"
     And I click on "Group B" "link"
@@ -96,8 +95,7 @@ Feature: The description of a group can be viewed by students and teachers
     And I should see "Description for Group A"
     And ".groupinfobox" "css_element" should exist
     And I set the field "type" in the "Filter 1" "fieldset" to "Groups"
-    And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
-    And I click on "Group B" "list_item"
+    And I set the field "Type or select..." in the "Filter 1" "fieldset" to "Group B"
     And I click on "Apply filters" "button"
     And I click on "Student 2" "link" in the "participants" "table"
     And I click on "Group B" "link"
index 933e7fd..c712eed 100644 (file)
@@ -29,5 +29,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+$string['parentlanguage'] = '';
 $string['thisdirection'] = 'ltr';
 $string['thislanguage'] = 'Español - Internacional';
diff --git a/install/lang/ky/admin.php b/install/lang/ky/admin.php
new file mode 100644 (file)
index 0000000..674950e
--- /dev/null
@@ -0,0 +1,44 @@
+<?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['clianswerno'] = 'Жок';
+$string['cliansweryes'] = 'Ооба';
+$string['cliincorrectvalueerror'] = 'Ката, «{$a->option}» үчүн  «{$a->value}» туура эмес маани';
+$string['cliincorrectvalueretry'] = 'Туура эмес маани, кайрадан аракет кылып көрүңүз';
+$string['clitypevalue'] = 'Маанисин киргизгиле';
+$string['clitypevaluedefault'] = 'Маанисин киргизгиле, алгач белгиленген ({$a}) маанисин  колдонуу үчүн Enter баскычын баскыла';
+$string['cliunknowoption'] = 'Таанылбаган параметрлер {$a}
+
+Сураныч, --help параметрин колдонуңуз';
+$string['cliyesnoprompt'] = 'У киргизиңиз (Ооба дегенди билдирет) же n киргизиңиз (Жок дегенди билдирет)';
+$string['environmentrequireinstall'] = 'Орнотуу жана күйгүзүү зарыл';
+$string['environmentrequireversion'] = '{$a->needed} версиясы талап кылынат, сиз колдонгон версия {$a->current}';
+$string['upgradekeyset'] = 'Жаӊыртуу ачкычы (аны көрсөткүӊүз келбесе бош калтырыӊыз)';
index d427fa4..8a9b9ab 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$string['cannotcreatelangdir'] = 'Не може да се креира lang директориумот.';
-$string['cannotcreatetempdir'] = 'Не може да се креира temp директориумот.';
-$string['cannotdownloadcomponents'] = 'Не може да се преземат компонентите.';
-$string['cannotdownloadzipfile'] = 'Не може да се преземи ZIP датотеката.';
-$string['cannotfindcomponent'] = 'Не може да се најде компонентата.';
-$string['cannotsavemd5file'] = 'Не може да се сними md5 датотеката.';
-$string['cannotsavezipfile'] = 'Не може да се сними ZIP датотеката.';
-$string['cannotunzipfile'] = 'Не може да се отпакува датотеката.';
-$string['componentisuptodate'] = 'Компонентата е од последната верзија';
-$string['downloadedfilecheckfailed'] = 'Проверката на преземената датотека не заврши успешно.';
-$string['invalidmd5'] = 'невалидна md5';
-$string['missingrequiredfield'] = 'Некои задолжителни полиња недостасуваат';
-$string['wrongdestpath'] = 'Погрешна дестинација.';
-$string['wrongsourcebase'] = 'Погрешна изворна адреса.';
-$string['wrongzipfilename'] = 'Погрешна ZIP датотека.';
+$string['cannotcreatedboninstall'] = '<p>Не може да се креира базата со податоци.</p>
+<p>Посочената база со податоци не постои и корисникот нема овластувања да ја креира базата со податоци.</p>
+<p>Администраторот на сајтот треба да ја верификува конфигурацијата на базата со податоци.</p>';
+$string['cannotcreatelangdir'] = 'Lang именик не може да се креира';
+$string['cannotcreatetempdir'] = 'Не може да се креира привремена папка';
+$string['cannotdownloadcomponents'] = 'Не можете да ги симнете компонентите';
+$string['cannotdownloadzipfile'] = 'Не можете да спуштите ZIP фајл';
+$string['cannotfindcomponent'] = 'Компонентата не е пронајдена';
+$string['cannotsavemd5file'] = 'Не може да се сними md5 фајлот';
+$string['cannotsavezipfile'] = 'Не може да се сними ZIP фајл';
+$string['cannotunzipfile'] = 'Не можеше да се одзипува фајлот';
+$string['componentisuptodate'] = 'Компонентата е ажурирана.';
+$string['dmlexceptiononinstall'] = '<p>Настана грешка во базата на податоци [{$a->errorcode}].<br />{$a->debuginfo}</p>';
+$string['downloadedfilecheckfailed'] = 'Проверката на преземениот фајл не успеа';
+$string['invalidmd5'] = 'Променливата за проверка беше погрешна - обидете се повторно';
+$string['missingrequiredfield'] = 'Некое задолжително поле недостасува';
+$string['remotedownloaderror'] = '<p>Преземањето на компонентата на вашиот сервер не успеа. Проверете ги поставките за прокси; се препорачува екстензијата PHP cURL.</p>
+<p>Мора да го рачно да го симнете фајлот <a href="{$a->url}">{$a->url}</a>, копирајте го "{$a->dest}" во вашиот сервер и таму отпакувајте го.</p>';
+$string['wrongdestpath'] = 'Погрешна дестинациска патека';
+$string['wrongsourcebase'] = 'Погрешна URL на извор';
+$string['wrongzipfilename'] = 'Погрешно име на ZIP фајл';
index 7f30f96..5f95e7a 100644 (file)
@@ -33,4 +33,4 @@ $string['language'] = 'Јазик';
 $string['moodlelogo'] = 'Moodle лого';
 $string['next'] = 'Следно';
 $string['previous'] = 'Претходно';
-$string['reload'] = 'Ð\92Ñ\87иÑ\82аÑ\98 Ð¿Ð¾Ð²Ñ\82оÑ\80но';
+$string['reload'] = 'Ð\92Ñ\87иÑ\82аÑ\98 Ð¾Ð´Ð½Ð¾Ð²о';
index 98e3ffb..069ca73 100644 (file)
@@ -32,7 +32,7 @@ defined('MOODLE_INTERNAL') || die();
 $string['admindirname'] = 'Adminmappe';
 $string['availablelangs'] = 'Tilgjengelige språkpakker';
 $string['chooselanguagehead'] = 'Velg et språk';
-$string['chooselanguagesub'] = 'Velg språk (bare for INSTALLASJONEN). Du vil kunne velge språk for nettsted og bruker på et skjermbilde senere.';
+$string['chooselanguagesub'] = 'Velg et språk for installasjonen. Dette språket vil også bli brukt som standardspråk for nettstedet, men det kan endres senere.';
 $string['clialreadyconfigured'] = 'Konfigurasjonsfilen config.php finnes allerede. Vennligst bruk admin/cli/install_database.php hvis du vil installere Moodle på denne portalen.';
 $string['clialreadyinstalled'] = 'Filen config.php eksisterer allerede. Vennligst bruk admin/cli/install_database.php hvis du vil oppgradere Moodle på denne portalen.';
 $string['cliinstallheader'] = 'Moodle {$a} kommandolinje installasjonsprogram';
@@ -47,7 +47,7 @@ $string['environmenthead'] = 'Sjekker "omgivelsene" dine...';
 $string['environmentsub2'] = 'Hver Moodle-utgivelse har minimumskrav til PHP versjon og et sett med obligatoriske PHP-tillegg. En full sjekk av omgivelser gjøres før hver installering og oppgradering. Vær vennlig å kontakte serveradministrator hvis du ikke vet hvordan du skal installere nye versjoner eller slå på PHP-tillegg.';
 $string['errorsinenvironment'] = 'Omgivelseskontroll feilet!';
 $string['installation'] = 'Installasjon';
-$string['langdownloaderror'] = 'Beklageligvis ble ikke språkpakken "{$a}" installert. Installasjonsprosessen vil fortsette på Engelsk.';
+$string['langdownloaderror'] = 'Beklageligvis kunne ikke språkpakken "{$a}" lastes ned. Installasjonsprosessen vil fortsette på Engelsk.';
 $string['memorylimithelp'] = '<p>PHP minnegrensen for serveren din er nå satt til {$a}.</p>
 <p>Dette kan skape minneproblemer for Moodle senere, spesielt hvis du har mange moduler tillatt og/eller mange brukere.</p>
 <p>Vi anbefaler at du konfigurer PHP med en høyere grense enn mulig, for eksepmel 40M. Det er flere måter å gjøre dette på.:</p>
index fdb59b1..a9dc958 100644 (file)
@@ -49,7 +49,7 @@ Verificarea completă a mediului se face înainte de fiecare instalare și upgra
 $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>
+$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>
@@ -90,7 +90,7 @@ $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 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> Î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!';
index 94ca6e0..8c7a79d 100644 (file)
@@ -29,5 +29,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+$string['parentlanguage'] = '';
 $string['thisdirection'] = 'ltr';
 $string['thislanguage'] = 'Română';
index a4ab49e..1473fdc 100644 (file)
@@ -31,4 +31,4 @@ defined('MOODLE_INTERNAL') || die();
 
 $string['parentlanguage'] = 'ro';
 $string['thisdirection'] = 'ltr';
-$string['thislanguage'] = 'Română';
+$string['thislanguage'] = 'Română Workplace';
index 062aa99..2f51129 100644 (file)
@@ -39,5 +39,5 @@ $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}';
+$string['environmentrequireinstall'] = 'måste vara installerat och aktiverat';
+$string['environmentrequireversion'] = 'version {$a->needed} krävs och du använder {$a->current}';
index e6103e5..92781b5 100644 (file)
@@ -348,6 +348,8 @@ $string['configsessioncookie'] = 'This setting customises the name of the cookie
 $string['configsessioncookiedomain'] = 'This allows you to change the domain that the Moodle cookies are available from. This is useful for Moodle customisations (e.g. authentication or enrolment plugins) that need to share Moodle session information with a web application on another subdomain. <strong>WARNING: it is strongly recommended to leave this setting at the default (empty) - an incorrect value will prevent all logins to the site.</strong>';
 $string['configsessioncookiepath'] = 'If you need to change where browsers send the Moodle cookies, you can change this setting to specify a subdirectory of your web site.  Otherwise the default \'/\' should be fine.';
 $string['configsessiontimeout'] = 'If people logged in to this site are idle for a long time (without loading pages) then they are automatically logged out (their session is ended).  This variable specifies how long this time should be.';
+$string['configsessiontimeoutwarning'] = 'If people logged in to this site are idle for a long time (without loading pages) then they are warned about their session is about to end.  This variable specifies how long this time should be.';
+$string['configsessiontimeoutwarningcheck'] = 'Session timeout warning must be less than session timeout';
 $string['configshowicalsource'] = 'Show source information for iCal events';
 $string['configshowcommentscount'] = 'Show comments count, it will cost one more query when display comments link';
 $string['configshowsiteparticipantslist'] = 'All of these site students and site teachers will be listed on the site participants list. Who shall be allowed to see this site participants list?';
@@ -575,6 +577,8 @@ $string['enableuserfeedback'] = 'Enable feedback about this software';
 $string['enableuserfeedback_desc'] = 'If enabled, a \'Give feedback about this software\' link is displayed in the footer for users to give feedback about the Moodle software to Moodle HQ. If the \'Next feedback reminder\' option is set, the user is also shown a reminder on the Dashboard at the specified interval. Setting \'Next feedback reminder\' to \'Never\' disables the Dashboard reminder, while leaving the \'Give feedback about this software\' link in the footer.';
 $string['enablewebservices'] = 'Enable web services';
 $string['enablewsdocumentation'] = 'Web services documentation';
+$string['encryptedpassword_set'] = '(Set and encrypted)';
+$string['encryptedpassword_edit'] = 'Enter new value';
 $string['enrolinstancedefaults'] = 'Enrolment instance defaults';
 $string['enrolinstancedefaults_desc'] = 'Default enrolment settings in new courses.';
 $string['enrolmultipleusers'] = 'Enrol the users';
@@ -1189,6 +1193,7 @@ $string['sessioncookiedomain'] = 'Cookie domain';
 $string['sessioncookiepath'] = 'Cookie path';
 $string['sessionhandling'] = 'Session handling';
 $string['sessiontimeout'] = 'Timeout';
+$string['sessiontimeoutwarning'] = 'Timeout Warning';
 $string['settingdependenton'] = 'This setting may be hidden, based on the value of <strong>{$a}</strong>.';
 $string['settingfileuploads'] = 'File uploading is required for normal operation, please enable it in PHP configuration.';
 $string['settingmemorylimit'] = 'Insufficient memory detected, please set higher memory limit in PHP settings.';
index 8be1b32..096bc74 100644 (file)
@@ -236,6 +236,12 @@ $string['duplicaterolename'] = 'There is already a role with this name!';
 $string['duplicateroleshortname'] = 'There is already a role with this short name!';
 $string['duplicateusername'] = 'Duplicate username - skipping record';
 $string['emailfail'] = 'Emailing failed';
+$string['encryption_encryptfailed'] = 'Encryption failed';
+$string['encryption_decryptfailed'] = 'Decryption failed';
+$string['encryption_invalidkey'] = 'Invalid key';
+$string['encryption_keyalreadyexists'] = 'Key already exists';
+$string['encryption_nokey'] = 'Key not found';
+$string['encryption_wrongmethod'] = 'Data does not match a supported encryption method';
 $string['enddatebeforestartdate'] = 'The course end date must be after the start date.';
 $string['error'] = 'Error occurred';
 $string['error_question_answers_missing_in_db'] = 'Failed to find an answer matching "{$a->answer}" in the question_answers database table. This occurred while restoring the question with id {$a->filequestionid} in the backup file, which has been matched to the existing question with id {$a->dbquestionid} in the database.';
index 8fbcca4..95c040d 100644 (file)
@@ -1180,6 +1180,7 @@ $string['loginstepsnone'] = '<p>Hi!</p>
 <p>All you need to do is make up a username and password and use it in the form on this page!</p>
 <p>If someone else has already chosen your username then you\'ll have to try again using a different username.</p>';
 $string['loginto'] = 'Log in to {$a}';
+$string['loginagain'] = 'Log in again';
 $string['logout'] = 'Log out';
 $string['logoutconfirm'] = 'Do you really want to log out?';
 $string['logs'] = 'Logs';
index 6764caf..1177b59 100644 (file)
@@ -2724,6 +2724,58 @@ class admin_setting_configpasswordunmask_with_advanced extends admin_setting_con
     }
 }
 
+/**
+ * Admin setting class for encrypted values using secure encryption.
+ *
+ * @copyright 2019 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class admin_setting_encryptedpassword extends admin_setting {
+
+    /**
+     * Constructor. Same as parent except that the default value is always an empty string.
+     *
+     * @param string $name Internal name used in config table
+     * @param string $visiblename Name shown on form
+     * @param string $description Description that appears below field
+     */
+    public function __construct(string $name, string $visiblename, string $description) {
+        parent::__construct($name, $visiblename, $description, '');
+    }
+
+    public function get_setting() {
+        return $this->config_read($this->name);
+    }
+
+    public function write_setting($data) {
+        $data = trim($data);
+        if ($data === '') {
+            // Value can really be set to nothing.
+            $savedata = '';
+        } else {
+            // Encrypt value before saving it.
+            $savedata = \core\encryption::encrypt($data);
+        }
+        return ($this->config_write($this->name, $savedata) ? '' : get_string('errorsetting', 'admin'));
+    }
+
+    public function output_html($data, $query='') {
+        global $OUTPUT;
+
+        $default = $this->get_defaultsetting();
+        $context = (object) [
+            'id' => $this->get_id(),
+            'name' => $this->get_full_name(),
+            'set' => $data !== '',
+            'novalue' => $this->get_setting() === null
+        ];
+        $element = $OUTPUT->render_from_template('core_admin/setting_encryptedpassword', $context);
+
+        return format_admin_setting($this, $this->visiblename, $element, $this->description,
+                true, '', $default, $query);
+    }
+}
+
 /**
  * Empty setting used to allow flags (advanced) on settings that can have no sensible default.
  * Note: Only advanced makes sense right now - locked does not.
@@ -3762,6 +3814,8 @@ class admin_setting_configduration extends admin_setting {
 
     /** @var int default duration unit */
     protected $defaultunit;
+    /** @var callable|null Validation function */
+    protected $validatefunction = null;
 
     /**
      * Constructor
@@ -3785,6 +3839,36 @@ class admin_setting_configduration extends admin_setting {
         parent::__construct($name, $visiblename, $description, $defaultsetting);
     }
 
+    /**
+     * Sets a validate function.
+     *
+     * The callback will be passed one parameter, the new setting value, and should return either
+     * an empty string '' if the value is OK, or an error message if not.
+     *
+     * @param callable|null $validatefunction Validate function or null to clear
+     * @since Moodle 3.10
+     */
+    public function set_validate_function(?callable $validatefunction = null) {
+        $this->validatefunction = $validatefunction;
+    }
+
+    /**
+     * Validate the setting. This uses the callback function if provided; subclasses could override
+     * to carry out validation directly in the class.
+     *
+     * @param int $data New value being set
+     * @return string Empty string if valid, or error message text
+     * @since Moodle 3.10
+     */
+    protected function validate_setting(int $data): string {
+        // If validation function is specified, call it now.
+        if ($this->validatefunction) {
+            return call_user_func($this->validatefunction, $data);
+        } else {
+            return '';
+        }
+    }
+
     /**
      * Returns selectable units.
      * @static
@@ -3870,6 +3954,12 @@ class admin_setting_configduration extends admin_setting {
             return get_string('errorsetting', 'admin');
         }
 
+        // Validate the new setting.
+        $error = $this->validate_setting($seconds);
+        if ($error) {
+            return $error;
+        }
+
         $result = $this->config_write($this->name, $seconds);
         return ($result ? '' : get_string('errorsetting', 'admin'));
     }
index 23e42cb..71d7b14 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js and b/lib/amd/build/form-autocomplete.min.js differ
index 5c1fe60..89cf819 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js.map and b/lib/amd/build/form-autocomplete.min.js.map differ
index 81e4444..680602f 100644 (file)
Binary files a/lib/amd/build/network.min.js and b/lib/amd/build/network.min.js differ
index b2204e1..df849d1 100644 (file)
Binary files a/lib/amd/build/network.min.js.map and b/lib/amd/build/network.min.js.map differ
index 349aade..6843e1b 100644 (file)
Binary files a/lib/amd/build/paged_content_factory.min.js and b/lib/amd/build/paged_content_factory.min.js differ
index 9c4ea67..e541e28 100644 (file)
Binary files a/lib/amd/build/paged_content_factory.min.js.map and b/lib/amd/build/paged_content_factory.min.js.map differ
index f53bd41..3e20015 100644 (file)
Binary files a/lib/amd/build/permissionmanager.min.js and b/lib/amd/build/permissionmanager.min.js differ
index 8413c76..63e284b 100644 (file)
Binary files a/lib/amd/build/permissionmanager.min.js.map and b/lib/amd/build/permissionmanager.min.js.map differ
index f7f0f21..ffe8b07 100644 (file)
@@ -1173,6 +1173,9 @@ function($, log, str, templates, notification, LoadingIcon, Aria) {
                 originalSelect.hide();
                 var container = originalSelect.parent();
 
+                // Ensure that the data-fieldtype is set for behat.
+                input.find('input').attr('data-fieldtype', 'autocomplete');
+
                 container.append(layout);
                 container.find('[data-region="form_autocomplete-input"]').replaceWith(input);
                 container.find('[data-region="form_autocomplete-suggestions"]').replaceWith(suggestions);
index 5e21a6a..fcf1a0b 100644 (file)
@@ -32,14 +32,66 @@ define(['jquery', 'core/ajax', 'core/config', 'core/notification', 'core/str'],
     var sessionTimeout = false;
     // 1/10 of session timeout, max of 10 minutes.
     var checkFrequency = Math.min((Config.sessiontimeout / 10), 600) * 1000;
-    // 1/5 of sessiontimeout.
-    var warningLimit = checkFrequency * 2;
-
+    // Check if sessiontimeoutwarning is set or double the checkFrequency.
+    var warningLimit = (Config.sessiontimeoutwarning > 0) ? (Config.sessiontimeoutwarning * 1000) : (checkFrequency * 2);
+    // First wait is minimum of remaining time or half of the session timeout.
+    var firstWait = (Config.sessiontimeoutwarning > 0) ?
+        Math.min((Config.sessiontimeout - Config.sessiontimeoutwarning) * 1000, checkFrequency * 5) : checkFrequency * 5;
     /**
      * The session time has expired - we can't extend it now.
+     * @param {Modal} modal
      */
-    var timeoutSessionExpired = function() {
+    var timeoutSessionExpired = function(modal) {
         sessionTimeout = true;
+        warningDisplayed = false;
+        closeModal(modal);
+        displaySessionExpired();
+    };
+
+    /**
+     * Close modal - this relies on modal object passed from Notification.confirm.
+     *
+     * @param {Modal} modal
+     */
+    var closeModal = function(modal) {
+        modal.destroy();
+    };
+
+    /**
+     * The session time has expired - we can't extend it now.
+     * @return {Promise}
+     */
+    var displaySessionExpired = function() {
+        // Check again if its already extended before displaying session expired popup in case multiple tabs are open.
+        var request = {
+            methodname: 'core_session_time_remaining',
+            args: { }
+        };
+
+        return Ajax.call([request], true, true, true)[0].then(function(args) {
+            if (args.timeremaining * 1000 > warningLimit) {
+                return false;
+            } else {
+                return Str.get_strings([
+                    {key: 'sessionexpired', component: 'error'},
+                    {key: 'sessionerroruser', component: 'error'},
+                    {key: 'loginagain', component: 'moodle'},
+                    {key: 'cancel', component: 'moodle'}
+                ]).then(function(strings) {
+                    Notification.confirm(
+                        strings[0], // Title.
+                        strings[1], // Message.
+                        strings[2], // Login Again.
+                        strings[3], // Cancel.
+                        function() {
+                            location.reload();
+                            return true;
+                        }
+                    );
+                    return true;
+                }).catch(Notification.exception);
+            }
+        });
     };
 
     /**
@@ -55,23 +107,14 @@ define(['jquery', 'core/ajax', 'core/config', 'core/notification', 'core/str'],
 
         if (sessionTimeout) {
             // We timed out before we extended the session.
-            return Str.get_strings([
-                {key: 'sessionexpired', component: 'error'},
-                {key: 'sessionerroruser', component: 'error'}
-            ]).then(function(strings) {
-                Notification.alert(
-                    strings[0], // Title.
-                    strings[1] // Message.
-                );
-                return true;
-            }).fail(Notification.exception);
+            return displaySessionExpired();
         } else {
             return Ajax.call([request], true, true, false, requestTimeout)[0].then(function() {
                 if (keepAliveFrequency > 0) {
                     setTimeout(touchSession, keepAliveFrequency);
                 }
                 return true;
-            }).fail(function() {
+            }).catch(function() {
                 Notification.alert('', keepAliveMessage);
             });
         }
@@ -88,27 +131,14 @@ define(['jquery', 'core/ajax', 'core/config', 'core/notification', 'core/str'],
             methodname: 'core_session_time_remaining',
             args: { }
         };
-
         sessionTimeout = false;
         return Ajax.call([request], true, true, true)[0].then(function(args) {
             if (args.userid <= 0) {
                 return false;
             }
-            if (args.timeremaining < 0) {
-                Str.get_strings([
-                    {key: 'sessionexpired', component: 'error'},
-                    {key: 'sessionerroruser', component: 'error'}
-                ]).then(function(strings) {
-                    Notification.alert(
-                        strings[0], // Title.
-                        strings[1] // Message.
-                    );
-                    return true;
-                }).fail(Notification.exception);
-
-            } else if (args.timeremaining * 1000 < warningLimit && !warningDisplayed) {
-                // If we don't extend the session before the timeout - warn.
-                setTimeout(timeoutSessionExpired, args.timeremaining * 1000);
+            if (args.timeremaining <= 0) {
+                return displaySessionExpired();
+            } else if (args.timeremaining * 1000 <= warningLimit && !warningDisplayed) {
                 warningDisplayed = true;
                 Str.get_strings([
                     {key: 'norecentactivity', component: 'moodle'},
@@ -116,7 +146,7 @@ define(['jquery', 'core/ajax', 'core/config', 'core/notification', 'core/str'],
                     {key: 'extendsession', component: 'moodle'},
                     {key: 'cancel', component: 'moodle'}
                 ]).then(function(strings) {
-                    Notification.confirm(
+                     return Notification.confirm(
                         strings[0], // Title.
                         strings[1], // Message.
                         strings[2], // Extend session.
@@ -124,17 +154,20 @@ define(['jquery', 'core/ajax', 'core/config', 'core/notification', 'core/str'],
                         function() {
                             touchSession();
                             warningDisplayed = false;
-                            // First wait is half the session timeout.
-                            setTimeout(checkSession, checkFrequency * 5);
+                            // First wait is minimum of remaining time or half of the session timeout.
+                            setTimeout(checkSession, firstWait);
                             return true;
                         },
                         function() {
-                            warningDisplayed = false;
+                            // User has cancelled notification.
                             setTimeout(checkSession, checkFrequency);
                         }
                     );
-                    return true;
-                }).fail(Notification.exception);
+                }).then(modal => {
+                    // If we don't extend the session before the timeout - warn.
+                    setTimeout(timeoutSessionExpired, args.timeremaining * 1000, modal);
+                    return;
+                }).catch(Notification.exception);
             } else {
                 setTimeout(checkSession, checkFrequency);
             }
@@ -151,8 +184,8 @@ define(['jquery', 'core/ajax', 'core/config', 'core/notification', 'core/str'],
         if (keepAliveFrequency > 0) {
             setTimeout(touchSession, keepAliveFrequency);
         } else {
-            // First wait is half the session timeout.
-            setTimeout(checkSession, checkFrequency * 5);
+            // First wait is minimum of remaining time or half of the session timeout.
+            setTimeout(checkSession, firstWait);
         }
     };
 
index 577ccec..359e16a 100644 (file)
@@ -166,9 +166,11 @@ function(
      * @return {int|array}
      */
     var buildItemsPerPagePagingBarContext = function(itemsPerPage) {
+        var context = [];
+
         if ($.isArray(itemsPerPage)) {
             // Convert the array into a format accepted by the template.
-            var context = itemsPerPage.map(function(num) {
+            context = itemsPerPage.map(function(num) {
                 if (typeof num === 'number') {
                     // If the item is just a plain number then convert it into
                     // an object with value and active keys.
@@ -190,11 +192,12 @@ function(
             if (!activeItems.length) {
                 context[0].active = true;
             }
-
-            return context;
         } else {
-            return itemsPerPage;
+            // Convert the integer into a format accepted by the template.
+            context = [{value: itemsPerPage, active: true}];
         }
+
+        return context;
     };
 
     /**
index 0772104..3f350d3 100644 (file)
@@ -204,7 +204,7 @@ define(['jquery', 'core/config', 'core/notification', 'core/templates', 'core/yu
                 .done(function(content) {
                     panel.set('bodyContent', content);
                     panel.show();
-                    $('div.role_buttons').on('click', 'input', function(e) {
+                    $('div.role_buttons').on('click', 'button', function(e) {
                         var roleid = $(e.currentTarget).data('role-id');
                         changePermissions(row, roleid, action);
                     });
index 1a807a2..193d2af 100644 (file)
@@ -101,7 +101,6 @@ class behat_field_manager {
      * @return behat_form_field
      */
     public static function get_field_instance($type, NodeElement $fieldnode, Session $session) {
-
         global $CFG;
 
         // If the field is not part of a moodleform, we should still try to find out
@@ -152,6 +151,10 @@ class behat_field_manager {
             $type = $fieldnode->getAttribute('type');
             switch ($type) {
                 case 'text':
+                    if ($fieldtype = $fieldnode->getAttribute('data-fieldtype')) {
+                        return self::normalise_fieldtype($fieldtype);
+                    }
+                    return 'text';
                 case 'password':
                 case 'email':
                 case 'file':
index 2746070..7f2dd0c 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 interface behat_session_interface {
-    /**
-     * Small timeout.
-     *
-     * A reduced timeout for cases where self::TIMEOUT is too much
-     * and a simple $this->getSession()->getPage()->find() could not
-     * be enough.
-     *
-     * @deprecated since Moodle 3.7 MDL-64979 - please use get_reduced_timeout() instead
-     * @todo MDL-64982 This will be deleted in Moodle 3.11
-     * @see behat_base::get_reduced_timeout()
-     */
-    const REDUCED_TIMEOUT = 2;
-
-    /**
-     * The timeout for each Behat step (load page, wait for an element to load...).
-     *
-     * @deprecated since Moodle 3.7 MDL-64979 - please use get_timeout() instead
-     * @todo MDL-64982 This will be deleted in Moodle 3.11
-     * @see behat_base::get_timeout()
-     */
-    const TIMEOUT = 6;
-
-    /**
-     * And extended timeout for specific cases.
-     *
-     * @deprecated since Moodle 3.7 MDL-64979 - please use get_extended_timeout() instead
-     * @todo MDL-64982 This will be deleted in Moodle 3.11
-     * @see behat_base::get_extended_timeout()
-     */
-    const EXTENDED_TIMEOUT = 10;
-
     /**
      * The JS code to check that the page is ready.
      *
index 238bbee..b8144f0 100644 (file)
@@ -48,43 +48,75 @@ class behat_form_autocomplete extends behat_form_text {
             throw new coding_exception('Setting the value of an autocomplete field requires javascript.');
         }
 
-        // Set the value of the autocomplete's input.
-        // If this autocomplete offers suggestions then these should be fetched by setting the value and waiting for the
-        // JS to finish fetching those suggestions.
+        // Clear all current selections.
+        $rootnode = $this->field->getParent()->getParent();
+        $selections = $rootnode->findAll('css', '.form-autocomplete-selection [role=option]');
+        foreach (array_reverse($selections) as $selection) {
+            $selection->click();
+            $this->wait_for_pending_js();
+        }
 
-        $istagelement = $this->field->hasAttribute('data-tags') && $this->field->getAttribute('data-tags');
+        $allowscreation = $this->field->hasAttribute('data-tags') && !empty($this->field->getAttribute('data-tags'));
+        $hasmultiple = $this->field->hasAttribute('data-multiple') && !empty($this->field->getAttribute('data-multiple'));
 
-        if ($istagelement && false !== strpos($value, ',')) {
-            // Commas have a special meaning as a value separator in 'tag' autocomplete elements.
+        if ($hasmultiple && false !== strpos($value, ',')) {
+            // Commas have a special meaning as a value separator in 'multiple' autocomplete elements.
             // To handle this we break the value up by comma, and enter it in chunks.
             $values = explode(',', $value);
 
             while ($value = array_shift($values)) {
-                $this->set_value($value);
+                $this->add_value(trim($value), $allowscreation);
             }
         } else {
-            $this->field->setValue($value);
-            $this->wait_for_pending_js();
+            $this->add_value(trim($value), $allowscreation);
+        }
+    }
 
-            // If the autocomplete found suggestions, then it will have:
-            // 1) marked itself as expanded; and
-            // 2) have an aria-selected suggestion in the list.
-            $expanded = $this->field->getAttribute('aria-expanded');
-            $suggestion = $this->field->getParent()->find('css', '.form-autocomplete-suggestions > [aria-selected="true"]');
-
-            if ($expanded && null !== $suggestion) {
-                // A suggestion was found.
-                // Click on the first item in the list.
-                $suggestion->click();
-            } else {
-                // Press the return key to create a new tag.
-                behat_base::type_keys($this->session, [behat_keys::ENTER]);
-            }
-            $this->wait_for_pending_js();
+    /**
+     * Add a value to the autocomplete.
+     *
+     * @param   string $value
+     * @param   bool $allowscreation
+     */
+    protected function add_value(string $value, bool $allowscreation): void {
+        $value = trim($value);
 
-            // Press the escape to close the autocomplete suggestions list.
-            behat_base::type_keys($this->session, [behat_keys::ESCAPE]);
-            $this->wait_for_pending_js();
+        // Click into the field.
+        $this->field->click();
+
+        // Remove any existing text.
+        do {
+            behat_base::type_keys($this->session, [behat_keys::BACKSPACE, behat_keys::DELETE]);
+        } while (strlen($this->field->getValue()) > 0);
+        $this->wait_for_pending_js();
+
+        // Type in the new value.
+        behat_base::type_keys($this->session, str_split($value));
+        $this->wait_for_pending_js();
+
+        // If the autocomplete found suggestions, then it will have:
+        // 1) marked itself as expanded; and
+        // 2) have an aria-selected suggestion in the list.
+        $expanded = $this->field->getAttribute('aria-expanded');
+        $suggestion = $this->field->getParent()->getParent()->find('css', '.form-autocomplete-suggestions > [aria-selected="true"]');
+
+        if ($expanded && null !== $suggestion) {
+            // A suggestion was found.
+            // Click on the first item in the list.
+            $suggestion->click();
+        } else if ($allowscreation) {
+            // Press the return key to create a new entry.
+            behat_base::type_keys($this->session, [behat_keys::ENTER]);
+        } else {
+            throw new \InvalidArgumentException(
+                "Unable to find '{$value}' in the list of options, and unable to create a new option"
+            );
         }
+
+        $this->wait_for_pending_js();
+
+        // Press the escape to close the autocomplete suggestions list.
+        behat_base::type_keys($this->session, [behat_keys::ESCAPE]);
+        $this->wait_for_pending_js();
     }
 }
diff --git a/lib/classes/encryption.php b/lib/classes/encryption.php
new file mode 100644 (file)
index 0000000..84560f6
--- /dev/null
@@ -0,0 +1,318 @@
+<?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;
+        }
+    }
+}
index 574ea94..6c49aaa 100644 (file)
@@ -473,7 +473,7 @@ class client extends \oauth2_client {
 
         // Store the access token and, if provided by the server, the new refresh token.
         $this->store_token($receivedtokens['access_token']);
-        if (isset($receivedtokens['refreshtoken'])) {
+        if (isset($receivedtokens['refresh_token'])) {
             $systemaccount->set('refreshtoken', $receivedtokens['refresh_token']->token);
             $systemaccount->update();
         }
index 1db678b..023193d 100644 (file)
@@ -84,9 +84,10 @@ class notification implements \renderable, \templatable {
      * Notification constructor.
      *
      * @param string $message the message to print out
-     * @param string $messagetype one of the NOTIFY_* constants..
+     * @param ?string $messagetype one of the NOTIFY_* constants..
+     * @param bool $closebutton Whether to show a close icon to remove the notification (default true).
      */
-    public function __construct($message, $messagetype = null) {
+    public function __construct($message, $messagetype = null, $closebutton = true) {
         $this->message = $message;
 
         if (empty($messagetype)) {
@@ -94,6 +95,8 @@ class notification implements \renderable, \templatable {
         }
 
         $this->messagetype = $messagetype;
+
+        $this->closebutton = $closebutton;
     }
 
     /**
index 559c68f..b9ef74e 100644 (file)
@@ -679,7 +679,6 @@ class manager {
 
         $where = "(lastruntime IS NULL OR lastruntime < :timestart1)
                   AND (nextruntime IS NULL OR nextruntime < :timestart2)
-                  AND disabled = 0
                   ORDER BY lastruntime, id ASC";
         $params = array('timestart1' => $timestart, 'timestart2' => $timestart);
         $records = $DB->get_records_select('task_scheduled', $where, $params);
@@ -688,14 +687,15 @@ class manager {
 
         foreach ($records as $record) {
 
+            $task = self::scheduled_task_from_record($record);
+            // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
+            // Also check to see if task is disabled or enabled after applying overrides.
+            if (!$task || $task->get_disabled()) {
+                continue;
+            }
+
             if ($lock = $cronlockfactory->get_lock(($record->classname), 0)) {
                 $classname = '\\' . $record->classname;
-                $task = self::scheduled_task_from_record($record);
-                // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
-                if (!$task) {
-                    $lock->release();
-                    continue;
-                }
 
                 $task->set_lock($lock);
 
index 4190bf2..0cf405b 100644 (file)
@@ -182,7 +182,7 @@ function cron_run_adhoc_tasks(int $timenow, $keepalive = 0, $checklimits = true)
 
         try {
             $task = \core\task\manager::get_next_adhoc_task(time(), $checklimits);
-        } catch (Exception $e) {
+        } catch (\Throwable $e) {
             if ($adhoclock) {
                 // Release the adhoc task runner lock.
                 $adhoclock->release();
@@ -260,7 +260,7 @@ function cron_run_inner_scheduled_task(\core\task\task_base $task) {
         }
         mtrace('Scheduled task complete: ' . $fullname);
         \core\task\manager::scheduled_task_complete($task);
-    } catch (Exception $e) {
+    } catch (\Throwable $e) {
         if ($DB && $DB->is_transaction_started()) {
             error_log('Database transaction aborted automatically in ' . get_class($task));
             $DB->force_transaction_rollback();
@@ -348,7 +348,7 @@ function cron_run_inner_adhoc_task(\core\task\adhoc_task $task) {
         }
         mtrace("Adhoc task complete: " . get_class($task));
         \core\task\manager::adhoc_task_complete($task);
-    } catch (Exception $e) {
+    } catch (\Throwable $e) {
         if ($DB && $DB->is_transaction_started()) {
             error_log('Database transaction aborted automatically in ' . get_class($task));
             $DB->force_transaction_rollback();
index d24a5c2..685de9e 100644 (file)
@@ -2786,11 +2786,12 @@ function xmldb_main_upgrade($oldversion) {
     if ($oldversion < 2021052500.26) {
         // Delete orphaned course_modules_completion rows; these were not deleted properly
         // by remove_course_contents function.
-        $DB->delete_records_subquery('course_modules_completion', 'id', 'id',
-               "SELECT cmc.id
-                  FROM {course_modules_completion} cmc
-             LEFT JOIN {course_modules} cm ON cm.id = cmc.coursemoduleid
-                 WHERE cm.id IS NULL");
+        $DB->delete_records_select('course_modules_completion', "
+                NOT EXISTS (
+                        SELECT 1
+                          FROM {course_modules} cm
+                         WHERE cm.id = {course_modules_completion}.coursemoduleid
+                )");
         upgrade_main_savepoint(true, 2021052500.26);
     }
 
index 40a154d..1b50ada 100644 (file)
@@ -1795,7 +1795,7 @@ abstract class moodle_database {
      * If the return ID isn't required, then this just reports success as true/false.
      * $data is an object containing needed data
      * @param string $table The database table to be inserted into
-     * @param object $dataobject A data object with values for one or more fields in the record
+     * @param object|array $dataobject A data object with values for one or more fields in the record
      * @param bool $returnid Should the id of the newly created record entry be returned? If this option is not requested then true/false is returned.
      * @param bool $bulk Set to true is multiple inserts are expected
      * @return bool|int true or new id
index 4459502..13eceb3 100644 (file)
@@ -1374,7 +1374,7 @@ class mysqli_native_moodle_database extends moodle_database {
      * If the return ID isn't required, then this just reports success as true/false.
      * $data is an object containing needed data
      * @param string $table The database table to be inserted into
-     * @param object $data A data object with values for one or more fields in the record
+     * @param object|array $dataobject A data object with values for one or more fields in the record
      * @param bool $returnid Should the id of the newly created record entry be returned? If this option is not requested then true/false is returned.
      * @return bool|int true or new id
      * @throws dml_exception A DML specific exception is thrown for any errors.
index ce5ae22..3230f34 100644 (file)
@@ -1284,7 +1284,7 @@ class oci_native_moodle_database extends moodle_database {
      * If the return ID isn't required, then this just reports success as true/false.
      * $data is an object containing needed data
      * @param string $table The database table to be inserted into
-     * @param object $data A data object with values for one or more fields in the record
+     * @param object|array $dataobject A data object with values for one or more fields in the record
      * @param bool $returnid Should the id of the newly created record entry be returned? If this option is not requested then true/false is returned.
      * @return bool|int true or new id
      * @throws dml_exception A DML specific exception is thrown for any errors.
index 7f71905..d17ba24 100644 (file)
@@ -390,7 +390,7 @@ abstract class pdo_moodle_database extends moodle_database {
      * If the return ID isn't required, then this just reports success as true/false.
      * $data is an object containing needed data
      * @param string $table The database table to be inserted into
-     * @param object $data A data object with values for one or more fields in the record
+     * @param object|array $dataobject A data object with values for one or more fields in the record
      * @param bool $returnid Should the id of the newly created record entry be returned? If this option is not requested then true/false is returned.
      * @param bool $bulk true means repeated inserts expected
      * @return bool|int true or new id
index 848cee1..5cb4e74 100644 (file)
@@ -1044,7 +1044,7 @@ class pgsql_native_moodle_database extends moodle_database {
      * If the return ID isn't required, then this just reports success as true/false.
      * $data is an object containing needed data
      * @param string $table The database table to be inserted into
-     * @param object $data A data object with values for one or more fields in the record
+     * @param object|array $dataobject A data object with values for one or more fields in the record
      * @param bool $returnid Should the id of the newly created record entry be returned? If this option is not requested then true/false is returned.
      * @return bool|int true or new id
      * @throws dml_exception A DML specific exception is thrown for any errors.
index 8b1f156..799438b 100644 (file)
@@ -1118,7 +1118,7 @@ class sqlsrv_native_moodle_database extends moodle_database {
      * If the return ID isn't required, then this just reports success as true/false.
      * $data is an object containing needed data
      * @param string $table The database table to be inserted into
-     * @param object $data A data object with values for one or more fields in the record
+     * @param object|array $dataobject A data object with values for one or more fields in the record
      * @param bool $returnid Should the id of the newly created record entry be returned? If this option is not requested then true/false is returned.
      * @return bool|int true or new id
      * @throws dml_exception A DML specific exception is thrown for any errors.
index fcc4bfa..9d3fe04 100644 (file)
@@ -249,7 +249,7 @@ abstract class test_moodle_database extends moodle_database {
     /**
      * Default implementation, throws Exception
      * @param string $table
-     * @param StdObject $dataobject
+     * @param object|array $dataobject
      * @param bool $returnid
      * @param bool $bulk
      * @return bool|int true or new id
diff --git a/lib/form/amd/build/encryptedpassword.min.js b/lib/form/amd/build/encryptedpassword.min.js
new file mode 100644 (file)
index 0000000..069dbc0
Binary files /dev/null and b/lib/form/amd/build/encryptedpassword.min.js differ
diff --git a/lib/form/amd/build/encryptedpassword.min.js.map b/lib/form/amd/build/encryptedpassword.min.js.map
new file mode 100644 (file)
index 0000000..6a62ecf
Binary files /dev/null and b/lib/form/amd/build/encryptedpassword.min.js.map differ
diff --git a/lib/form/amd/src/encryptedpassword.js b/lib/form/amd/src/encryptedpassword.js
new file mode 100644 (file)
index 0000000..2641ff6
--- /dev/null
@@ -0,0 +1,104 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <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;
+};
index d64ad69..7524c00 100644 (file)
                     {{{label}}}
                 {{/text}}
             </label>
-            <span class="ml-2 d-flex align-items-center align-self-start">
+            <div class="ml-2 d-flex align-items-center align-self-start">
                 {{#required}}
                     <div class="text-danger" title="{{#str}}required{{/str}}">
                     {{#pix}}req, core, {{#str}}required{{/str}}{{/pix}}
                     </div>
                 {{/required}}
                 {{{helpbutton}}}
-            </span>
+            </div>
         </div>
         <div class="form-control-feedback invalid-feedback" id="{{element.iderror}}" {{#error}} style="display: block;"{{/error}}>
             {{{error}}}
index 4aacd4f..b3497a4 100644 (file)
                     {{{label}}}
                 {{/text}}
             </label>
-            <span class="ml-2 d-flex align-items-center align-self-start">
+            <div class="ml-2 d-flex align-items-center align-self-start">
                 {{#required}}
                     <div class="text-danger" title="{{#str}}required{{/str}}">
                     {{#pix}}req, core, {{#str}}required{{/str}}{{/pix}}
                     </div>
                 {{/required}}
                 {{{helpbutton}}}
-            </span>
+            </div>
         </div>
         <div class="form-control-feedback invalid-feedback" id="{{element.iderror}}" {{#error}} style="display: block;"{{/error}}>
             {{{error}}}
index 17e557e..0efeff5 100644 (file)
                         {{{label}}}
                     {{/text}}
                 </label>
-                <span class="ml-2 d-flex align-items-center align-self-start">
+                <div class="ml-2 d-flex align-items-center align-self-start">
                     {{#required}}
                         <div class="text-danger" title="{{#str}}required{{/str}}">
                         {{#pix}}req, core, {{#str}}required{{/str}}{{/pix}}
                         </div>
                     {{/required}}
                     {{{helpbutton}}}
-                </span>
+                </div>
         </div>
         <div class="form-control-feedback invalid-feedback" id="{{element.iderror}}" {{#error}} style="display: block;"{{/error}}>
             {{{error}}}
index afa6576..b77408b 100644 (file)
                 </span>
             {{/element.staticlabel}}
         {{/ label }}{{/ label}}
-        <span class="ml-1 ml-md-auto d-flex align-items-center align-self-start">
+        <div class="ml-1 ml-md-auto d-flex align-items-center align-self-start">
             {{#required}}
                 <div class="text-danger" title="{{#str}}required{{/str}}">
                 {{#pix}}req, core, {{#str}}required{{/str}}{{/pix}}
                 </div>
             {{/required}}
             {{{helpbutton}}}
-        </span>
+        </div>
     </div>
     <div class="col-md-9 form-inline align-items-start felement" data-fieldtype="{{element.type}}">
         {{$ element }}
index f279879..9fd9123 100644 (file)
@@ -28,14 +28,14 @@ class Html2Text
     /**
      * Contains the HTML content to convert.
      *
-     * @type string
+     * @var string $html
      */
     protected $html;
 
     /**
      * Contains the converted, formatted text.
      *
-     * @type string
+     * @var string $text
      */
     protected $text;
 
@@ -43,7 +43,7 @@ class Html2Text
      * List of preg* regular expression patterns to search for,
      * used in conjunction with $replace.
      *
-     * @type array
+     * @var array $search
      * @see $replace
      */
     protected $search = array(
@@ -54,6 +54,7 @@ class Html2Text
         '/<style\b[^>]*>.*?<\/style>/i',                  // <style>s -- which strip_tags supposedly has problems with
         '/<i\b[^>]*>(.*?)<\/i>/i',                        // <i>
         '/<em\b[^>]*>(.*?)<\/em>/i',                      // <em>
+        '/<ins\b[^>]*>(.*?)<\/ins>/i',                    // <ins>
         '/(<ul\b[^>]*>|<\/ul>)/i',                        // <ul> and </ul>
         '/(<ol\b[^>]*>|<\/ol>)/i',                        // <ol> and </ol>
         '/(<dl\b[^>]*>|<\/dl>)/i',                        // <dl> and </dl>
@@ -73,7 +74,7 @@ class Html2Text
     /**
      * List of pattern replacements corresponding to patterns searched.
      *
-     * @type array
+     * @var array $replace
      * @see $search
      */
     protected $replace = array(
@@ -84,6 +85,7 @@ class Html2Text
         '',                              // <style>s -- which strip_tags supposedly has problems with
         '_\\1_',                         // <i>
         '_\\1_',                         // <em>
+        '_\\1_',                         // <ins>
         "\n\n",                          // <ul> and </ul>
         "\n\n",                          // <ol> and </ol>
         "\n\n",                          // <dl> and </dl>
@@ -104,7 +106,7 @@ class Html2Text
      * List of preg* regular expression patterns to search for,
      * used in conjunction with $entReplace.
      *
-     * @type array
+     * @var array $entSearch
      * @see $entReplace
      */
     protected $entSearch = array(
@@ -118,7 +120,7 @@ class Html2Text
     /**
      * List of pattern replacements corresponding to patterns searched.
      *
-     * @type array
+     * @var array $entReplace
      * @see $entSearch
      */
     protected $entReplace = array(
@@ -133,7 +135,7 @@ class Html2Text
      * List of preg* regular expression patterns to search for
      * and replace using callback function.
      *
-     * @type array
+     * @var array $callbackSearch
      */
     protected $callbackSearch = array(
         '/<(h)[123456]( [^>]*)?>(.*?)<\/h[123456]>/i',           // h1 - h6
@@ -141,6 +143,7 @@ class Html2Text
         '/<(br)[^>]*>[ ]*/i',                                    // <br> with leading whitespace after the newline.
         '/<(b)( [^>]*)?>(.*?)<\/b>/i',                           // <b>
         '/<(strong)( [^>]*)?>(.*?)<\/strong>/i',                 // <strong>
+        '/<(del)( [^>]*)?>(.*?)<\/del>/i',                       // <del>
         '/<(th)( [^>]*)?>(.*?)<\/th>/i',                         // <th> and </th>
         '/<(a) [^>]*href=("|\')([^"\']+)\2([^>]*)>(.*?)<\/a>/i'  // <a href="">
     );
@@ -149,7 +152,7 @@ class Html2Text
      * List of preg* regular expression patterns to search for in PRE body,
      * used in conjunction with $preReplace.
      *
-     * @type array
+     * @var array $preSearch
      * @see $preReplace
      */
     protected $preSearch = array(
@@ -163,7 +166,7 @@ class Html2Text
     /**
      * List of pattern replacements corresponding to patterns searched for PRE body.
      *
-     * @type array
+     * @var array $preReplace
      * @see $preSearch
      */
     protected $preReplace = array(
@@ -177,21 +180,21 @@ class Html2Text
     /**
      * Temporary workspace used during PRE processing.
      *
-     * @type string
+     * @var string $preContent
      */
     protected $preContent = '';
 
     /**
      * Contains the base URL that relative links should resolve to.
      *
-     * @type string
+     * @var string $baseurl
      */
     protected $baseurl = '';
 
     /**
      * Indicates whether content in the $html variable has been converted yet.
      *
-     * @type boolean
+     * @var boolean $converted
      * @see $html, $text
      */
     protected $converted = false;
@@ -199,7 +202,7 @@ class Html2Text
     /**
      * Contains URL addresses from links to be rendered in plain text.
      *
-     * @type array
+     * @var array $linkList
      * @see buildlinkList()
      */
     protected $linkList = array();
@@ -207,7 +210,7 @@ class Html2Text
     /**
      * Various configuration options (able to be set in the constructor)
      *
-     * @type array
+     * @var array $options
      */
     protected $options = array(
         'do_links' => 'inline', // 'none'
@@ -281,7 +284,7 @@ class Html2Text
     /**
      * Returns the text, converted from HTML.
      *
-     * @return string
+     * @return string Plain text
      */
     public function getText()
     {
@@ -414,7 +417,7 @@ class Html2Text
         }
 
         // Ignored link types
-        if (preg_match('!^(javascript:|mailto:|#)!i', $link)) {
+        if (preg_match('!^(javascript:|mailto:|#)!i', html_entity_decode($link))) {
             return $display;
         }
 
@@ -450,6 +453,11 @@ class Html2Text
         }
     }
 
+    /**
+     * Helper function for PRE body conversion.
+     *
+     * @param string &$text HTML content
+     */
     protected function convertPre(&$text)
     {
         // get the content of PRE element
@@ -486,7 +494,7 @@ class Html2Text
     /**
      * Helper function for BLOCKQUOTE body conversion.
      *
-     * @param string $text HTML content
+     * @param string &$text HTML content
      */
     protected function convertBlockquotes(&$text)
     {
@@ -563,6 +571,8 @@ class Html2Text
             case 'b':
             case 'strong':
                 return $this->toupper($matches[3]);
+            case 'del':
+                return $this->tostrike($matches[3]);
             case 'th':
                 return $this->toupper("\t\t" . $matches[3] . "\n");
             case 'h':
@@ -628,4 +638,21 @@ class Html2Text
 
         return $str;
     }
+
+    /**
+     * Helper function for DEL conversion.
+     *
+     * @param  string $text HTML content
+     * @return string Converted text
+     */
+    protected function tostrike($str)
+    {
+        $rtn = '';
+        for ($i = 0; $i < mb_strlen($str); $i++) {
+            $chr = mb_substr($str, $i, 1);
+            $combiningChr = chr(0xC0 | 0x336 >> 6). chr(0x80 | 0x336 & 0x3F);
+            $rtn .= $chr . $combiningChr;
+        }
+        return $rtn;
+    }
 }
index 111f11a..37cab25 100644 (file)
@@ -1,4 +1,4 @@
-Description of Html2Text library import into Moodle
+Description of Html2Text v4.3.1 library import into Moodle
 
 Please note that we override some mb_* functions in Html2Text's namespace at
 run time. Until Html2Text adds some sort of fallback for the mb_* functions
@@ -7,7 +7,8 @@ running PHP without mbstring don't see nasty undefined function errors.
 
 Instructions
 ------------
-1. Clone https://github.com/mtibben/html2text.git into an unrelated directory
-2. Copy /path/to/html2text/src/Html2Text.php to lib/html2text/
+1. Download the latest release of Html2Text from https://github.com/mtibben/html2text/releases/
+2. Extract the contents of the release archive into a directory.
+3. Copy src/Html2Text.php to lib/html2text/
 
-Imported from: https://github.com/mtibben/html2text.git
+Imported from: https://github.com/mtibben/html2text/releases/
index b0f5114..59fcd1c 100644 (file)
@@ -3906,13 +3906,19 @@ function get_enabled_auth_plugins($fix=false) {
 
     if ($fix) {
         $auths = array_unique($auths);
+        $oldauthconfig = implode(',', $auths);
         foreach ($auths as $k => $authname) {
-            if (!exists_auth_plugin($authname) or in_array($authname, $default)) {
+            $authplugindoesnotexist = !exists_auth_plugin($authname);
+            if ($authplugindoesnotexist || in_array($authname, $default)) {
+                if ($authplugindoesnotexist) {
+                    debugging(get_string('authpluginnotfound', 'debug', $authname));
+                }
                 unset($auths[$k]);
             }
         }
         $newconfig = implode(',', $auths);
         if (!isset($CFG->auth) or $newconfig != $CFG->auth) {
+            add_to_config_log('auth', $oldauthconfig, $newconfig, 'core');
             set_config('auth', $newconfig);
         }
     }
index 6a64c28..6b61cab 100644 (file)
@@ -2875,10 +2875,11 @@ EOD;
      * Note: \core\notification::add() may be more suitable for your usage.
      *
      * @param string $message The message to print out.
-     * @param string $type    The type of notification. See constants on \core\output\notification.
+     * @param ?string $type   The type of notification. See constants on \core\output\notification.
+     * @param bool $closebutton Whether to show a close icon to remove the notification (default true).
      * @return string the HTML to output.
      */
-    public function notification($message, $type = null) {
+    public function notification($message, $type = null, $closebutton = true) {
         $typemappings = [
             // Valid types.
             'success'           => \core\output\notification::NOTIFY_SUCCESS,
@@ -2922,7 +2923,7 @@ EOD;
             }
         }
 
-        $notification = new \core\output\notification($message, $type);
+        $notification = new \core\output\notification($message, $type, $closebutton);
         if (count($extraclasses)) {
             $notification->set_extra_classes($extraclasses);
         }
@@ -4886,9 +4887,10 @@ class core_renderer_cli extends core_renderer {
      *
      * @param string $message The message to print out.
      * @param string $type    The type of notification. See constants on \core\output\notification.
+     * @param bool $closebutton Whether to show a close icon to remove the notification (default true).
      * @return string A template fragment for a notification
      */
-    public function notification($message, $type = null) {
+    public function notification($message, $type = null, $closebutton = true) {
         $message = clean_text($message);
         if ($type === 'notifysuccess' || $type === 'success') {
             return "++ $message ++\n";
@@ -4972,8 +4974,10 @@ class core_renderer_ajax extends core_renderer {
      *
      * @param string $message The message to print out.
      * @param string $type    The type of notification. See constants on \core\output\notification.
+     * @param bool $closebutton Whether to show a close icon to remove the notification (default true).
      */
-    public function notification($message, $type = null) {}
+    public function notification($message, $type = null, $closebutton = true) {
+    }
 
     /**
      * Used to display a redirection message.
index 7bca2a7..d18b9f5 100644 (file)
@@ -319,20 +319,21 @@ class page_requirements_manager {
             }
 
             $this->M_cfg = array(
-                'wwwroot'             => $CFG->wwwroot,
-                'sesskey'             => sesskey(),
-                'sessiontimeout'      => $CFG->sessiontimeout,
-                'themerev'            => theme_get_revision(),
-                'slasharguments'      => (int)(!empty($CFG->slasharguments)),
-                'theme'               => $page->theme->name,
-                'iconsystemmodule'    => $iconsystem->get_amd_name(),
-                'jsrev'               => $this->get_jsrev(),
-                'admin'               => $CFG->admin,
-                'svgicons'            => $page->theme->use_svg_icons(),
-                'usertimezone'        => usertimezone(),
-                'contextid'           => $contextid,
-                'langrev'             => get_string_manager()->get_revision(),
-                'templaterev'         => $this->get_templaterev()
+                'wwwroot'               => $CFG->wwwroot,
+                'sesskey'               => sesskey(),
+                'sessiontimeout'        => $CFG->sessiontimeout,
+                'sessiontimeoutwarning' => $CFG->sessiontimeoutwarning,
+                'themerev'              => theme_get_revision(),
+                'slasharguments'        => (int)(!empty($CFG->slasharguments)),
+                'theme'                 => $page->theme->name,
+                'iconsystemmodule'      => $iconsystem->get_amd_name(),
+                'jsrev'                 => $this->get_jsrev(),
+                'admin'                 => $CFG->admin,
+                'svgicons'              => $page->theme->use_svg_icons(),
+                'usertimezone'          => usertimezone(),
+                'contextid'             => $contextid,
+                'langrev'               => get_string_manager()->get_revision(),
+                'templaterev'           => $this->get_templaterev()
             );
             if ($CFG->debugdeveloper) {
                 $this->M_cfg['developerdebug'] = true;
index 1326f13..210bdee 100644 (file)
@@ -801,6 +801,10 @@ if (CLI_SCRIPT) {
 if (empty($CFG->sessiontimeout)) {
     $CFG->sessiontimeout = 8 * 60 * 60;
 }
+// Set sessiontimeoutwarning 20 minutes.
+if (empty($CFG->sessiontimeoutwarning)) {
+    $CFG->sessiontimeoutwarning = 20 * 60;
+}
 \core\session\manager::start();
 
 // Set default content type and encoding, developers are still required to use
index a822a4b..6503e83 100644 (file)
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2007, Ryan Parman and Geoffrey Sneddon.
+Copyright (c) 2004-2007, Ryan Parman and Sam Sneddon.
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without modification, are 
index c346204..5522b7b 100644 (file)
@@ -11,9 +11,7 @@ compatibility and standards compliance][what_is].
 
 Requirements
 ------------
-* PHP 5.3.0+ (5.3.6+ recommended since SimplePie 1.4.2)
-       * Support for PHP 5.2 stopped in branch `one-dot-three`
-       * Support for PHP 4 stopped in branch `one-dot-two`
+* PHP 5.6+ (Required since SimplePie 1.5.3)
 * libxml2 (certain 2.7.x releases are too buggy for words, and will crash)
 * One of iconv, mbstring or intl extensions
 * cURL or fsockopen()
@@ -89,14 +87,14 @@ Authors and contributors
 ### Alumni
 * [Ryan McCue][] (developer, support)
 * [Ryan Parman][] (Creator, developer, evangelism, support)
-* [Geoffrey Sneddon][] (Lead developer)
+* [Sam Sneddon][] (Lead developer)
 * [Michael Shipley][] (Submitter of patches, support)
 * [Steve Minutillo][] (Submitter of patches)
 
-[Malcolm Blaney]: https://unicyclic.com/mal
+[Malcolm Blaney]: https://mblaney.xyz
 [Ryan McCue]: http://ryanmccue.info
 [Ryan Parman]: http://ryanparman.com
-[Geoffrey Sneddon]: http://gsnedders.com
+[Sam Sneddon]: https://gsnedders.com
 [Michael Shipley]: http://michaelpshipley.com
 [Steve Minutillo]: http://minutillo.com/steve/
 
index fd7690d..e42f07b 100644 (file)
@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
- * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Sam Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,9 +33,9 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Sam Sneddon, Ryan McCue
  * @author Ryan Parman
- * @author Geoffrey Sneddon
+ * @author Sam Sneddon
  * @author Ryan McCue
  * @link http://simplepie.org/ SimplePie
  * @license http://www.opensource.org/licenses/bsd-license.php BSD License
index c4c6f61..a6a5a9e 100644 (file)
@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
- * Copyright (c) 2004-2017, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2017, Ryan Parman, Sam Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without modification, are
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @version 1.5.3
- * @copyright 2004-2017 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @version 1.5.6
+ * @copyright 2004-2017 Ryan Parman, Sam Sneddon, Ryan McCue
  * @author Ryan Parman
- * @author Geoffrey Sneddon
+ * @author Sam Sneddon
  * @author Ryan McCue
  * @link http://simplepie.org/ SimplePie
  * @license http://www.opensource.org/licenses/bsd-license.php BSD License
@@ -50,7 +50,7 @@ define('SIMPLEPIE_NAME', 'SimplePie');
 /**
  * SimplePie Version
  */
-define('SIMPLEPIE_VERSION', '1.5.3');
+define('SIMPLEPIE_VERSION', '1.5.6');
 
 /**
  * SimplePie Build
@@ -665,9 +665,9 @@ class SimplePie
         */
        public function __construct()
        {
-               if (version_compare(PHP_VERSION, '5.3', '<'))
+               if (version_compare(PHP_VERSION, '5.6', '<'))
                {
-                       trigger_error('Please upgrade to PHP 5.3 or newer.');
+                       trigger_error('Please upgrade to PHP 5.6 or newer.');
                        die();
                }
 
@@ -706,7 +706,7 @@ class SimplePie
         */
        public function __destruct()
        {
-               if ((version_compare(PHP_VERSION, '5.3', '<') || !gc_enabled()) && !ini_get('zend.ze1_compatibility_mode'))
+               if (!gc_enabled())
                {
                        if (!empty($this->data['items']))
                        {
@@ -1251,8 +1251,8 @@ class SimplePie
        /**
         * Set the handler to enable the display of cached images.
         *
-        * @param str $page Web-accessible path to the handler_image.php file.
-        * @param str $qs The query string that the value should be passed to.
+        * @param string $page Web-accessible path to the handler_image.php file.
+        * @param string $qs The query string that the value should be passed to.
         */
        public function set_image_handler($page = false, $qs = 'i')
        {
@@ -1373,7 +1373,8 @@ class SimplePie
                        // Decide whether to enable caching
                        if ($this->cache && $parsed_feed_url['scheme'] !== '')
                        {
-                               $cache = $this->registry->call('Cache', 'get_handler', array($this->cache_location, call_user_func($this->cache_name_function, $this->feed_url), 'spc'));
+                               $url = $this->feed_url . ($this->force_feed ? '#force_feed' : '');
+                               $cache = $this->registry->call('Cache', 'get_handler', array($this->cache_location, call_user_func($this->cache_name_function, $url), 'spc'));
                        }
 
                        // Fetch the data via SimplePie_File into $this->raw_data
@@ -1712,8 +1713,8 @@ class SimplePie
                                        }
                                        $cache = $this->registry->call('Cache', 'get_handler', array($this->cache_location, call_user_func($this->cache_name_function, $file->url), 'spc'));
                                }
-                               $this->feed_url = $file->url;
                        }
+                       $this->feed_url = $file->url;
                        $locate = null;
                }
 
@@ -1911,7 +1912,8 @@ class SimplePie
         *
         * When the 'permanent' mode is disabled (default),
         * may or may not be different from the URL passed to {@see set_feed_url()},
-        * depending on whether auto-discovery was used.
+        * depending on whether auto-discovery was used, and whether there were
+        * any redirects along the way.
         *
         * @since Preview Release (previously called `get_feed_url()` since SimplePie 0.8.)
         * @todo Support <itunes:new-feed-url>
index 14794cf..563932f 100644 (file)
@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
- * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Sam Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,9 +33,9 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Sam Sneddon, Ryan McCue
  * @author Ryan Parman
- * @author Geoffrey Sneddon
+ * @author Sam Sneddon
  * @author Ryan McCue
  * @link http://simplepie.org/ SimplePie
  * @license http://www.opensource.org/licenses/bsd-license.php BSD License
index d98cc65..9c5577d 100644 (file)
@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
- * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Sam Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,9 +33,9 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Sam Sneddon, Ryan McCue
  * @author Ryan Parman
- * @author Geoffrey Sneddon
+ * @author Sam Sneddon
  * @author Ryan McCue
  * @link http://simplepie.org/ SimplePie
  * @license http://www.opensource.org/licenses/bsd-license.php BSD License
index 333fb05..522ff7e 100644 (file)
@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
- * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Sam Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,9 +33,9 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Sam Sneddon, Ryan McCue
  * @author Ryan Parman
- * @author Geoffrey Sneddon
+ * @author Sam Sneddon
  * @author Ryan McCue
  * @link http://simplepie.org/ SimplePie
  * @license http://www.opensource.org/licenses/bsd-license.php BSD License
index 7e8f775..74d57b8 100644 (file)
@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
- * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Sam Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,9 +33,9 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Sam Sneddon, Ryan McCue
  * @author Ryan Parman
- * @author Geoffrey Sneddon
+ * @author Sam Sneddon
  * @author Ryan McCue
  * @link http://simplepie.org/ SimplePie
  * @license http://www.opensource.org/licenses/bsd-license.php BSD License
index a09dea6..03758e9 100644 (file)
@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
- * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Sam Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,9 +33,9 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Sam Sneddon, Ryan McCue
  * @author Ryan Parman
- * @author Geoffrey Sneddon
+ * @author Sam Sneddon
  * @author Ryan McCue
  * @link http://simplepie.org/ SimplePie
  * @license http://www.opensource.org/licenses/bsd-license.php BSD License
index 5190eef..caf7852 100644 (file)
@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
- * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Sam Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,9 +33,9 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Sam Sneddon, Ryan McCue
  * @author Ryan Parman
- * @author Geoffrey Sneddon
+ * @author Sam Sneddon
  * @author Ryan McCue
  * @link http://simplepie.org/ SimplePie
  * @license http://www.opensource.org/licenses/bsd-license.php BSD License
index 1f73b38..0b40d87 100644 (file)
@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
- * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Sam Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,9 +33,9 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Sam Sneddon, Ryan McCue
  * @author Ryan Parman
- * @author Geoffrey Sneddon
+ * @author Sam Sneddon
  * @author Ryan McCue
  * @link http://simplepie.org/ SimplePie
  * @license http://www.opensource.org/licenses/bsd-license.php BSD License
index 061ed04..a684eb8 100644 (file)
@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
- * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Sam Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,9 +33,9 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Sam Sneddon, Ryan McCue
  * @author Ryan Parman
- * @author Geoffrey Sneddon
+ * @author Sam Sneddon
  * @author Ryan McCue
  * @link http://simplepie.org/ SimplePie
  * @license http://www.opensource.org/licenses/bsd-license.php BSD License
index 8548576..3d7bfdd 100644 (file)
@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
- * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Sam Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,9 +33,9 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Sam Sneddon, Ryan McCue
  * @author Ryan Parman
- * @author Geoffrey Sneddon
+ * @author Sam Sneddon
  * @author Ryan McCue
  * @link http://simplepie.org/ SimplePie
  * @license http://www.opensource.org/licenses/bsd-license.php BSD License
index df0f13f..e4dabed 100644 (file)
@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
- * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Sam Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,9 +33,9 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Sam Sneddon, Ryan McCue
  * @author Ryan Parman
- * @author Geoffrey Sneddon
+ * @author Sam Sneddon
  * @author Ryan McCue
  * @link http://simplepie.org/ SimplePie
  * @license http://www.opensource.org/licenses/bsd-license.php BSD License
index b86dfa3..027e131 100644 (file)
@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
- * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Sam Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,9 +33,9 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Sam Sneddon, Ryan McCue
  * @author Ryan Parman
- * @author Geoffrey Sneddon
+ * @author Sam Sneddon
  * @author Ryan McCue
  * @link http://simplepie.org/ SimplePie
  * @license http://www.opensource.org/licenses/bsd-license.php BSD License
@@ -150,7 +150,7 @@ class SimplePie_Content_Type_Sniffer
                }
                elseif (preg_match('/[\x00-\x08\x0E-\x1A\x1C-\x1F]/', $this->file->body))
                {
-                       return 'application/octect-stream';
+                       return 'application/octet-stream';
                }
 
                return 'text/plain';
index a57f323..92f9b09 100644 (file)
@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
- * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Sam Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,9 +33,9 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Sam Sneddon, Ryan McCue
  * @author Ryan Parman
- * @author Geoffrey Sneddon
+ * @author Sam Sneddon
  * @author Ryan McCue
  * @link http://simplepie.org/ SimplePie
  * @license http://www.opensource.org/licenses/bsd-license.php BSD License
index c856ba3..ffcca46 100644 (file)
@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
- * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Sam Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,9 +33,9 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Sam Sneddon, Ryan McCue
  * @author Ryan Parman
- * @author Geoffrey Sneddon
+ * @author Sam Sneddon
  * @author Ryan McCue
  * @link http://simplepie.org/ SimplePie
  * @license http://www.opensource.org/licenses/bsd-license.php BSD License
index 064a1b8..d6ff07e 100644 (file)
@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
- * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Sam Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,9 +33,9 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Sam Sneddon, Ryan McCue
  * @author Ryan Parman
- * @author Geoffrey Sneddon
+ * @author Sam Sneddon
  * @author Ryan McCue
  * @link http://simplepie.org/ SimplePie
  * @license http://www.opensource.org/licenses/bsd-license.php BSD License
index 773481a..a43c374 100644 (file)
@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
- * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Sam Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,9 +33,9 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Sam Sneddon, Ryan McCue
  * @author Ryan Parman
- * @author Geoffrey Sneddon
+ * @author Sam Sneddon
  * @author Ryan McCue
  * @link http://simplepie.org/ SimplePie
  * @license http://www.opensource.org/licenses/bsd-license.php BSD License
index ddbbc3c..32216d8 100644 (file)
@@ -5,7 +5,7 @@
  * A PHP-Based RSS and Atom Feed Framework.
  * Takes the hard work out of managing a complete RSS/Atom solution.
  *
- * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
+ * Copyright (c) 2004-2016, Ryan Parman, Sam Sneddon, Ryan McCue, and contributors
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without modification, are
@@ -33,9 +33,9 @@
  * POSSIBILITY OF SUCH DAMAGE.
  *
  * @package SimplePie
- * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
+ * @copyright 2004-2016 Ryan Parman, Sam Sneddon, Ryan McCue
  * @author Ryan Parman
- * @author Geoffrey Sneddon
+ * @author Sam Sneddon
  * @author Ryan McCue
  * @link http://simplepie.org/ SimplePie
  * @license http://www.opensource.org/licenses/bsd-license.php BSD License