Merge branch 'MDL-58544-master' of git://github.com/damyon/moodle
authorDan Poltawski <dan@moodle.com>
Mon, 10 Jul 2017 07:36:40 +0000 (08:36 +0100)
committerDan Poltawski <dan@moodle.com>
Mon, 10 Jul 2017 07:36:40 +0000 (08:36 +0100)
167 files changed:
.travis.yml
admin/category.php
admin/cli/build_theme_css.php [new file with mode: 0644]
admin/settings/security.php
admin/tool/filetypes/lang/en/tool_filetypes.php
admin/tool/log/store/database/classes/log/store.php
admin/tool/log/store/database/lang/en/logstore_database.php
admin/tool/log/store/database/settings.php
admin/tool/log/store/database/test_settings.php
admin/tool/log/store/database/tests/store_test.php
admin/tool/log/store/database/upgrade.txt [new file with mode: 0644]
admin/tool/log/store/database/version.php
admin/tool/lp/templates/competency_grader.mustache
admin/tool/lp/templates/competency_rule_config.mustache
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/mobile/lib.php
admin/tool/mobile/settings.php
admin/tool/oauth2/lang/en/tool_oauth2.php
admin/tool/task/lang/en/tool_task.php
auth/cas/CAS/CAS/Client.php
auth/cas/CAS/moodle_readme.txt
auth/classes/external.php
auth/oauth2/classes/auth.php
backup/backup.php
backup/import.php
backup/restore.php
blocks/login/block_login.php
blocks/myoverview/classes/output/courses_view.php
blocks/myoverview/lang/en/block_myoverview.php
blocks/participants/tests/behat/block_participants_course.feature
cache/classes/factory.php
cache/classes/helper.php
cache/tests/cache_test.php
calendar/classes/local/event/container.php
calendar/export.php
calendar/lib.php
calendar/managesubscriptions.php
calendar/renderer.php
calendar/tests/container_test.php
calendar/view.php
course/lib.php
course/publish/backup.php
course/renderer.php
course/switchrole.php
course/tests/externallib_test.php
dataformat/html/classes/writer.php
dataformat/json/classes/writer.php
dataformat/upgrade.txt
enrol/guest/locallib.php
enrol/ldap/lib.php
enrol/self/locallib.php
grade/edit/outcome/export.php
grade/edit/outcome/import.php
grade/edit/scale/edit.php
grade/edit/scale/index.php
grade/edit/settings/index.php
grade/edit/tree/action.php
grade/edit/tree/calculation.php
grade/edit/tree/category.php
grade/edit/tree/grade.php
grade/edit/tree/index.php
grade/edit/tree/item.php
grade/edit/tree/outcomeitem.php
grade/export/keymanager.php
grade/export/ods/dump.php
grade/export/ods/export.php
grade/export/ods/index.php
grade/export/txt/dump.php
grade/export/txt/export.php
grade/export/txt/index.php
grade/export/xls/dump.php
grade/export/xls/export.php
grade/export/xls/index.php
grade/export/xml/dump.php
grade/export/xml/export.php
grade/export/xml/index.php
grade/import/csv/index.php
grade/import/direct/index.php
grade/import/keymanager.php
grade/import/xml/fetch.php
grade/import/xml/import.php
grade/import/xml/index.php
grade/report/grader/ajax_callbacks.php
grade/report/grader/index.php
grade/report/grader/preferences.php
grade/report/grader/quickedit_item.php
grade/report/index.php
grade/report/outcomes/index.php
grade/report/overview/index.php
grade/report/singleview/index.php
grade/report/user/index.php
group/groupings.php
install/lang/no/install.php
install/lang/pt_br/error.php
lang/en/admin.php
lang/en/blog.php
lang/en/calendar.php
lang/en/hub.php
lang/en/repository.php
lib/adminlib.php
lib/amd/build/ajax.min.js
lib/amd/build/notification.min.js
lib/amd/src/ajax.js
lib/amd/src/notification.js
lib/classes/dataformat/base.php
lib/classes/dataformat/spout_base.php
lib/classes/output/icon_system_fontawesome.php
lib/classes/output/mustache_javascript_helper.php
lib/classes/plugin_manager.php
lib/csslib.php
lib/dataformatlib.php
lib/db/services.php
lib/db/upgrade.php
lib/filelib.php
lib/navigationlib.php
lib/outputcomponents.php
lib/outputlib.php
lib/outputrenderers.php
lib/tablelib.php
lib/templates/loginform.mustache
lib/tests/admintree_test.php
lib/tests/behat/alpha_chooser.feature
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-debug.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-min.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception.js
lib/yui/src/notification/js/exception.js
login/forgot_password_form.php
login/index.php
login/lib.php
login/signup.php
login/signup_form.php
login/tests/lib_test.php [new file with mode: 0644]
mod/assign/submission/file/lang/en/assignsubmission_file.php
mod/data/field/latlong/field.class.php
mod/feedback/lang/en/feedback.php
mod/forum/user.php
mod/glossary/lang/en/glossary.php
mod/lesson/lang/en/lesson.php
mod/lesson/locallib.php
mod/lesson/pagetypes/branchtable.php
mod/lesson/view.php
mod/quiz/report.php
mod/quiz/report/statistics/report.php
mod/resource/locallib.php
mod/workshop/lang/en/workshop.php
mod/workshop/submission_form.php
phpunit.xml.dist
pluginfile.php
question/type/multichoice/lang/en/qtype_multichoice.php
report/log/tests/behat/filter_log.feature
report/log/tests/behat/user_log.feature
repository/googledocs/lib.php
repository/onedrive/lib.php
repository/repository_callback.php
tag/classes/collection.php
theme/boost/config.php
theme/boost/scss/moodle/core.scss
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/style/moodle.css
theme/more/config.php
theme/styles.php
user/classes/participants_table.php [new file with mode: 0644]
user/index.php
user/lib.php
user/tests/behat/view_full_profile.feature
user/tests/userlib_test.php
version.php

index ff2b674..d58effa 100644 (file)
@@ -2,7 +2,7 @@
 # process (which uses our internal CI system) this file is here for the benefit
 # of community developers git clones - see MDL-51458.
 
-sudo: false
+sudo: required
 
 # We currently disable Travis notifications entirely until https://github.com/travis-ci/travis-ci/issues/4976
 # is fixed.
@@ -18,6 +18,10 @@ php:
 
 addons:
   postgresql: "9.3"
+  packages:
+    - mysql-server-5.6
+    - mysql-client-core-5.6
+    - mysql-client-5.6
 
 services:
     - redis-server
@@ -73,6 +77,28 @@ cache:
       - $HOME/.npm
 
 install:
+    - sudo apt-get -y install haveged
+    - sudo service haveged start
+    - >
+        if [ "$DB" = 'mysqli' ];
+        then
+            sudo mkdir /mnt/ramdisk
+            sudo mount -t tmpfs -o size=1024m tmpfs /mnt/ramdisk
+            sudo stop mysql
+            sudo mv /var/lib/mysql /mnt/ramdisk
+            sudo ln -s /mnt/ramdisk/mysql /var/lib/mysql
+            sudo start mysql
+        fi
+    - >
+        if [ "$DB" = 'pgsql' ];
+        then
+            sudo mkdir /mnt/ramdisk
+            sudo mount -t tmpfs -o size=1024m tmpfs /mnt/ramdisk
+            sudo service postgresql stop
+            sudo mv /var/lib/postgresql /mnt/ramdisk
+            sudo ln -s /mnt/ramdisk/postgresql /var/lib/postgresql
+            sudo service postgresql start 9.3
+        fi
     - >
         if [ "$TASK" = 'PHPUNIT' ];
         then
index cb83e0d..0a80394 100644 (file)
@@ -89,7 +89,7 @@ if ($PAGE->user_allowed_editing()) {
 $savebutton = false;
 $outputhtml = '';
 foreach ($settingspage->children as $childpage) {
-    if ($childpage->is_hidden()) {
+    if ($childpage->is_hidden() || !$childpage->check_access()) {
         continue;
     }
     if ($childpage instanceof admin_externalpage) {
diff --git a/admin/cli/build_theme_css.php b/admin/cli/build_theme_css.php
new file mode 100644 (file)
index 0000000..30cd996
--- /dev/null
@@ -0,0 +1,119 @@
+<?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/>.
+
+/**
+ * Build and store theme CSS.
+ *
+ * @package    core
+ * @subpackage cli
+ * @copyright  2017 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+
+require(__DIR__.'/../../config.php');
+require_once("$CFG->libdir/clilib.php");
+require_once("$CFG->libdir/csslib.php");
+require_once("$CFG->libdir/outputlib.php");
+
+$longparams = [
+    'themes'    => null,
+    'direction' => null,
+    'help'      => false,
+    'verbose'   => false
+];
+
+$shortmappings = [
+    't' => 'themes',
+    'd' => 'direction',
+    'h' => 'help',
+    'v' => 'verbose'
+];
+
+// Get CLI params.
+list($options, $unrecognized) = cli_get_params($longparams, $shortmappings);
+
+if ($unrecognized) {
+    $unrecognized = implode("\n  ", $unrecognized);
+    cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
+}
+
+if ($options['help']) {
+    echo
+"Compile the CSS for one or more installed themes.
+Existing CSS caches will replaced.
+By default all themes will be recompiled unless otherwise specified.
+
+Options:
+-t, --themes    A comma separated list of themes to be compiled
+-d, --direction Only compile a single direction (either ltr or rtl)
+-v, --verbose   Print info comments to stdout
+-h, --help      Print out this help
+
+Example:
+\$ sudo -u www-data /usr/bin/php admin/cli/build_theme_css.php --themes=boost --direction=ltr
+";
+
+    die;
+}
+
+if (empty($options['verbose'])) {
+    $trace = new null_progress_trace();
+} else {
+    $trace = new text_progress_trace();
+}
+
+cli_heading('Build theme css');
+
+// Determine which themes we need to build.
+$themenames = [];
+if (is_null($options['themes'])) {
+    $trace->output('No themes specified. Finding all installed themes.');
+    $themenames = array_keys(core_component::get_plugin_list('theme'));
+} else {
+    if (is_string($options['themes'])) {
+        $themenames = explode(',', $options['themes']);
+    } else {
+        cli_error('--themes must be a comma separated list of theme names');
+    }
+}
+
+$trace->output('Checking that each theme is correctly installed...');
+$themeconfigs = [];
+foreach ($themenames as $themename) {
+    if (is_null(theme_get_config_file_path($themename))) {
+        cli_error("Unable to find theme config for {$themename}");
+    }
+
+    // Load the config for the theme.
+    $themeconfigs[] = theme_config::load($themename);
+}
+
+$directions = ['ltr', 'rtl'];
+
+if (!is_null($options['direction'])) {
+    if (!in_array($options['direction'], $directions)) {
+         cli_error("--direction must be either ltr or rtl");
+    }
+
+    $directions = [$options['direction']];
+}
+
+$trace->output('Building CSS for themes: ' . implode(', ', $themenames));
+theme_build_css_for_themes($themeconfigs, $directions);
+
+exit(0);
index c36a424..34185cb 100644 (file)
@@ -18,6 +18,11 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $temp->add(new admin_setting_configcheckbox('forceloginforprofiles', new lang_string('forceloginforprofiles', 'admin'), new lang_string('configforceloginforprofiles', 'admin'), 1));
     $temp->add(new admin_setting_configcheckbox('forceloginforprofileimage', new lang_string('forceloginforprofileimage', 'admin'), new lang_string('forceloginforprofileimage_help', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('opentogoogle', new lang_string('opentogoogle', 'admin'), new lang_string('configopentogoogle', 'admin'), 0));
+    $temp->add(new admin_setting_configselect('allowindexing', new lang_string('allowindexing', 'admin'), new lang_string('allowindexing_desc', 'admin'),
+        0,
+        array(0 => new lang_string('allowindexingexceptlogin', 'admin'),
+              1 => new lang_string('allowindexingeverywhere', 'admin'),
+              2 => new lang_string('allowindexingnowhere', 'admin'))));
     $temp->add(new admin_setting_pickroles('profileroles',
         new lang_string('profileroles','admin'),
         new lang_string('configprofileroles', 'admin'),
index 2640b44..c2a817c 100644 (file)
@@ -31,7 +31,7 @@ $string['delete_confirmation'] = 'Are you absolutely sure you want to remove <st
 $string['deletea'] = 'Delete {$a}';
 $string['deletefiletypes'] = 'Delete a file type';
 $string['description'] = 'Custom description';
-$string['description_help'] = 'Simple file type description, e.g. &lsquo;Kindle ebook&rsquo;. If your site supports multiple languages and uses the multi-language filter, you can enter multi-language tags in this field to supply a description in different languages.';
+$string['description_help'] = 'Simple file type description, e.g. \'Kindle ebook\'. If your site supports multiple languages and uses the multi-language filter, you can enter multi-language tags in this field to supply a description in different languages.';
 $string['descriptiontype'] = 'Description type';
 $string['descriptiontype_help'] = 'There are three possible ways to specify a description.
 
@@ -49,15 +49,15 @@ $string['error_defaulticon'] = 'Another file extension with the same MIME type i
 $string['error_extension'] = 'The file type extension <strong>{$a}</strong> already exists or is invalid. File extensions must be unique and must not contain special characters.';
 $string['error_notfound'] = 'The file type with extension {$a} cannot be found.';
 $string['extension'] = 'Extension';
-$string['extension_help'] = 'File name extension without the dot, e.g. &lsquo;mobi&rsquo;';
+$string['extension_help'] = 'File name extension without the dot, e.g. \'mobi\'';
 $string['groups'] = 'Type groups';
-$string['groups_help'] = 'Optional list of file type groups that this type belongs to. These are generic categories such as &lsquo;document&rsquo; and &lsquo;image&rsquo;.';
+$string['groups_help'] = 'Optional list of file type groups that this type belongs to. These are generic categories such as \'document\' and \'image\'.';
 $string['icon'] = 'File icon';
 $string['icon_help'] = 'Icon filename.
 
 The list of icons is taken from the /pix/f directory inside your Moodle installation. You can add custom icons to this folder if required.';
 $string['mimetype'] = 'MIME type';
-$string['mimetype_help'] = 'MIME type associated with this file type, e.g. &lsquo;application/x-mobipocket-ebook&rsquo;';
+$string['mimetype_help'] = 'MIME type associated with this file type, e.g. \'application/x-mobipocket-ebook\'';
 $string['pluginname'] = 'File types';
 $string['revert'] = 'Restore {$a} to Moodle defaults';
 $string['revert_confirmation'] = 'Are you sure you want to restore <strong>.{$a}</strong> to Moodle defaults, discarding your changes?';
index cddf3f9..10398df 100644 (file)
@@ -88,6 +88,7 @@ class store implements \tool_log\log\writer, \core\log\sql_reader {
         $dboptions['dbport'] = $this->get_config('dbport', '');
         $dboptions['dbschema'] = $this->get_config('dbschema', '');
         $dboptions['dbcollation'] = $this->get_config('dbcollation', '');
+        $dboptions['dbhandlesoptions'] = $this->get_config('dbhandlesoptions', false);
         try {
             $db->connect($this->get_config('dbhost'), $this->get_config('dbuser'), $this->get_config('dbpass'),
                 $this->get_config('dbname'), false, $dboptions);
index 4e81a4a..0873229 100644 (file)
@@ -31,6 +31,8 @@ $string['databasesettings_help'] = 'Connection details for the external log data
 $string['databasepersist'] = 'Persistent database connections';
 $string['databaseschema'] = 'Database schema';
 $string['databasecollation'] = 'Database collation';
+$string['databasehandlesoptions'] = 'Database handles options';
+$string['databasehandlesoptions_help'] = 'Does the remote database handle its own options.';
 $string['databasetable'] = 'Database table';
 $string['databasetable_help'] = 'Name of the table where logs will be stored. This table should have a structure identical to the one used by logstore_standard (mdl_logstore_standard_log).';
 $string['includeactions'] = 'Include actions of these types';
index 5b930f4..4e45038 100644 (file)
@@ -54,6 +54,8 @@ if ($hassiteconfig) {
         'logstore_database'), '', ''));
     $settings->add(new admin_setting_configtext('logstore_database/dbcollation', get_string('databasecollation',
         'logstore_database'), '', ''));
+    $settings->add(new admin_setting_configcheckbox('logstore_database/dbhandlesoptions', get_string('databasehandlesoptions',
+        'logstore_database'), get_string('databasehandlesoptions_help', 'logstore_database'), '0'));
     $settings->add(new admin_setting_configtext('logstore_database/buffersize', get_string('buffersize',
         'logstore_database'), get_string('buffersize_help', 'logstore_database'), 50));
 
index ce35bc8..21a470e 100644 (file)
@@ -64,6 +64,7 @@ $dboptions['dbsocket'] = get_config('logstore_database', 'dbsocket');
 $dboptions['dbport'] = get_config('logstore_database', 'dbport');
 $dboptions['dbschema'] = get_config('logstore_database', 'dbschema');
 $dboptions['dbcollation'] = get_config('logstore_database', 'dbcollation');
+$dboptions['dbhandlesoptions'] = get_config('logstore_database', 'dbhandlesoptions');
 
 try {
     $db->connect(get_config('logstore_database', 'dbhost'), get_config('logstore_database', 'dbuser'),
index e40a8a4..a05c33f 100644 (file)
@@ -83,6 +83,11 @@ class logstore_database_store_testcase extends advanced_testcase {
         } else {
             set_config('dbcollation', '', 'logstore_database');
         }
+        if (!empty($CFG->dboptions['dbhandlesoptions'])) {
+            set_config('dbhandlesoptions', $CFG->dboptions['dbhandlesoptions'], 'logstore_database');
+        } else {
+            set_config('dbhandlesoptions', false, 'logstore_database');
+        }
 
         // Enable logging plugin.
         set_config('enabled_stores', 'logstore_database', 'tool_log');
diff --git a/admin/tool/log/store/database/upgrade.txt b/admin/tool/log/store/database/upgrade.txt
new file mode 100644 (file)
index 0000000..a8fa834
--- /dev/null
@@ -0,0 +1,11 @@
+This files describes API changes in the logstore_database code.
+
+=== 3.4 ===
+* PostgreSQL connections now use advanced options to reduce connection overhead.  These options are not compatible
+  with some connection poolers.  The dbhandlesoptions parameter has been added to allow the database to configure the
+  required defaults. The parameters that are required in the database are;
+    ALTER DATABASE moodle SET client_encoding = UTF8;
+    ALTER DATABASE moodle SET standard_conforming_strings = on;
+    ALTER DATABASE moodle SET search_path = 'moodle,public';  -- Optional, if you wish to use a custom schema.
+  You can set these options against the database or the moodle user who connects.
+
index 40d4db1..3105716 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version = 2017051500; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version = 2017062600; // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires = 2017050500; // Requires this Moodle version.
 $plugin->component = 'logstore_database'; // Full name of the plugin (used for diagnostics).
index 20060b7..19d1ea0 100644 (file)
@@ -51,7 +51,7 @@
             </div>
         </div>
         <div data-region="footer" class="pull-xs-right">
-            <input type="button" data-action="rate" value="{{#str}}rate, tool_lp{{/str}}" class="btn btn-primary">
+            <button data-action="rate" class="btn btn-primary">{{#str}}rate, tool_lp{{/str}}</button>
             <button data-action="cancel" class="btn btn-secondary">{{#str}}cancel{{/str}}</button>
         </div>
         <div class="clearfix"></div>
index 167a423..59f4d57 100644 (file)
@@ -60,7 +60,7 @@
         <div data-region="rule-base" class="form">
             <div data-region="rule-outcome" class="form-group">
                 <label>{{#str}}outcome, tool_lp{{/str}}</label>
-                <select name="outcome" class="custom-select" ng-label="{{#str}}outcome, tool_lp{{/str}}">
+                <select name="outcome" class="custom-select">
                     {{#outcomes}}
                     <option value="{{code}}" {{#selected}}selected{{/selected}}>{{name}}</option>
                     {{/outcomes}}
@@ -68,7 +68,7 @@
             </div>
             <div data-region="rule-type" class="form-group">
                 <label>{{#str}}when, tool_lp{{/str}}</label>
-                <select name="rule" class="custom-select" ng-label="{{#str}}when, tool_lp{{/str}}">
+                <select name="rule" class="custom-select">
                     <option value="-1">{{#str}}choosedots{{/str}}</option>
                     {{#rules}}
                     <option value="{{type}}" {{#selected}}selected{{/selected}}>{{name}}</option>
index 56e929a..9955cb6 100644 (file)
@@ -62,14 +62,19 @@ $string['logininthebrowser'] = 'Via a browser window (for SSO plugins)';
 $string['loginintheembeddedbrowser'] = 'Via an embedded browser (for SSO plugins)';
 $string['mainmenu'] = 'Main menu';
 $string['mobileapp'] = 'Mobile app';
+$string['mobileappconnected'] = 'Mobile app connected';
+$string['mobileappenabled'] = 'This site has mobile app access enabled.<br /><a href="{$a}">Download the mobile app</a>.';
 $string['mobileappearance'] = 'Mobile appearance';
 $string['mobileauthentication'] = 'Mobile authentication';
 $string['mobilecssurl'] = 'CSS';
 $string['mobilefeatures'] = 'Mobile features';
 $string['mobilesettings'] = 'Mobile settings';
 $string['pluginname'] = 'Moodle Mobile tools';
+$string['setuplink'] = 'App download page';
+$string['setuplink_desc'] = 'URL of page with links to download the mobile app from the App Store and Google Play.';
 $string['smartappbanners'] = 'App Banners';
 $string['pluginnotenabledorconfigured'] = 'Plugin not enabled or configured.';
 $string['remoteaddons'] = 'Remote add-ons';
 $string['typeoflogin'] = 'Type of login';
 $string['typeoflogin_desc'] = 'If the site uses a SSO authentication method, then select via a browser window or via an embedded browser. An embedded browser provides a better user experience, though it doesn\'t work with all SSO plugins. If using SSO, autologinguests should be disabled.';
+$string['getmoodleonyourmobile'] = 'Get Moodle on your mobile';
index de43b81..0ecfeff 100644 (file)
@@ -50,3 +50,94 @@ function tool_mobile_before_standard_html_head() {
     }
     return $output;
 }
+
+/**
+ * Generate the app download url to promote moodle mobile.
+ *
+ * @return moodle_url|void App download moodle_url object or return if setuplink is not set.
+ */
+function tool_mobile_create_app_download_url() {
+    global $CFG;
+
+    $mobilesettings = get_config('tool_mobile');
+
+    if (empty($mobilesettings->setuplink)) {
+        return;
+    }
+
+    $downloadurl = new moodle_url($mobilesettings->setuplink);
+    $downloadurl->param('version', $CFG->version);
+    $downloadurl->param('lang', current_language());
+
+    if (!empty($mobilesettings->iosappid)) {
+        $downloadurl->param('iosappid', $mobilesettings->iosappid);
+    }
+
+    if (!empty($mobilesettings->androidappid)) {
+        $downloadurl->param('androidappid', $mobilesettings->androidappid);
+    }
+
+    return $downloadurl;
+}
+
+/**
+ * User profile page callback.
+ *
+ * Used add a section about the moodle mobile app.
+ *
+ * @param \core_user\output\myprofile\tree $tree My profile tree where the setting will be added.
+ * @param stdClass $user The user object.
+ * @param bool $iscurrentuser Is this the current user viewing
+ * @return void Return if the mobile web services setting is disabled or if not the current user.
+ */
+function tool_mobile_myprofile_navigation(\core_user\output\myprofile\tree $tree, $user, $iscurrentuser) {
+    global $CFG, $DB;
+
+    if (empty($CFG->enablemobilewebservice)) {
+        return;
+    }
+
+    if (!$iscurrentuser) {
+        return;
+    }
+
+    if (!$url = tool_mobile_create_app_download_url()) {
+        return;
+    }
+
+    $sql = "SELECT 1
+              FROM {external_tokens} t, {external_services} s
+             WHERE t.externalserviceid = s.id
+               AND s.enabled = 1
+               AND s.shortname IN ('moodle_mobile_app', 'local_mobile')
+               AND t.userid = ?";
+    $userhastoken = $DB->record_exists_sql($sql, [$user->id]);
+
+    $mobilecategory = new core_user\output\myprofile\category('mobile', get_string('mobileapp', 'tool_mobile'),
+            'loginactivity');
+    $tree->add_category($mobilecategory);
+
+    if ($userhastoken) {
+        $mobilestr = get_string('mobileappconnected', 'tool_mobile');
+    } else {
+        $mobilestr = get_string('mobileappenabled', 'tool_mobile', $url->out());
+    }
+
+    $node = new  core_user\output\myprofile\node('mobile', 'mobileappnode', $mobilestr, null);
+    $tree->add_node($node);
+}
+
+/**
+ * Callback to add footer elements.
+ *
+ * @return str valid html footer content
+ * @since  Moodle 3.4
+ */
+function tool_mobile_standard_footer_html() {
+    global $CFG;
+    $output = '';
+    if (!empty($CFG->enablemobilewebservice) && $url = tool_mobile_create_app_download_url()) {
+        $output .= html_writer::link($url, get_string('getmoodleonyourmobile', 'tool_mobile'));
+    }
+    return $output;
+}
index 4f8630c..8af64c6 100644 (file)
@@ -83,6 +83,9 @@ if ($hassiteconfig) {
         $temp->add(new admin_setting_configtext('tool_mobile/androidappid', new lang_string('androidappid', 'tool_mobile'),
                     new lang_string('androidappid_desc', 'tool_mobile'), 'com.moodle.moodlemobile', PARAM_NOTAGS));
 
+        $temp->add(new admin_setting_configtext('tool_mobile/setuplink', new lang_string('setuplink', 'tool_mobile'),
+            new lang_string('setuplink_desc', 'tool_mobile'), 'https://download.moodle.org/mobile', PARAM_URL));
+
         $ADMIN->add('mobileapp', $temp);
 
         // Features related settings.
index 795c0a1..8c20c6a 100644 (file)
@@ -72,15 +72,15 @@ $string['issuerimage'] = 'Logo URL';
 $string['issuerloginparams'] = 'Additional parameters included in a login request.';
 $string['issuerloginparams_help'] = 'Some systems require additional parameters for a login request in order to read the user\'s basic profile.';
 $string['issuerloginparamsoffline'] = 'Additional parameters included in a login request for offline access.';
-$string['issuerloginparamsoffline_help'] = 'Each OAuth system defines a different way to request offline access. E.g. Google requires the additional params: "access_type=offline&prompt=consent" these parameters should be in url query parameter format.';
-$string['issuerloginscopes_help'] = 'Some systems require additional scopes for a login request in order to read the users basic profile. The standard scopes for an OpenID Connect compliant system are "openid profile email".';
-$string['issuerloginscopesoffline_help'] = 'Each OAuth system defines a different way to request offline access. E.g. Microsoft requires an additional scope "offline_access"';
+$string['issuerloginparamsoffline_help'] = 'Each OAuth system defines a different way to request offline access. E.g. Google requires the additional params: "access_type=offline&prompt=consent". These parameters should be in URL query parameter format.';
+$string['issuerloginscopes_help'] = 'Some systems require additional scopes for a login request in order to read the user\'s basic profile. The standard scopes for an OpenID Connect compliant system are "openid profile email".';
+$string['issuerloginscopesoffline_help'] = 'Each OAuth system defines a different way to request offline access. E.g. Microsoft requires an additional scope "offline_access".';
 $string['issuerloginscopesoffline'] = 'Scopes included in a login request for offline access.';
 $string['issuerloginscopes'] = 'Scopes included in a login request.';
 $string['issuername_help'] = 'Name of the identity issuer. May be displayed on login page.';
 $string['issuername'] = 'Name';
-$string['issuershowonloginpage_help'] = 'If the OpenID Connect Authentication plugin is enabled, this login issuer will be listed on the login page to allow users to log in with accounts from this issuer.';
-$string['issuershowonloginpage'] = 'Show on login page.';
+$string['issuershowonloginpage_help'] = 'If the OAuth 2 authentication plugin is enabled, this login issuer will be listed on the login page to allow users to log in with accounts from this issuer.';
+$string['issuershowonloginpage'] = 'Show on login page';
 $string['issuerrequireconfirmation_help'] = 'Require that all users verify their email address before they can log in with OAuth. This applies to newly created accounts as part of the login process, or when an existing Moodle account is connected to an OAuth login via matching email addresses.';
 $string['issuerrequireconfirmation'] = 'Require email verification';
 $string['issuers'] = 'Issuers';
index 593ea98..8663112 100644 (file)
@@ -31,7 +31,7 @@ $string['default'] = 'Default';
 $string['disabled'] = 'Disabled';
 $string['disabled_help'] = 'Disabled scheduled tasks are not executed from cron, however they can still be executed manually via the CLI tool.';
 $string['edittaskschedule'] = 'Edit task schedule: {$a}';
-$string['enablerunnow'] = 'Allow &lsquo;Run now&rsquo; for scheduled tasks';
+$string['enablerunnow'] = 'Allow \'Run now\' for scheduled tasks';
 $string['enablerunnow_desc'] = 'Allows administrators to run a single scheduled task immediately, rather than waiting for it to run as scheduled. The task runs on the web server, so some sites may wish to disable this feature to avoid potential performance issues.';
 $string['faildelay'] = 'Fail delay';
 $string['lastruntime'] = 'Last run';
@@ -41,7 +41,7 @@ $string['pluginname'] = 'Scheduled task configuration';
 $string['resettasktodefaults'] = 'Reset task schedule to defaults';
 $string['resettasktodefaults_help'] = 'This will discard any local changes and revert the schedule for this task back to its original settings.';
 $string['runnow'] = 'Run now';
-$string['runnow_confirm'] = 'Are you sure you want to run this task &lsquo;{$a}&rsquo; now? The task will run on the web server and may take some time to complete.';
+$string['runnow_confirm'] = 'Are you sure you want to run this task \'{$a}\' now? The task will run on the web server and may take some time to complete.';
 $string['scheduledtasks'] = 'Scheduled tasks';
 $string['scheduledtaskchangesdisabled'] = 'Modifications to the list of scheduled tasks have been prevented in Moodle configuration';
 $string['taskdisabled'] = 'Task disabled';
index 7282e28..522d6c6 100644 (file)
@@ -3187,6 +3187,18 @@ class CAS_Client
                 false/*$no_response*/, true/*$bad_response*/, $text_response
             );
             $result = false;
+       } else if ( $tree_response->getElementsByTagName("authenticationFailure")->length != 0) {
+            // authentication failed, extract the error code and message and throw exception
+            $auth_fail_list = $tree_response
+                ->getElementsByTagName("authenticationFailure");
+            throw new CAS_AuthenticationException(
+                $this, 'Ticket not validated', $validate_url,
+                false/*$no_response*/, false/*$bad_response*/,
+                $text_response,
+                $auth_fail_list->item(0)->getAttribute('code')/*$err_code*/,
+                trim($auth_fail_list->item(0)->nodeValue)/*$err_msg*/
+            );
+            $result = false;
         } else if ($tree_response->getElementsByTagName("authenticationSuccess")->length != 0) {
             // authentication succeded, extract the user name
             $success_elements = $tree_response
@@ -3227,18 +3239,6 @@ class CAS_Client
                     $result = true;
                 }
             }
-        } else if ( $tree_response->getElementsByTagName("authenticationFailure")->length != 0) {
-            // authentication succeded, extract the error code and message
-            $auth_fail_list = $tree_response
-                ->getElementsByTagName("authenticationFailure");
-            throw new CAS_AuthenticationException(
-                $this, 'Ticket not validated', $validate_url,
-                false/*$no_response*/, false/*$bad_response*/,
-                $text_response,
-                $auth_fail_list->item(0)->getAttribute('code')/*$err_code*/,
-                trim($auth_fail_list->item(0)->nodeValue)/*$err_msg*/
-            );
-            $result = false;
         } else {
             throw new CAS_AuthenticationException(
                 $this, 'Ticket not validated', $validate_url,
index e0d726d..2ef9d66 100644 (file)
@@ -2,3 +2,4 @@ Description of phpCAS 1.3.4 library import
 
 * downloaded from http://downloads.jasig.org/cas-clients/php/current/
 
+* MDL-59456 phpCAS library has been patched because of an authentication bypass security vulnerability.
\ No newline at end of file
index 3bf9979..baa86d9 100644 (file)
@@ -121,4 +121,98 @@ class core_auth_external extends external_api {
             )
         );
     }
+
+    /**
+     * Describes the parameters for request_password_reset.
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.4
+     */
+    public static function request_password_reset_parameters() {
+        return new external_function_parameters(
+            array(
+                'username' => new external_value(core_user::get_property_type('username'), 'User name', VALUE_DEFAULT, ''),
+                'email' => new external_value(core_user::get_property_type('email'), 'User email', VALUE_DEFAULT, ''),
+            )
+        );
+    }
+
+    /**
+     * Requests a password reset.
+     *
+     * @param  string $username user name
+     * @param  string $email    user email
+     * @return array warnings and success status (including notices and errors while processing)
+     * @since Moodle 3.4
+     * @throws moodle_exception
+     */
+    public static function request_password_reset($username = '', $email = '') {
+        global $CFG, $PAGE;
+        require_once($CFG->dirroot . '/login/lib.php');
+
+        $warnings = array();
+        $params = self::validate_parameters(
+            self::request_password_reset_parameters(),
+            array(
+                'username' => $username,
+                'email' => $email,
+            )
+        );
+
+        $context = context_system::instance();
+        $PAGE->set_context($context);   // Needed by format_string calls.
+
+        // Check if an alternate forgotten password method is set.
+        if (!empty($CFG->forgottenpasswordurl)) {
+            throw new moodle_exception('cannotmailconfirm');
+        }
+
+        $errors = core_login_validate_forgot_password_data($params);
+        if (!empty($errors)) {
+            $status = 'dataerror';
+            $notice = '';
+
+            foreach ($errors as $itemname => $message) {
+                $warnings[] = array(
+                    'item' => $itemname,
+                    'itemid' => 0,
+                    'warningcode' => 'fielderror',
+                    'message' => s($message)
+                );
+            }
+        } else {
+            list($status, $notice, $url) = core_login_process_password_reset($params['username'], $params['email']);
+        }
+
+        return array(
+            'status' => $status,
+            'notice' => $notice,
+            'warnings' => $warnings,
+        );
+    }
+
+    /**
+     * Describes the request_password_reset return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.4
+     */
+    public static function request_password_reset_returns() {
+
+        return new external_single_structure(
+            array(
+                'status' => new external_value(PARAM_ALPHANUMEXT, 'The returned status of the process:
+                    dataerror: Error in the sent data (username or email). More information in warnings field.
+                    emailpasswordconfirmmaybesent: Email sent or not (depends on user found in database).
+                    emailpasswordconfirmnotsent: Failure, user not found.
+                    emailpasswordconfirmnoemail: Failure, email not found.
+                    emailalreadysent: Email already sent.
+                    emailpasswordconfirmsent: User pending confirmation.
+                    emailresetconfirmsent: Email sent.
+                '),
+                'notice' => new external_value(PARAM_RAW, 'Important information for the user about the process.'),
+                'warnings'  => new external_warnings(),
+            )
+        );
+    }
 }
index ac32247..be99012 100644 (file)
@@ -346,7 +346,7 @@ class auth extends \auth_plugin_base {
      * Complete the login process after oauth handshake is complete.
      * @param \core\oauth2\client $client
      * @param string $redirecturl
-     * @return none Either redirects or throws an exception
+     * @return void Either redirects or throws an exception
      */
     public function complete_login(client $client, $redirecturl) {
         global $CFG, $SESSION, $PAGE;
@@ -356,7 +356,7 @@ class auth extends \auth_plugin_base {
         if (!$userinfo) {
             // Trigger login failed event.
             $failurereason = AUTH_LOGIN_NOUSER;
-            $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'],
+            $event = \core\event\user_login_failed::create(['other' => ['username' => 'unknown',
                                                                         'reason' => $failurereason]]);
             $event->trigger();
 
@@ -368,7 +368,7 @@ class auth extends \auth_plugin_base {
         if (empty($userinfo['username']) || empty($userinfo['email'])) {
             // Trigger login failed event.
             $failurereason = AUTH_LOGIN_NOUSER;
-            $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'],
+            $event = \core\event\user_login_failed::create(['other' => ['username' => 'unknown',
                                                                         'reason' => $failurereason]]);
             $event->trigger();
 
index 4e8ba89..3df327a 100644 (file)
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
+/**
+ * This script is used to configure and execute the backup proccess.
+ *
+ * @package    core
+ * @subpackage backup
+ * @copyright  Moodle
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('NO_OUTPUT_BUFFERING', true);
+
 require_once('../config.php');
 require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_plan_builder.class.php');
index 04681c9..57be494 100644 (file)
@@ -1,5 +1,31 @@
 <?php
 
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This script is used to configure and execute the import proccess.
+ *
+ * @package    core
+ * @subpackage backup
+ * @copyright  Moodle
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('NO_OUTPUT_BUFFERING', true);
+
 // Require both the backup and restore libs
 require_once('../config.php');
 require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
index ab97369..06f58e6 100644 (file)
@@ -1,5 +1,29 @@
 <?php
-    //This script is used to configure and execute the restore proccess.
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This script is used to configure and execute the restore proccess.
+ *
+ * @package    core
+ * @subpackage backup
+ * @copyright  Moodle
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('NO_OUTPUT_BUFFERING', true);
 
 require_once('../config.php');
 require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
index 636b1ba..e8f2491 100644 (file)
@@ -115,7 +115,7 @@ class block_login extends block_base {
                 $this->content->text .= '<div class="potentialidplist">';
                 foreach ($potentialidps as $idp) {
                     $this->content->text .= '<div class="potentialidp">';
-                    $this->content->text .= '<a class="btn btn-secondary btn-block" ';
+                    $this->content->text .= '<a class="btn btn-default btn-block" ';
                     $this->content->text .= 'href="' . $idp['url']->out() . '" title="' . s($idp['name']) . '">';
                     if (!empty($idp['iconurl'])) {
                         $this->content->text .= '<img src="' . s($idp['iconurl']) . '" width="24" height="24" class="m-r-1"/>';
index 798eb7b..2949c09 100644 (file)
@@ -63,6 +63,9 @@ class courses_view implements renderable, templatable {
      * @return array
      */
     public function export_for_template(renderer_base $output) {
+        global $CFG;
+        require_once($CFG->dirroot.'/course/lib.php');
+
         // Build courses view data structure.
         $coursesview = [
             'hascourses' => !empty($this->courses)
index 99fb83f..2176ec5 100644 (file)
@@ -23,7 +23,7 @@
  */
 
 $string['defaulttab'] = 'Default tab';
-$string['defaulttab_desc'] = 'This is the default tab that will be shown to a user.';
+$string['defaulttab_desc'] = 'The tab that will be displayed when a user first views their course overview. When returning to their course overview, the user\'s active tab is remembered.';
 $string['future'] = 'Future';
 $string['inprogress'] = 'In progress';
 $string['morecourses'] = 'More courses';
index aeb75be..a850602 100644 (file)
@@ -30,7 +30,6 @@ Feature: People Block used in a course
     And I am on "Course 1" course homepage
     And I click on "Participants" "link" in the "People" "block"
     Then I should see "All participants" in the "#page-content" "css_element"
-    And the "My courses" select box should contain "C101"
 
   Scenario: Student without permission can not view participants link
     Given the following "permission overrides" exist:
index 6ae461d..9abfe0e 100644 (file)
@@ -335,6 +335,15 @@ class cache_factory {
         return $this->cachesfromdefinitions;
     }
 
+    /**
+     * Gets all adhoc caches that have been used within this request.
+     *
+     * @return cache_store[] Caches currently in use
+     */
+    public function get_adhoc_caches_in_use() {
+        return $this->cachesfromparams;
+    }
+
     /**
      * Creates a cache config instance with the ability to write if required.
      *
index f15e2ee..c2aa399 100644 (file)
@@ -484,6 +484,9 @@ class cache_helper {
         foreach ($config->get_all_stores() as $store) {
             self::purge_store($store['name'], $config);
         }
+        foreach ($factory->get_adhoc_caches_in_use() as $cache) {
+            $cache->purge();
+        }
     }
 
     /**
index 0a47e4f..8d04cd1 100644 (file)
@@ -1730,6 +1730,16 @@ class core_cache_testcase extends advanced_testcase {
         }
     }
 
+    /**
+     * Tests that ad-hoc caches are correctly purged with a purge_all call.
+     */
+    public function test_purge_all_with_adhoc_caches() {
+        $cache = \cache::make_from_params(\cache_store::MODE_REQUEST, 'core_cache', 'test');
+        $cache->set('test', 123);
+        cache_helper::purge_all();
+        $this->assertFalse($cache->get('test'));
+    }
+
     /**
      * Test that the default stores all support searching.
      */
index 9ac8cbf..70c2cb7 100644 (file)
@@ -137,10 +137,15 @@ class container {
                     // have that capability set on the "Authenticated User" role rather than
                     // on "Student" role, which means uservisible returns true even when the user
                     // is no longer enrolled in the course.
-                    $modulecontext = \context_module::instance($cm->id);
-                    // A user with the 'moodle/course:view' capability is able to see courses
-                    // that they are not a participant in.
-                    $canseecourse = (has_capability('moodle/course:view', $modulecontext) || is_enrolled($modulecontext));
+                    // So, with the following we are checking -
+                    // 1) Only process modules if $cm->uservisible is true.
+                    // 2) Only process modules for courses a user has the capability to view OR they are enrolled in.
+                    // 3) Only process modules for courses that are visible OR if the course is not visible, the user
+                    //    has the capability to view hidden courses.
+                    $coursecontext = \context_course::instance($dbrow->courseid);
+                    $canseecourse = has_capability('moodle/course:view', $coursecontext) || is_enrolled($coursecontext);
+                    $canseecourse = $canseecourse &&
+                        ($cm->get_course()->visible || has_capability('moodle/course:viewhiddencourses', $coursecontext));
                     if (!$cm->uservisible || !$canseecourse) {
                         return true;
                     }
index 8c5d175..04e6cf3 100644 (file)
@@ -77,7 +77,8 @@ if (!empty($day) && !empty($mon) && !empty($year)) {
 }
 
 if ($courseid != SITEID && !empty($courseid)) {
-    $course = $DB->get_record('course', array('id' => $courseid));
+    // Course ID must be valid and existing.
+    $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
     $courses = array($course->id => $course);
     $issite = false;
 } else {
@@ -85,7 +86,7 @@ if ($courseid != SITEID && !empty($courseid)) {
     $courses = calendar_get_default_courses();
     $issite = true;
 }
-require_course_login($course);
+require_login($course, false);
 
 $url = new moodle_url('/calendar/export.php', array('time' => $time));
 
index 55ad67f..f6f1e4a 100644 (file)
@@ -1457,6 +1457,15 @@ function calendar_get_mini($courses, $groups, $users, $calmonth = false, $calyea
                         $name = format_string($event->name, true);
                     }
                 }
+                // Include course's shortname into the event name, if applicable.
+                if (!empty($event->courseid) && $event->courseid !== SITEID) {
+                    $course = get_course($event->courseid);
+                    $eventnameparams = (object)[
+                        'name' => $name,
+                        'course' => format_string($course->shortname, true, array('context' => $event->context))
+                    ];
+                    $name = get_string('eventnameandcourse', 'calendar', $eventnameparams);
+                }
                 $popupcontent .= \html_writer::link($dayhref, $name);
                 $popupcontent .= \html_writer::end_tag('div');
             }
index 3e4e0e9..f1c74d6 100644 (file)
@@ -45,13 +45,15 @@ $PAGE->set_pagelayout('admin');
 $PAGE->navbar->add(get_string('managesubscriptions', 'calendar'));
 
 if ($courseid != SITEID && !empty($courseid)) {
-    $course = $DB->get_record('course', array('id' => $courseid));
+    // Course ID must be valid and existing.
+    $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
     $courses = array($course->id => $course);
 } else {
     $course = get_site();
     $courses = calendar_get_default_courses();
 }
-require_course_login($course);
+require_login($course, false);
+
 if (!calendar_user_can_add_event($course)) {
     print_error('errorcannotimport', 'calendar');
 }
index 8abfb03..6711693 100644 (file)
@@ -502,7 +502,22 @@ class core_calendar_renderer extends plugin_renderer_base {
                         $attributes['class'] = $events[$eventindex]->class;
                     }
                     $dayhref->set_anchor('event_'.$events[$eventindex]->id);
-                    $link = html_writer::link($dayhref, format_string($events[$eventindex]->name, true));
+
+                    $eventcontext = $events[$eventindex]->context;
+                    $eventformatopts = array('context' => $eventcontext);
+                    // Get event name.
+                    $eventname = format_string($events[$eventindex]->name, true, $eventformatopts);
+                    // Include course's shortname into the event name, if applicable.
+                    $courseid = $events[$eventindex]->courseid;
+                    if (!empty($courseid) && $courseid !== SITEID) {
+                        $course = get_course($courseid);
+                        $eventnameparams = (object)[
+                            'name' => $eventname,
+                            'course' => format_string($course->shortname, true, $eventformatopts)
+                        ];
+                        $eventname = get_string('eventnameandcourse', 'calendar', $eventnameparams);
+                    }
+                    $link = html_writer::link($dayhref, $eventname);
                     $cell->text .= html_writer::tag('li', $link, $attributes);
                 }
                 $cell->text .= html_writer::end_tag('ul');
index 23a4108..ba1eb6c 100644 (file)
@@ -183,6 +183,63 @@ class core_calendar_container_testcase extends advanced_testcase {
         $this->assertNull($event);
     }
 
+    /**
+     * Test that the event factory deals with invisible courses as an admin.
+     *
+     * @dataProvider get_event_factory_testcases()
+     * @param \stdClass $dbrow Row from the "database".
+     */
+    public function test_event_factory_when_course_visibility_is_toggled_as_admin($dbrow) {
+        $legacyevent = $this->create_event($dbrow);
+        $factory = \core_calendar\local\event\container::get_event_factory();
+
+        // Create a hidden course with an assignment.
+        $course = $this->getDataGenerator()->create_course(['visible' => 0]);
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+        $moduleinstance = $generator->create_instance(['course' => $course->id]);
+
+        $dbrow->id = $legacyevent->id;
+        $dbrow->courseid = $course->id;
+        $dbrow->instance = $moduleinstance->id;
+        $dbrow->modulename = 'assign';
+        $event = $factory->create_instance($dbrow);
+
+        // Module is still visible to admins even if the course is invisible.
+        $this->assertInstanceOf(event_interface::class, $event);
+    }
+
+    /**
+     * Test that the event factory deals with invisible courses as a student.
+     *
+     * @dataProvider get_event_factory_testcases()
+     * @param \stdClass $dbrow Row from the "database".
+     */
+    public function test_event_factory_when_course_visibility_is_toggled_as_student($dbrow) {
+        $legacyevent = $this->create_event($dbrow);
+        $factory = \core_calendar\local\event\container::get_event_factory();
+
+        // Create a hidden course with an assignment.
+        $course = $this->getDataGenerator()->create_course(['visible' => 0]);
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+        $moduleinstance = $generator->create_instance(['course' => $course->id]);
+
+        // Enrol a student into this course.
+        $student = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($student->id, $course->id);
+
+        // Set the user to the student.
+        $this->setUser($student);
+
+        $dbrow->id = $legacyevent->id;
+        $dbrow->courseid = $course->id;
+        $dbrow->instance = $moduleinstance->id;
+        $dbrow->modulename = 'assign';
+        $event = $factory->create_instance($dbrow);
+
+        // Module is invisible to students if the course is invisible.
+        $this->assertNull($event);
+    }
+
     /**
      * Test that the event factory deals with completion related events properly.
      */
@@ -285,6 +342,31 @@ class core_calendar_container_testcase extends advanced_testcase {
         $this->assertInstanceOf(event_interface::class, $factory->create_instance($event));
     }
 
+    /**
+     * Test that when course module is deleted all events are also deleted.
+     */
+    public function test_delete_module_delete_events() {
+        global $DB;
+        $user = $this->getDataGenerator()->create_user();
+        // Create the course we will be using.
+        $course = $this->getDataGenerator()->create_course();
+        $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
+
+        foreach (core_component::get_plugin_list('mod') as $modname => $unused) {
+            $module = $this->getDataGenerator()->create_module($modname, ['course' => $course->id]);
+
+            // Create bunch of events of different type (user override, group override, module event).
+            $this->create_event(['userid' => $user->id, 'modulename' => $modname, 'instance' => $module->id]);
+            $this->create_event(['groupid' => $group->id, 'modulename' => $modname, 'instance' => $module->id]);
+            $this->create_event(['modulename' => $modname, 'instance' => $module->id]);
+            $this->create_event(['modulename' => $modname, 'instance' => $module->id, 'courseid' => $course->id]);
+
+            // Delete module and make sure all events are deleted.
+            course_delete_module($module->cmid);
+            $this->assertEmpty($DB->get_record('event', ['modulename' => $modname, 'instance' => $module->id]));
+        }
+    }
+
     /**
      * Test getting the event mapper.
      */
index a55aee6..18245a5 100644 (file)
@@ -85,7 +85,8 @@ $url->param('time', $time);
 $PAGE->set_url($url);
 
 if ($courseid != SITEID && !empty($courseid)) {
-    $course = $DB->get_record('course', array('id' => $courseid));
+    // Course ID must be valid and existing.
+    $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
     $courses = array($course->id => $course);
     $issite = false;
     navigation_node::override_active_url(new moodle_url('/course/view.php', array('id' => $course->id)));
@@ -95,7 +96,7 @@ if ($courseid != SITEID && !empty($courseid)) {
     $issite = true;
 }
 
-require_course_login($course);
+require_login($course, false);
 
 $calendar = new calendar_information(0, 0, 0, $time);
 $calendar->prepare_for_view($course, $courses);
index f079a74..bc444f8 100644 (file)
@@ -1205,8 +1205,10 @@ function course_delete_module($cmid, $async = false) {
 
     // Delete events from calendar.
     if ($events = $DB->get_records('event', array('instance' => $cm->instance, 'modulename' => $modulename))) {
+        $coursecontext = context_course::instance($cm->course);
         foreach($events as $event) {
-            $calendarevent = calendar_event::load($event->id);
+            $event->context = $coursecontext;
+            $calendarevent = calendar_event::load($event);
             $calendarevent->delete();
         }
     }
index c081b73..ebd5c49 100644 (file)
 //                                                                       //
 ///////////////////////////////////////////////////////////////////////////
 
-/*
+/**
+ * This page display the publication backup form
+ *
  * @package    course
  * @subpackage publish
  * @author     Jerome Mouneyrac <jerome@mouneyrac.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL
  * @copyright  (C) 1999 onwards Martin Dougiamas  http://dougiamas.com
- *
- * This page display the publication backup form
  */
 
+define('NO_OUTPUT_BUFFERING', true);
+
 require_once('../../config.php');
 require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_plan_builder.class.php');
index 565f90c..609ae6a 100644 (file)
@@ -450,7 +450,7 @@ class core_course_renderer extends plugin_renderer_base {
     public function course_section_cm_completion($course, &$completioninfo, cm_info $mod, $displayoptions = array()) {
         global $CFG;
         $output = '';
-        if (!$mod->is_visible_on_course_page()) {
+        if (!empty($displayoptions['hidecompletion']) || !isloggedin() || isguestuser() || !$mod->uservisible) {
             return $output;
         }
         if ($completioninfo === null) {
index 4b50090..b4840f9 100644 (file)
@@ -101,7 +101,8 @@ if ($switchrole > 0 && has_capability('moodle/role:switchroles', $context)) {
 
     foreach ($roles as $key => $role) {
         $url = new moodle_url('/course/switchrole.php', array('id' => $id, 'switchrole' => $key, 'returnurl' => $returnurl));
-        echo $OUTPUT->container($OUTPUT->single_button($url, $role), 'm-x-3 m-b-1');
+        // Button encodes special characters, apply htmlspecialchars_decode() to avoid double escaping.
+        echo $OUTPUT->container($OUTPUT->single_button($url, htmlspecialchars_decode($role)), 'm-x-3 m-b-1');
     }
 
     $url = new moodle_url($returnurl);
index 8012a7f..d932dd8 100644 (file)
@@ -361,6 +361,40 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         core_course_external::update_categories($categories);
     }
 
+    /**
+     * Test create_courses numsections
+     */
+    public function test_create_course_numsections() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        // Set the required capabilities by the external function.
+        $contextid = context_system::instance()->id;
+        $roleid = $this->assignUserCapability('moodle/course:create', $contextid);
+        $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid);
+
+        $numsections = 10;
+        $category  = self::getDataGenerator()->create_category();
+
+        // Create base categories.
+        $course1['fullname'] = 'Test course 1';
+        $course1['shortname'] = 'Testcourse1';
+        $course1['categoryid'] = $category->id;
+        $course1['courseformatoptions'][] = array('name' => 'numsections', 'value' => $numsections);
+
+        $courses = array($course1);
+
+        $createdcourses = core_course_external::create_courses($courses);
+        foreach ($createdcourses as $createdcourse) {
+            $existingsections = $DB->get_records('course_sections', array('course' => $createdcourse['id']));
+            $modinfo = get_fast_modinfo($createdcourse['id']);
+            $sections = $modinfo->get_section_info_all();
+            $this->assertEquals(count($sections), $numsections + 1); // Includes generic section.
+            $this->assertEquals(count($existingsections), $numsections + 1); // Includes generic section.
+        }
+    }
+
     /**
      * Test create_courses
      */
index 90ca368..0eaa555 100644 (file)
@@ -42,11 +42,9 @@ class writer extends \core\dataformat\base {
     public $extension = ".html";
 
     /**
-     * Write the start of the format
-     *
-     * @param array $columns
+     * Write the start of the output
      */
-    public function write_header($columns) {
+    public function start_output() {
         echo "<!DOCTYPE html><html>";
         echo \html_writer::tag('title', $this->filename);
         echo "<style>
@@ -75,9 +73,16 @@ table {
     margin: auto;
 }
 </style>
-<body>
-<table border=1 cellspacing=0 cellpadding=3>
-";
+<body>";
+    }
+
+    /**
+     * Write the start of the sheet we will be adding data to.
+     *
+     * @param array $columns
+     */
+    public function start_sheet($columns) {
+        echo "<table border=1 cellspacing=0 cellpadding=3>";
         echo \html_writer::start_tag('tr');
         foreach ($columns as $k => $v) {
             echo \html_writer::tag('th', $v);
@@ -100,12 +105,18 @@ table {
     }
 
     /**
-     * Write the end of the format
+     * Write the end of the sheet containing the data.
      *
      * @param array $columns
      */
-    public function write_footer($columns) {
-        echo "</table></body></html>";
+    public function close_sheet($columns) {
+        echo "</table>";
     }
 
+    /**
+     * Write the end of the sheet containing the data.
+     */
+    public function close_output() {
+        echo "</body></html>";
+    }
 }
index 84f3cc6..8e3cfba 100644 (file)
@@ -41,12 +41,31 @@ class writer extends \core\dataformat\base {
     /** @var $extension */
     public $extension = ".json";
 
+    /** @var $sheetstarted */
+    public $sheetstarted = false;
+
+    /** @var $sheetdatadded */
+    public $sheetdatadded = false;
+
+    /**
+     * Write the start of the file.
+     */
+    public function start_output() {
+        echo "[";
+    }
+
     /**
-     * Write the start of the format
+     * Write the start of the sheet we will be adding data to.
      *
      * @param array $columns
      */
-    public function write_header($columns) {
+    public function start_sheet($columns) {
+        if ($this->sheetstarted) {
+            echo ",";
+        } else {
+            $this->sheetstarted = true;
+        }
+        $this->sheetdatadded = false;
         echo "[";
     }
 
@@ -57,19 +76,28 @@ class writer extends \core\dataformat\base {
      * @param int $rownum
      */
     public function write_record($record, $rownum) {
-        if ($rownum) {
+        if ($this->sheetdatadded) {
             echo ",";
         }
+
         echo json_encode($record);
+
+        $this->sheetdatadded = true;
     }
 
     /**
-     * Write the end of the format
+     * Write the end of the sheet containing the data.
      *
      * @param array $columns
      */
-    public function write_footer($columns) {
+    public function close_sheet($columns) {
         echo "]";
     }
 
+    /**
+     * Write the end of the file.
+     */
+    public function close_output() {
+        echo "]";
+    }
 }
index bfd29d1..5868991 100644 (file)
@@ -1,7 +1,14 @@
 This files describes API changes in /dataformat/ download system,
 information provided here is intended especially for developers.
 
-=== 3.1 ===
-* Added new plugin system with low memory support for csv, ods, xls and json
+=== 3.4 ===
 
+* In order to allow multiple sheets in an exported file the functions write_header() and write_footer() have
+  been removed from core dataformat plugins and have been replaced.
+  - write_header() has been replaced with the two functions start_output() and start_sheet().
+  - write_footer() has been replaced with the two functions close_output() and close_sheet().
+  For backwards compatibility write_header() and write_footer() will continue to work but if used will
+  trigger the function error_log().
 
+=== 3.1 ===
+* Added new plugin system with low memory support for csv, ods, xls and json
index b1757a7..934d47a 100644 (file)
@@ -38,7 +38,7 @@ class enrol_guest_enrol_form extends moodleform {
         $heading = $plugin->get_instance_name($instance);
         $mform->addElement('header', 'guestheader', $heading);
 
-        $mform->addElement('passwordunmask', 'guestpassword', get_string('password', 'enrol_guest'));
+        $mform->addElement('password', 'guestpassword', get_string('password', 'enrol_guest'));
 
         $this->add_action_buttons(false, get_string('submit'));
 
index aafc0e7..e6f4140 100644 (file)
@@ -328,7 +328,7 @@ class enrol_ldap_plugin extends enrol_plugin {
             return;
         }
 
-        $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'));
+        $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'), $this->ldapconnection);
 
         // we may need a lot of memory here
         core_php_time_limit::raise();
@@ -761,7 +761,7 @@ class enrol_ldap_plugin extends enrol_plugin {
 
         // Get all contexts and look for first matching user
         $ldap_contexts = explode(';', $ldap_contexts);
-        $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'));
+        $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'), $this->ldapconnection);
         foreach ($ldap_contexts as $context) {
             $context = trim($context);
             if (empty($context)) {
index a60224e..dc17af1 100644 (file)
@@ -78,7 +78,7 @@ class enrol_self_enrol_form extends moodleform {
 
         if ($instance->password) {
             // Change the id of self enrolment key input as there can be multiple self enrolment methods.
-            $mform->addElement('passwordunmask', 'enrolpassword', get_string('password', 'enrol_self'),
+            $mform->addElement('password', 'enrolpassword', get_string('password', 'enrol_self'),
                     array('id' => 'enrolpassword_'.$instance->id));
             $context = context_course::instance($this->instance->courseid);
             $keyholders = get_users_by_capability($context, 'enrol/self:holdkey', user_picture::fields('u'));
index 395b072..65179a7 100644 (file)
@@ -32,7 +32,7 @@ $action   = optional_param('action', '', PARAM_ALPHA);
 /// Make sure they can even access this course
 if ($courseid) {
     if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-        print_error('nocourseid');
+        print_error('invalidcourseid');
     }
     require_login($course);
     $context = context_course::instance($course->id);
index 3377552..ec5117e 100644 (file)
@@ -38,7 +38,7 @@ $PAGE->set_pagelayout('admin');
 /// Make sure they can even access this course
 if ($courseid) {
     if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-        print_error('nocourseid');
+        print_error('invalidcourseid');
     }
     require_login($course);
     $context = context_course::instance($course->id);
index e016560..830c675 100644 (file)
@@ -71,7 +71,7 @@ if ($id) {
     $heading = get_string('addscale', 'grades');
     /// adding new scale from course
     if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-        print_error('nocourseid');
+        print_error('invalidcourseid');
     }
     $scale_rec = new stdClass();
     $scale_rec->standard = 0;
index 0b8e207..30b2ef8 100644 (file)
@@ -34,7 +34,7 @@ $PAGE->set_url('/grade/edit/scale/index.php', array('id' => $courseid));
 /// Make sure they can even access this course
 if ($courseid) {
     if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-        print_error('nocourseid');
+        print_error('invalidcourseid');
     }
     require_login($course);
     $context = context_course::instance($course->id);
index 5b885d4..5b73474 100644 (file)
@@ -33,7 +33,7 @@ $PAGE->set_url('/grade/edit/settings/index.php', array('id'=>$courseid));
 $PAGE->set_pagelayout('admin');
 
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 require_login($course);
 $context = context_course::instance($course->id);
index cea8791..1117f9d 100644 (file)
@@ -33,7 +33,7 @@ $PAGE->set_url('/grade/edit/tree/action.php', array('id'=>$courseid, 'action'=>$
 
 /// Make sure they can even access this course
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 require_login($course);
 $context = context_course::instance($course->id);
index e0dabcf..76fea61 100644 (file)
@@ -39,7 +39,7 @@ if ($section !== 'calculation') {
 $PAGE->set_url($url);
 
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index 9886fe2..deb397b 100644 (file)
@@ -40,7 +40,7 @@ navigation_node::override_active_url(new moodle_url('/grade/edit/tree/index.php'
     array('id'=>$courseid)));
 
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index dd9cce3..698cde7 100644 (file)
@@ -45,7 +45,7 @@ if ($userid !== 0) {
 $PAGE->set_url($url);
 
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 $PAGE->set_pagelayout('incourse');
index bcdbbb8..3a1ce5e 100644 (file)
@@ -38,7 +38,7 @@ $PAGE->set_pagelayout('admin');
 
 /// Make sure they can even access this course
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index 7ee5cbe..2ece98f 100644 (file)
@@ -41,7 +41,7 @@ navigation_node::override_active_url(new moodle_url('/grade/edit/tree/index.php'
     array('id'=>$courseid)));
 
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index 02c76a8..5d0a628 100644 (file)
@@ -40,7 +40,7 @@ navigation_node::override_active_url(new moodle_url('/grade/edit/tree/index.php'
     array('id'=>$courseid)));
 
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index aca4e87..303a7a6 100644 (file)
@@ -31,7 +31,7 @@ $id = required_param('id', PARAM_INT); // course id
 $PAGE->set_url('/grade/export/keymanager.php', array('id' => $id));
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index c2e7a34..80b2c5a 100644 (file)
@@ -28,7 +28,7 @@ $decimalpoints      = optional_param('decimalpoints', $CFG->grade_export_decimal
 $onlyactive         = optional_param('export_onlyactive', 0, PARAM_BOOL);
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_user_key_login('grade/export', $id); // we want different keys for each course
index 7ab7660..cfb3bfe 100644 (file)
@@ -23,7 +23,7 @@ $id                = required_param('id', PARAM_INT); // course id
 $PAGE->set_url('/grade/export/ods/export.php', array('id'=>$id));
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index 29ee8a1..9fe78e7 100644 (file)
@@ -24,7 +24,7 @@ $id = required_param('id', PARAM_INT); // course id
 $PAGE->set_url('/grade/export/ods/index.php', array('id'=>$id));
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index 5933954..79f1d43 100644 (file)
@@ -29,7 +29,7 @@ $decimalpoints      = optional_param('decimalpoints', $CFG->grade_export_decimal
 $onlyactive         = optional_param('export_onlyactive', 0, PARAM_BOOL);
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_user_key_login('grade/export', $id); // we want different keys for each course
index 903cfbf..bba045a 100644 (file)
@@ -23,7 +23,7 @@ $id                = required_param('id', PARAM_INT); // course id
 $PAGE->set_url('/grade/export/txt/export.php', array('id'=>$id));
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index c5420ce..98c0f73 100644 (file)
@@ -24,7 +24,7 @@ $id = required_param('id', PARAM_INT); // course id
 $PAGE->set_url('/grade/export/txt/index.php', array('id'=>$id));
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index 287c513..5459c04 100644 (file)
@@ -28,7 +28,7 @@ $decimalpoints      = optional_param('decimalpoints', $CFG->grade_export_decimal
 $onlyactive         = optional_param('export_onlyactive', 0, PARAM_BOOL);
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_user_key_login('grade/export', $id); // we want different keys for each course
index 0855517..507ce12 100644 (file)
@@ -23,7 +23,7 @@ $id                = required_param('id', PARAM_INT); // course id
 $PAGE->set_url('/grade/export/xls/export.php', array('id'=>$id));
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index 4d86ba8..519f9ea 100644 (file)
@@ -24,7 +24,7 @@ $id = required_param('id', PARAM_INT); // course id
 $PAGE->set_url('/grade/export/xls/index.php', array('id'=>$id));
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index a8ed187..592b1f4 100644 (file)
@@ -29,7 +29,7 @@ $decimalpoints      = optional_param('decimalpoints', $CFG->grade_export_decimal
 $onlyactive         = optional_param('export_onlyactive', 0, PARAM_BOOL);
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_user_key_login('grade/export', $id); // we want different keys for each course
index d7ae8e7..e3a55b0 100644 (file)
@@ -23,7 +23,7 @@ $id                = required_param('id', PARAM_INT); // course id
 $PAGE->set_url('/grade/export/xml/export.php', array('id'=>$id));
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index 3b70fd7..40fa2ec 100644 (file)
@@ -24,7 +24,7 @@ $id = required_param('id', PARAM_INT); // course id
 $PAGE->set_url('/grade/export/xml/index.php', array('id'=>$id));
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index ddc1fe9..ecdcb3a 100644 (file)
@@ -39,7 +39,7 @@ if ($verbosescales !== 1) {
 $PAGE->set_url($url);
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index 44aabad..0d795d8 100644 (file)
@@ -35,7 +35,7 @@ if ($verbosescales !== 1) {
 $PAGE->set_url($url);
 
 if (!$course = $DB->get_record('course', array('id' => $id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index 3ff82cb..6cdc379 100644 (file)
@@ -31,7 +31,7 @@ $id = required_param('id', PARAM_INT); // course id
 $PAGE->set_url('/grade/import/keymanager.php', array('id' => $id));
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index 17e1dea..b127968 100644 (file)
@@ -20,7 +20,7 @@ require_once '../../../config.php';
 
 $id = required_param('id', PARAM_INT); // course id
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_user_key_login('grade/import', $id); // we want different keys for each course
index cdb537f..f12e799 100644 (file)
@@ -30,7 +30,7 @@ if ($feedback !== 0) {
 $PAGE->set_url($url);
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index b1ebdff..d587d1f 100644 (file)
@@ -25,7 +25,7 @@ $PAGE->set_url(new moodle_url('/grade/import/xml/index.php', array('id'=>$id)));
 $PAGE->set_pagelayout('admin');
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index 0d971a7..8260320 100644 (file)
@@ -37,7 +37,7 @@ $newvalue = optional_param('newvalue', false, PARAM_TEXT);
 
 /// basic access checks
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 $context = context_course::instance($course->id);
 require_login($course);
index 43917fa..10e0c3e 100644 (file)
@@ -56,7 +56,7 @@ $PAGE->requires->yui_module('moodle-gradereport_grader-gradereporttable', 'Y.M.g
 
 // basic access checks
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 require_login($course);
 $context = context_course::instance($course->id);
index 029afa4..4b0fccc 100644 (file)
@@ -35,7 +35,7 @@ $PAGE->set_pagelayout('admin');
 /// Make sure they can even access this course
 
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index cd18dc7..2a9741d 100644 (file)
@@ -44,7 +44,7 @@ $PAGE->set_url($url);
 
 /// basic access checks
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 if (!$item = $DB->get_record('grade_items', array('id' => $itemid))) {
index 5e7522a..261c277 100644 (file)
@@ -30,7 +30,7 @@ $PAGE->set_url('/grade/report/index.php', array('id'=>$courseid));
 
 /// basic access checks
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 require_login($course);
 $context = context_course::instance($course->id);
index 092373a..605a023 100644 (file)
@@ -31,7 +31,7 @@ $courseid = required_param('id', PARAM_INT);                   // course id
 $PAGE->set_url('/grade/report/outcomes/index.php', array('id'=>$courseid));
 
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index 4125157..f724fba 100644 (file)
@@ -33,7 +33,7 @@ $userid   = optional_param('userid', $USER->id, PARAM_INT);
 $PAGE->set_url(new moodle_url('/grade/report/overview/index.php', array('id' => $courseid, 'userid' => $userid)));
 
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 require_login(null, false);
 $PAGE->set_course($course);
index 71f7db4..9422fb1 100644 (file)
@@ -22,6 +22,8 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+define('NO_OUTPUT_BUFFERING', true);
+
 require_once('../../../config.php');
 require_once($CFG->dirroot.'/lib/gradelib.php');
 require_once($CFG->dirroot.'/grade/lib.php');
@@ -59,7 +61,7 @@ $PAGE->set_url(new moodle_url('/grade/report/singleview/index.php', $pageparams)
 $PAGE->set_pagelayout('incourse');
 
 if (!$course = $DB->get_record('course', $courseparams)) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index cad859f..d79b4c9 100644 (file)
@@ -41,7 +41,7 @@ if ($userview == 0) {
 
 /// basic access checks
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 require_login($course);
 $PAGE->set_pagelayout('report');
index 063397e..aab6a37 100644 (file)
@@ -31,7 +31,7 @@ $courseid = required_param('id', PARAM_INT);
 $PAGE->set_url('/group/groupings.php', array('id'=>$courseid));
 
 if (!$course = $DB->get_record('course', array('id'=>$courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index a0aa5c7..57047f6 100644 (file)
@@ -66,7 +66,9 @@ $string['pathshead'] = 'Bekreft stier';
 $string['pathsrodataroot'] = 'Dataroot katalog er ikke skrivbar.';
 $string['pathsroparentdataroot'] = 'Overordnet katalog ({$a->parent}) er ikke skrivbar. Datakatalogen ({$a->dataroot}) kan ikke opprettes av installasjonsprogrammet.';
 $string['pathssubadmindir'] = 'Noen ganske få webhoteller bruker  /admin som en egen url for å få tilgang til et kontrollpanel. Dessverre kommer det i konflikt med standard lokalisering av Moodle sine admin-sider. Du kan fikse dette ved å endre navn på admin-mappen og deretter oppgi dette navnet her. F.eks. <em>moodleadmin</em>.  Dette vil fikse adminlenkene i Moodle.';
-$string['pathssubdataroot'] = 'Du trenger et sted hvor Moodle kan lagre opplastede filer. Denne mappen må være med lese og skriverettigheter for webserver-brukeren (veldig ofte \'nobody\' eller \'apache\'), men denne mappen må IKKE være direkte tilgjengelig via web. Installasjonsprogrammet vil forsøke å opprette den om den ikke finnes fra før.';
+$string['pathssubdataroot'] = '<p>Du trenger et sted hvor Moodle kan lagre opplastede filer. </p>
+<p>Denne mappen må være med lese og skriverettigheter for webserver-brukeren (veldig ofte \'nobody\' eller \'apache\'), men denne mappen må IKKE være direkte tilgjengelig via web.</p>
+<p> Installasjonsprogrammet vil forsøke å opprette den om den ikke finnes fra før.</p>';
 $string['pathssubdirroot'] = '<p>Full mappesti til moodleinstallasjonen.</p>';
 $string['pathssubwwwroot'] = '<p>Full webadresse til der hvor Moodle skal vises, altså den addressen som brukere skriver inn i adresselinjen i nettlseren sin.</p>
 <p>Det er ikke mulig å bruke Moodle med mer enn en adresse. Dersom portalen din har flere webadresser bør du oppgi den enkleste av de her, og bruke videresending for  hver av de andre adressene.</p>
@@ -76,9 +78,9 @@ $string['pathsunsecuredataroot'] = 'Dataroot plassering er ikke sikker';
 $string['pathswrongadmindir'] = 'Adminkatalog finnes ikke';
 $string['phpextension'] = '{$a} PHP etternavn';
 $string['phpversion'] = 'PHP versjon';
-$string['phpversionhelp'] = '<p>Moodle trenger en PHP versjon minst 4.3.0 eller 5.1.0 (5.0.x har rekke kjente problem).</p>
+$string['phpversionhelp'] = '<p>Moodle trenger en PHP versjon minst 5.6.5 eller 7.1.0 (7.0.x har noen begrensninger).</p>
 <Du kjører nå versjon {$a}</p>
-<p>Du må oppgradere PHP eller flytte til en server med en nyere versjon av PHP!<br /> (I forhold til 5.0.x kan du også nedgradere til versjon 4.4.x)</p>';
+<p>Du må oppgradere PHP eller flytte til en server med en nyere versjon av PHP!</p>';
 $string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
 $string['welcomep20'] = 'Du ser denne siden fordi du nå har fullført installeringen og kjøringen av pakken <strong>{$a->packname} {$a->packversion}</strong> på datamaskinen din. Gratulerer!';
 $string['welcomep30'] = 'Denne versjonen av <strong>{$a->installername}</strong> inneholder programmer for å lage et miljø som <strong>Moodle</strong> jobber i, nemlig:';
index 65e5af6..9ac1174 100644 (file)
@@ -46,7 +46,8 @@ $string['dmlexceptiononinstall'] = '<p> Ocorreu um erro no banco de dados [{$a->
 $string['downloadedfilecheckfailed'] = 'A verificação do arquivo baixado falhou';
 $string['invalidmd5'] = 'A variável de verificação estava errada - tente novamente';
 $string['missingrequiredfield'] = 'Faltam informações obrigatórias';
-$string['remotedownloaderror'] = '<p>O download do componente falhou, por favor verifique as configurações do proxy. A extensão cURL do PHP é altamente recomendada.<p/><p>Você precisar baixar o <a href="{$a->url}">arquivo</a> manualmente, copiar para "{$a->dest}" e descompactar lá.<p/>';
+$string['remotedownloaderror'] = '<p>O download do componente falhou, por favor verifique as configurações do proxy. A extensão cURL do PHP é altamente recomendada.</p>
+<p>Você precisar baixar o <a href="{$a->url}">{$a->url}</a> manualmente, copiar para "{$a->dest}" e descompactar lá.</p>';
 $string['wrongdestpath'] = 'Caminho do destino errado';
 $string['wrongsourcebase'] = 'URL do recurso errada';
 $string['wrongzipfilename'] = 'Nome do arquivo ZIP errado';
index b9bb1e5..dca2305 100644 (file)
@@ -49,6 +49,11 @@ $string['allowcoursethemes'] = 'Allow course themes';
 $string['allowediplist'] = 'Allowed IP list';
 $string['allowedemaildomains'] = 'Allowed email domains';
 $string['allowemailaddresses'] = 'Allowed email domains';
+$string['allowindexing'] = 'Allow indexing by search engines';
+$string['allowindexing_desc'] = 'This determines whether to allow search engines to index your site. "Everywhere" will allow the search engines to search everywhere including login and signup pages, which means sites with Force Login turned on are still indexed. To avoid the risk of spam involved with the signup page being searchable, use "Everywhere except login and signup pages". "Nowhere" will tell search engines not to index any page. Note this is only a tag in the header of the site. It is up to the search engine to respect the tag.';
+$string['allowindexingeverywhere'] = 'Everywhere';
+$string['allowindexingexceptlogin'] = 'Everywhere except login and signup pages';
+$string['allowindexingnowhere'] = 'Nowhere';
 $string['allowusermailcharset'] = 'Allow user to select character set';
 $string['allowframembedding'] = 'Allow frame embedding';
 $string['allowframembedding_help'] = 'If enabled, this site may be embedded in a frame in a remote system, as recommended when using the \'Publish as LTI tool\' enrolment plugin. Otherwise, it is recommended to leave frame embedding disabled for security reasons.';
index 9eb4b7a..50c59d1 100644 (file)
@@ -185,7 +185,7 @@ $string['viewmyentriesaboutcourse'] = 'View my entries about this course';
 $string['viewsiteentries'] = 'View all entries';
 $string['viewuserentries'] = 'View all entries by {$a}';
 $string['worldblogs'] = 'The world can read entries set to be world-accessible';
-$string['wrongexternalid'] = 'Wrong external blog id';
+$string['wrongexternalid'] = 'Wrong external blog ID';
 $string['wrongpostid'] = 'Wrong blog post id';
 $string['page-blog-edit'] = 'Blog editing pages';
 $string['page-blog-index'] = 'Blog listing pages';
index 73d8b4a..d194188 100644 (file)
@@ -64,7 +64,7 @@ $string['errorbeforecoursestart'] = 'Cannot set event before course start date';
 $string['errorcannotimport'] = 'You cannot set up a calendar subscription at this time.';
 $string['errorhasuntilandcount'] = 'Either UNTIL or COUNT may appear in a recurrence rule, but UNTIL and COUNT MUST NOT occur in the same recurrence rule.';
 $string['errorinvalidbydaysuffix'] = 'Valid values for the day of the week parts of the BYDAY rule are MO, TU, WE, TH, FR, SA and SU';
-$string['errorinvalidbydayprefix'] = 'Integer values preceding BYDAY rules can only be present for MONTHLY or YEARLY RRULE.';
+$string['errorinvalidbydayprefix'] = 'Integer values preceding BYDAY rules can only be present for a MONTHLY or YEARLY recurrence rule.';
 $string['errorinvalidbyhour'] = 'Valid values for the BYHOUR rule are 0 to 23.';
 $string['errorinvalidinterval'] = 'The value for the INTERVAL rule must be a positive integer.';
 $string['errorinvalidbyminute'] = 'Valid values for the BYMINUTE rule are 0 to 59.';
@@ -83,9 +83,9 @@ $string['errornodescription'] = 'Description is required';
 $string['errornoeventname'] = 'Name is required';
 $string['errornonyearlyfreqwithbyweekno'] = 'The BYWEEKNO rule is only valid for YEARLY rules.';
 $string['errorrequiredurlorfile'] = 'Either a URL or a file is required to import a calendar.';
-$string['errorrrule'] = 'The passed rrule seems incorrect';
-$string['errorrrulefreq'] = 'The rrule has an invalid frequency parameter';
-$string['errorrruleday'] = 'The rrule has an invalid day parameter';
+$string['errorrrule'] = 'The passed recurrence rule seems incorrect.';
+$string['errorrrulefreq'] = 'The recurrence rule has an invalid frequency parameter.';
+$string['errorrruleday'] = 'The recurrence rule has an invalid day parameter.';
 $string['eventdate'] = 'Date';
 $string['eventdescription'] = 'Description';
 $string['eventduration'] = 'Duration';
@@ -93,6 +93,7 @@ $string['eventendtime'] = 'End time';
 $string['eventinstanttime'] = 'Time';
 $string['eventkind'] = 'Type of event';
 $string['eventname'] = 'Event title';
+$string['eventnameandcourse'] = '{$a->course}: {$a->name}';
 $string['eventnone'] = 'No events';
 $string['eventrepeat'] = 'Repeats';
 $string['eventsall'] = 'All events';
index d5e1d5f..3bf13d5 100644 (file)
@@ -162,8 +162,8 @@ $string['registeredsites'] = 'Registered sites';
 $string['registrationinfo'] = 'Registration information';
 $string['registeredmoodleorg'] = 'Moodle ({$a})';
 $string['registeredon'] = 'Where your site is registered';
-$string['registereduserdevices'] = 'Number of user\'s registered mobile devices ({$a})';
-$string['registeredactiveuserdevices'] = 'Number of active user\'s registered mobile devices ({$a})';
+$string['registereduserdevices'] = 'Number of users with registered mobile devices ({$a})';
+$string['registeredactiveuserdevices'] = 'Number of active users with registered mobile devices which are receiving notifications ({$a})';
 $string['registermoochtips'] = 'Register your site with Moodle to get security alerts and access to Moodle.net, our course sharing platform.';
 $string['registersite'] = 'Register with {$a}';
 $string['registerwith'] = 'Register with a hub';
index 1a89457..53825f5 100644 (file)
@@ -155,6 +155,7 @@ $string['linkexternal'] = 'Link external';
 $string['listview'] = 'View as list';
 $string['loading'] = 'Loading...';
 $string['login'] = 'Login to your account';
+$string['logintoaccount'] = 'Login to your {$a} account';
 $string['logout'] = 'Logout';
 $string['lostsource'] = 'Error. Source is missing. {$a}';
 $string['makefileinternal'] = 'Make a copy of the file';
index 7fc7d30..5386669 100644 (file)
@@ -8122,21 +8122,25 @@ function admin_find_write_settings($node, $data) {
     }
 
     if ($node instanceof admin_category) {
-        $entries = array_keys($node->children);
-        foreach ($entries as $entry) {
-            $return = array_merge($return, admin_find_write_settings($node->children[$entry], $data));
+        if ($node->check_access()) {
+            $entries = array_keys($node->children);
+            foreach ($entries as $entry) {
+                $return = array_merge($return, admin_find_write_settings($node->children[$entry], $data));
+            }
         }
 
     } else if ($node instanceof admin_settingpage) {
+        if ($node->check_access()) {
             foreach ($node->settings as $setting) {
                 $fullname = $setting->get_full_name();
                 if (array_key_exists($fullname, $data)) {
                     $return[$fullname] = $setting;
                 }
             }
-
         }
 
+    }
+
     return $return;
 }
 
index 96fcee8..8247f32 100644 (file)
Binary files a/lib/amd/build/ajax.min.js and b/lib/amd/build/ajax.min.js differ
index 35b66ab..9ed5a35 100644 (file)
Binary files a/lib/amd/build/notification.min.js and b/lib/amd/build/notification.min.js differ
index b0267a8..481247c 100644 (file)
@@ -46,6 +46,18 @@ define(['jquery', 'core/config', 'core/log'], function($, config, Log) {
         var request;
         var response;
 
+        if (responses.error) {
+            // There was an error with the request as a whole.
+            // We need to reject each promise.
+            // Unfortunately this may lead to duplicate dialogues, but each Promise must be rejected.
+            for (; i < requests.length; i++) {
+                request = requests[i];
+                request.deferred.reject(responses);
+            }
+
+            return;
+        }
+
         for (i = 0; i < requests.length; i++) {
             request = requests[i];
 
@@ -81,8 +93,9 @@ define(['jquery', 'core/config', 'core/log'], function($, config, Log) {
      * @private
      * @param {jqXHR} jqXHR The ajax object.
      * @param {string} textStatus The status string.
+     * @param {Error|Object} exception The error thrown.
      */
-    var requestFail = function(jqXHR, textStatus) {
+    var requestFail = function(jqXHR, textStatus, exception) {
         // Reject all the promises.
         var requests = this;
 
@@ -92,9 +105,10 @@ define(['jquery', 'core/config', 'core/log'], function($, config, Log) {
 
             if (unloading) {
                 // No need to trigger an error because we are already navigating.
-                Log.error("Page unload: " + textStatus);
+                Log.error("Page unloaded.");
+                Log.error(exception);
             } else {
-                request.deferred.reject(textStatus);
+                request.deferred.reject(exception);
             }
         }
     };
index e3212ba..601e50e 100644 (file)
@@ -168,6 +168,9 @@ function(Y, $, log) {
             if (ex.debuginfo) {
                 ex.stack += ex.debuginfo + '\n';
             }
+            if (!ex.backtrace && ex.stacktrace) {
+                ex.backtrace = ex.stacktrace;
+            }
             if (ex.backtrace) {
                 ex.stack += ex.backtrace;
                 var ln = ex.backtrace.match(/line ([^ ]*) of/);
index d9fe32d..d00e46f 100644 (file)
@@ -76,8 +76,6 @@ abstract class base {
      * Output file headers to initialise the download of the file.
      */
     public function send_http_headers() {
-        global $CFG;
-
         if (defined('BEHAT_SITE_RUNNING')) {
             // For text based formats - we cannot test the output with behat if we force a file download.
             return;
@@ -98,11 +96,18 @@ abstract class base {
     }
 
     /**
-     * Write the start of the format
+     * Write the start of the file.
+     */
+    public function start_output() {
+        // Override me if needed.
+    }
+
+    /**
+     * Write the start of the sheet we will be adding data to.
      *
      * @param array $columns
      */
-    public function write_header($columns) {
+    public function start_sheet($columns) {
         // Override me if needed.
     }
 
@@ -115,12 +120,18 @@ abstract class base {
     abstract public function write_record($record, $rownum);
 
     /**
-     * Write the end of the format
+     * Write the end of the sheet containing the data.
      *
      * @param array $columns
      */
-    public function write_footer($columns) {
+    public function close_sheet($columns) {
         // Override me if needed.
     }
 
+    /**
+     * Write the end of the file.
+     */
+    public function close_output() {
+        // Override me if needed.
+    }
 }
index 9208e60..6b0c10b 100644 (file)
@@ -44,6 +44,9 @@ abstract class spout_base extends \core\dataformat\base {
     /** @var $sheettitle */
     protected $sheettitle;
 
+    /** @var $renamecurrentsheet */
+    protected $renamecurrentsheet = false;
+
     /**
      * Output file headers to initialise the download of the file.
      */
@@ -54,10 +57,9 @@ abstract class spout_base extends \core\dataformat\base {
         }
         $filename = $this->filename . $this->get_extension();
         $this->writer->openToBrowser($filename);
-        if ($this->sheettitle && $this->writer instanceof \Box\Spout\Writer\AbstractMultiSheetsWriter) {
-            $sheet = $this->writer->getCurrentSheet();
-            $sheet->setName($this->sheettitle);
-        }
+
+        // By default one sheet is always created, but we want to rename it when we call start_sheet().
+        $this->renamecurrentsheet = true;
     }
 
     /**
@@ -68,18 +70,24 @@ abstract class spout_base extends \core\dataformat\base {
      * @param string $title
      */
     public function set_sheettitle($title) {
-        if (!$title) {
-            return;
-        }
         $this->sheettitle = $title;
     }
 
     /**
-     * Write the start of the format
+     * Write the start of the sheet we will be adding data to.
      *
      * @param array $columns
      */
-    public function write_header($columns) {
+    public function start_sheet($columns) {
+        if ($this->sheettitle && $this->writer instanceof \Box\Spout\Writer\AbstractMultiSheetsWriter) {
+            if ($this->renamecurrentsheet) {
+                $sheet = $this->writer->getCurrentSheet();
+                $this->renamecurrentsheet = false;
+            } else {
+                $sheet = $this->writer->addNewSheetAndMakeItCurrent();
+            }
+            $sheet->setName($this->sheettitle);
+        }
         $this->writer->addRow(array_values((array)$columns));
     }
 
@@ -94,13 +102,10 @@ abstract class spout_base extends \core\dataformat\base {
     }
 
     /**
-     * Write the end of the format
-     *
-     * @param array $columns
+     * Write the end of the file.
      */
-    public function write_footer($columns) {
+    public function close_output() {
         $this->writer->close();
         $this->writer = null;
     }
-
 }
index 89edd59..2a25897 100644 (file)
@@ -249,7 +249,7 @@ class icon_system_fontawesome extends icon_system_font {
             'core:i/move_2d' => 'fa-arrows',
             'core:i/navigationitem' => 'fa-fw',
             'core:i/ne_red_mark' => 'fa-remove',
-            'core:i/new' => 'fa-plus',
+            'core:i/new' => 'fa-bolt',
             'core:i/news' => 'fa-newspaper-o',
             'core:i/nosubcat' => 'fa-plus-square-o',
             'core:i/notifications' => 'fa-bell',
index 7ba9b2a..5644b4b 100644 (file)
@@ -34,16 +34,16 @@ namespace core\output;
  */
 class mustache_javascript_helper {
 
-    /** @var page_requirements_manager $requires - Page requirements manager for collecting JS calls. */
-    private $requires = null;
+    /** @var moodle_page $page - Page used to get requirement manager */
+    private $page = null;
 
     /**
      * Create new instance of mustache javascript helper.
      *
-     * @param page_requirements_manager $requires Page requirements manager.
+     * @param moodle_page $page Page.
      */
-    public function __construct($requires) {
-        $this->requires = $requires;
+    public function __construct($page) {
+        $this->page = $page;
     }
 
     /**
@@ -54,6 +54,6 @@ class mustache_javascript_helper {
      * @param \Mustache_LambdaHelper $helper Used to render the content of this block.
      */
     public function help($text, \Mustache_LambdaHelper $helper) {
-        $this->requires->js_amd_inline($helper->render($text));
+        $this->page->requires->js_amd_inline($helper->render($text));
     }
 }
index 22da707..5451edb 100644 (file)
@@ -146,7 +146,7 @@ class core_plugin_manager {
     public function get_plugin_types() {
         if (func_num_args() > 0) {
             if (!func_get_arg(0)) {
-                throw coding_exception('core_plugin_manager->get_plugin_types() does not support relative paths.');
+                throw new coding_exception('core_plugin_manager->get_plugin_types() does not support relative paths.');
             }
         }
         if ($this->plugintypes) {
index a7c5e40..669ca97 100644 (file)
@@ -329,6 +329,25 @@ function css_send_cached_css_content($csscontent, $etag) {
     die;
 }
 
+/**
+ * Sends CSS directly and disables all caching.
+ * The Content-Length of the body is also included, but the script is not ended.
+ *
+ * @param string $css The CSS content to send
+ */
+function css_send_temporary_css($css) {
+    header('Cache-Control: no-cache, no-store, must-revalidate');
+    header('Pragma: no-cache');
+    header('Expires: 0');
+    header('Content-Disposition: inline; filename="styles_debug.php"');
+    header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT');
+    header('Accept-Ranges: none');
+    header('Content-Type: text/css; charset=utf-8');
+    header('Content-Length: ' . strlen($css));
+
+    echo $css;
+}
+
 /**
  * Sends CSS directly without caching it.
  *
@@ -382,4 +401,4 @@ function css_send_unmodified($lastmodified, $etag) {
 function css_send_css_not_found() {
     header('HTTP/1.0 404 not found');
     die('CSS was not found, sorry.');
-}
\ No newline at end of file
+}
index 36b5173..fad18d6 100644 (file)
@@ -56,7 +56,15 @@ function download_as_dataformat($filename, $dataformat, $columns, $iterator, $ca
 
     $format->set_filename($filename);
     $format->send_http_headers();
-    $format->write_header($columns);
+    // This exists to support all dataformats - see MDL-56046.
+    if (method_exists($format, 'write_header')) {
+        error_log('The function write_header() does not support multiple sheets. In order to support multiple sheets you ' .
+            'must implement start_output() and start_sheet() and remove write_header() in your dataformat.');
+        $format->write_header($columns);
+    } else {
+        $format->start_output();
+        $format->start_sheet($columns);
+    }
     $c = 0;
     foreach ($iterator as $row) {
         if ($callback) {
@@ -67,6 +75,14 @@ function download_as_dataformat($filename, $dataformat, $columns, $iterator, $ca
         }
         $format->write_record($row, $c++);
     }
-    $format->write_footer($columns);
+    // This exists to support all dataformats - see MDL-56046.
+    if (method_exists($format, 'write_footer')) {
+        error_log('The function write_footer() does not support multiple sheets. In order to support multiple sheets you ' .
+            'must implement close_sheet() and close_output() and remove write_footer() in your dataformat.');
+        $format->write_footer($columns);
+    } else {
+        $format->close_sheet($columns);
+        $format->close_output();
+    }
 }
 
index d608f24..65d0504 100644 (file)
@@ -42,6 +42,14 @@ $functions = array(
         'ajax'          => true,
         'loginrequired' => false,
     ),
+    'core_auth_request_password_reset' => array(
+        'classname'   => 'core_auth_external',
+        'methodname'  => 'request_password_reset',
+        'description' => 'Requests a password reset.',
+        'type'        => 'write',
+        'ajax'          => true,
+        'loginrequired' => false,
+    ),
     'core_badges_get_user_badges' => array(
         'classname'     => 'core_badges_external',
         'methodname'    => 'get_user_badges',
index 13647e8..f1dc48d 100644 (file)
@@ -2887,7 +2887,7 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2017061301.00);
     }
 
-    if ($oldversion < 2017062700.00) {
+    if ($oldversion < 2017071000.00) {
 
         // Define field requireconfirmation to be added to oauth2_issuer.
         $table = new xmldb_table('oauth2_issuer');
@@ -2899,7 +2899,7 @@ function xmldb_main_upgrade($oldversion) {
         }
 
         // Main savepoint reached.
-        upgrade_main_savepoint(true, 2017062700.00);
+        upgrade_main_savepoint(true, 2017071000.00);
     }
 
     return true;
index 28faa7e..20112a0 100644 (file)
@@ -972,7 +972,9 @@ function file_save_draft_area_files($draftitemid, $contextid, $component, $filea
                 if (!empty($repoid)) {
                     $context = context::instance_by_id($contextid, MUST_EXIST);
                     $repo = repository::get_repository_by_id($repoid, $context);
-
+                    if (!empty($options)) {
+                        $repo->options = $options;
+                    }
                     $file_record['repositoryid'] = $repoid;
                     // This hook gives the repo a place to do some house cleaning, and update the $reference before it's saved
                     // to the file store. E.g. transfer ownership of the file to a system account etc.
@@ -3886,9 +3888,10 @@ class curl_cache {
  * @param null|string $preview the preview mode, defaults to serving the original file
  * @param boolean $offline If offline is requested - don't serve a redirect to an external file, return a file suitable for viewing
  *                         offline (e.g. mobile app).
+ * @param bool $embed Whether this file will be served embed into an iframe.
  * @todo MDL-31088 file serving improments
  */
-function file_pluginfile($relativepath, $forcedownload, $preview = null, $offline = false) {
+function file_pluginfile($relativepath, $forcedownload, $preview = null, $offline = false, $embed = false) {
     global $DB, $CFG, $USER;
     // relative path must start with '/'
     if (!$relativepath) {
@@ -3912,7 +3915,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null, $offlin
 
     $fs = get_file_storage();
 
-    $sendfileoptions = ['preview' => $preview, 'offline' => $offline];
+    $sendfileoptions = ['preview' => $preview, 'offline' => $offline, 'embed' => $embed];
 
     // ========================================================================================================================
     if ($component === 'blog') {
index 8c4ee1d..79981cc 100644 (file)
@@ -2272,8 +2272,28 @@ class global_navigation extends navigation_node {
             return false;
         }
         // Add a branch for the current user.
-        $canseefullname = has_capability('moodle/site:viewfullnames', $coursecontext);
-        $usernode = $usersnode->add(fullname($user, $canseefullname), $userviewurl, self::TYPE_USER, null, 'user' . $user->id);
+        // Only reveal user details if $user is the current user, or a user to which the current user has access.
+        $viewprofile = true;
+        if (!$iscurrentuser) {
+            require_once($CFG->dirroot . '/user/lib.php');
+            if ($this->page->context->contextlevel == CONTEXT_USER && !has_capability('moodle/user:viewdetails', $usercontext) ) {
+                $viewprofile = false;
+            } else if ($this->page->context->contextlevel != CONTEXT_USER && !user_can_view_profile($user, $course, $usercontext)) {
+                $viewprofile = false;
+            }
+            if (!$viewprofile) {
+                $viewprofile = user_can_view_profile($user, null, $usercontext);
+            }
+        }
+
+        // Now, conditionally add the user node.
+        if ($viewprofile) {
+            $canseefullname = has_capability('moodle/site:viewfullnames', $coursecontext);
+            $usernode = $usersnode->add(fullname($user, $canseefullname), $userviewurl, self::TYPE_USER, null, 'user' . $user->id);
+        } else {
+            $usernode = $usersnode->add(get_string('user'));
+        }
+
         if ($this->page->context->contextlevel == CONTEXT_USER && $user->id == $this->page->context->instanceid) {
             $usernode->make_active();
         }
@@ -3651,7 +3671,7 @@ class breadcrumb_navigation_node extends navigation_node {
                  $this->$key = $value;
             }
         } else {
-            throw coding_exception('Not a valid breadcrumb_navigation_node');
+            throw new coding_exception('Not a valid breadcrumb_navigation_node');
         }
     }
 
@@ -3705,7 +3725,7 @@ class flat_navigation_node extends navigation_node {
                  $this->$key = $value;
             }
         } else {
-            throw coding_exception('Not a valid flat_navigation_node');
+            throw new coding_exception('Not a valid flat_navigation_node');
         }
         $this->indent = $indent;
     }
index 3ec3b99..a1be437 100644 (file)
@@ -4747,6 +4747,10 @@ class progress_bar implements renderable, templatable {
      * @param bool $autostart Whether to start the progress bar right away.
      */
     public function __construct($htmlid = '', $width = 500, $autostart = false) {
+        if (!defined('NO_OUTPUT_BUFFERING') || !NO_OUTPUT_BUFFERING) {
+            debugging('progress_bar used without setting NO_OUTPUT_BUFFERING.', DEBUG_DEVELOPER);
+        }
+
         if (!empty($htmlid)) {
             $this->html_id  = $htmlid;
         } else {
index 34b54e6..e99177d 100644 (file)
@@ -35,15 +35,58 @@ require_once($CFG->libdir.'/outputrenderers.php');
 require_once($CFG->libdir.'/outputrequirementslib.php');
 
 /**
- * Invalidate all server and client side caches.
+ * Returns current theme revision number.
  *
- * This method deletes the physical directory that is used to cache the theme
- * files used for serving.
- * Because it deletes the main theme cache directory all themes are reset by
- * this function.
+ * @return int
  */
-function theme_reset_all_caches() {
-    global $CFG, $PAGE;
+function theme_get_revision() {
+    global $CFG;
+
+    if (empty($CFG->themedesignermode)) {
+        if (empty($CFG->themerev)) {
+            // This only happens during install. It doesn't matter what themerev we use as long as it's positive.
+            return 1;
+        } else {
+            return $CFG->themerev;
+        }
+
+    } else {
+        return -1;
+    }
+}
+
+/**
+ * Returns current theme sub revision number. This is the revision for
+ * this theme exclusively, not the global theme revision.
+ *
+ * @param string $themename The non-frankenstyle name of the theme
+ * @return int
+ */
+function theme_get_sub_revision_for_theme($themename) {
+    global $CFG;
+
+    if (empty($CFG->themedesignermode)) {
+        $pluginname = "theme_{$themename}";
+        $revision = during_initial_install() ? null : get_config($pluginname, 'themerev');
+
+        if (empty($revision)) {
+            // This only happens during install. It doesn't matter what themerev we use as long as it's positive.
+            return 1;
+        } else {
+            return $revision;
+        }
+    } else {
+        return -1;
+    }
+}
+
+/**
+ * Calculates and returns the next theme revision number.
+ *
+ * @return int
+ */
+function theme_get_next_revision() {
+    global $CFG;
 
     $next = time();
     if (isset($CFG->themerev) and $next <= $CFG->themerev and $CFG->themerev - $next < 60*60) {
@@ -53,7 +96,155 @@ function theme_reset_all_caches() {
         $next = $CFG->themerev+1;
     }
 
-    set_config('themerev', $next); // time is unique even when you reset/switch database
+    return $next;
+}
+
+/**
+ * Calculates and returns the next theme revision number.
+ *
+ * @param string $themename The non-frankenstyle name of the theme
+ * @return int
+ */
+function theme_get_next_sub_revision_for_theme($themename) {
+    global $CFG;
+
+    $next = time();
+    $current = theme_get_sub_revision_for_theme($themename);
+    if ($next <= $current and $current - $next < 60 * 60) {
+        // This resolves problems when reset is requested repeatedly within 1s,
+        // the < 1h condition prevents accidental switching to future dates
+        // because we might not recover from it.
+        $next = $current + 1;
+    }
+
+    return $next;
+}
+
+/**
+ * Sets the current theme revision number.
+ *
+ * @param int $revision The new theme revision number
+ */
+function theme_set_revision($revision) {
+    set_config('themerev', $revision);
+}
+
+/**
+ * Sets the current theme revision number for a specific theme.
+ * This does not affect the global themerev value.
+ *
+ * @param string $themename The non-frankenstyle name of the theme
+ * @param int    $revision  The new theme revision number
+ */
+function theme_set_sub_revision_for_theme($themename, $revision) {
+    set_config('themerev', $revision, "theme_{$themename}");
+}
+
+/**
+ * Get the path to a theme config.php file.
+ *
+ * @param string $themename The non-frankenstyle name of the theme to check
+ */
+function theme_get_config_file_path($themename) {
+    global $CFG;
+
+    if (file_exists("{$CFG->dirroot}/theme/{$themename}/config.php")) {
+        return "{$CFG->dirroot}/theme/{$themename}/config.php";
+    } else if (!empty($CFG->themedir) and file_exists("{$CFG->themedir}/{$themename}/config.php")) {
+        return "{$CFG->themedir}/{$themename}/config.php";
+    } else {
+        return null;
+    }
+}
+
+/**
+ * Get the path to the local cached CSS file.
+ *
+ * @param string $themename      The non-frankenstyle theme name.
+ * @param int    $globalrevision The global theme revision.
+ * @param int    $themerevision  The theme specific revision.
+ * @param string $direction      Either 'ltr' or 'rtl' (case sensitive).
+ */
+function theme_get_css_filename($themename, $globalrevision, $themerevision, $direction) {
+    global $CFG;
+
+    $path = "{$CFG->localcachedir}/theme/{$globalrevision}/{$themename}/css";
+    $filename = $direction == 'rtl' ? "all-rtl_{$themerevision}" : "all_{$themerevision}";
+    return "{$path}/{$filename}.css";
+}
+
+/**
+ * Generates and saves the CSS files for the given theme configs.
+ *
+ * @param theme_config[] $themeconfigs An array of theme_config instances.
+ * @param array          $directions   Must be a subset of ['rtl', 'ltr'].
+ * @param bool           $cache        Should the generated files be stored in local cache.
+ */
+function theme_build_css_for_themes($themeconfigs = [], $directions = ['rtl', 'ltr'], $cache = true) {
+    global $CFG;
+
+    if (empty($themeconfigs)) {
+        return;
+    }
+
+    require_once("{$CFG->libdir}/csslib.php");
+
+    $themescss = [];
+    $themerev = theme_get_revision();
+    // Make sure the local cache directory exists.
+    make_localcache_directory('theme');
+
+    foreach ($themeconfigs as $themeconfig) {
+        $themecss = [];
+        $oldrevision = theme_get_sub_revision_for_theme($themeconfig->name);
+        $newrevision = theme_get_next_sub_revision_for_theme($themeconfig->name);
+
+        // First generate all the new css.
+        foreach ($directions as $direction) {
+            $themeconfig->set_rtl_mode(($direction === 'rtl'));
+
+            $themecss[$direction] = $themeconfig->get_css_content();
+            if ($cache) {
+                $filename = theme_get_css_filename($themeconfig->name, $themerev, $newrevision, $direction);
+                css_store_css($themeconfig, $filename, $themecss[$direction]);
+            }
+        }
+        $themescss[] = $themecss;
+
+        if ($cache) {
+            // Only update the theme revision after we've successfully created the
+            // new CSS cache.
+            theme_set_sub_revision_for_theme($themeconfig->name, $newrevision);
+
+            // Now purge old files. We must purge all old files in the local cache
+            // because we've incremented the theme sub revision. This will leave any
+            // files with the old revision inaccessbile so we might as well removed
+            // them from disk.
+            foreach (['ltr', 'rtl'] as $direction) {
+                $oldcss = theme_get_css_filename($themeconfig->name, $themerev, $oldrevision, $direction);
+                if (file_exists($oldcss)) {
+                    unlink($oldcss);
+                }
+            }
+        }
+    }
+
+    return $themescss;
+}
+
+/**
+ * Invalidate all server and client side caches.
+ *
+ * This method deletes the physical directory that is used to cache the theme
+ * files used for serving.
+ * Because it deletes the main theme cache directory all themes are reset by
+ * this function.
+ */
+function theme_reset_all_caches() {
+    global $CFG, $PAGE;
+
+    $next = theme_get_next_revision();
+    theme_set_revision($next);
 
     if (!empty($CFG->themedesignermode)) {
         $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'themedesigner');
@@ -79,27 +270,6 @@ function theme_set_designer_mod($state) {
     theme_reset_all_caches();
 }
 
-/**
- * Returns current theme revision number.
- *
- * @return int
- */
-function theme_get_revision() {
-    global $CFG;
-
-    if (empty($CFG->themedesignermode)) {
-        if (empty($CFG->themerev)) {
-            // This only happens during install. It doesn't matter what themerev we use as long as it's positive.
-            return 1;
-        } else {
-            return $CFG->themerev;
-        }
-
-    } else {
-        return -1;
-    }
-}
-
 /**
  * Checks if the given device has a theme defined in config.php.
  *
@@ -193,6 +363,12 @@ class theme_config {
      */
     public $editor_sheets = array();
 
+    /**
+     * @var bool Whether a fallback version of the stylesheet will be used
+     * whilst the final version is generated.
+     */
+    public $usefallback = false;
+
     /**
      * @var array The names of all the javascript files this theme that you would
      * like included from head, in order. Give the names of the files without .js.
@@ -375,7 +551,7 @@ class theme_config {
      * @var stdClass Theme settings stored in config_plugins table.
      * This can not be set in theme config.php
      */
-    public $setting = null;
+    public $settings = null;
 
     /**
      * @var bool If set to true and the theme enables the dock then  blocks will be able
@@ -554,7 +730,7 @@ class theme_config {
         }
 
         $configurable = array(
-            'parents', 'sheets', 'parents_exclude_sheets', 'plugins_exclude_sheets',
+            'parents', 'sheets', 'parents_exclude_sheets', 'plugins_exclude_sheets', 'usefallback',
             'javascripts', 'javascripts_footer', 'parents_exclude_javascripts',
             'layouts', 'enable_dock', 'enablecourseajax', 'requiredblocks',
             'rendererfactory', 'csspostprocess', 'editor_sheets', 'rarrow', 'larrow', 'uarrow', 'darrow',
@@ -791,6 +967,14 @@ class theme_config {
         if ($rev > -1) {
             $filename = right_to_left() ? 'all-rtl' : 'all';
             $url = new moodle_url("$CFG->httpswwwroot/theme/styles.php");
+            $themesubrevision = theme_get_sub_revision_for_theme($this->name);
+
+            // Provide the sub revision to allow us to invalidate cached theme CSS
+            // on a per theme basis, rather than globally.
+            if ($themesubrevision && $themesubrevision > 0) {
+                $rev .= "_{$themesubrevision}";
+            }
+
             if (!empty($CFG->slasharguments)) {
                 $slashargs = '';
                 if (!$svg) {
@@ -924,6 +1108,19 @@ class theme_config {
         return $cache->set($key, $csscontent);
     }
 
+    /**
+     * Return whether the post processed CSS content has been cached.
+     *
+     * @return bool Whether the post-processed CSS is available in the cache.
+     */
+    public function has_css_cached_content() {
+
+        $key = $this->get_css_cache_key();
+        $cache = cache::make('core', 'postprocessedcss');
+
+        return $cache->has($key);
+    }
+
     /**
      * Return cached post processed CSS content.
      *
@@ -2040,6 +2237,15 @@ class theme_config {
         $this->rtlmode = $inrtl;
     }
 
+    /**
+     * Whether the theme is being served in RTL mode.
+     *
+     * @return bool True when in RTL mode.
+     */
+    public function get_rtl_mode() {
+        return $this->rtlmode;
+    }
+
     /**
      * Checks if file with any image extension exists.
      *
index 0aae68d..139e6f9 100644 (file)
@@ -89,7 +89,7 @@ class renderer_base {
             $loader = new \core\output\mustache_filesystem_loader();
             $stringhelper = new \core\output\mustache_string_helper();
             $quotehelper = new \core\output\mustache_quote_helper();
-            $jshelper = new \core\output\mustache_javascript_helper($this->page->requires);
+            $jshelper = new \core\output\mustache_javascript_helper($this->page);
             $pixhelper = new \core\output\mustache_pix_helper($this);
             $shortentexthelper = new \core\output\mustache_shorten_text_helper();
             $userdatehelper = new \core\output\mustache_user_date_helper();
@@ -650,6 +650,16 @@ class core_renderer extends renderer_base {
                     'type' => $type, 'title' => $alt->title, 'href' => $alt->url));
         }
 
+        // Add noindex tag if relevant page and setting applied.
+        $allowindexing = isset($CFG->allowindexing) ? $CFG->allowindexing : 0;
+        $loginpages = array('login-index', 'login-signup');
+        if ($allowindexing == 2 || ($allowindexing == 0 && in_array($this->page->pagetype, $loginpages))) {
+            if (!isset($CFG->additionalhtmlhead)) {
+                $CFG->additionalhtmlhead = '';
+            }
+            $CFG->additionalhtmlhead .= '<meta name="robots" content="noindex" />';
+        }
+
         if (!empty($CFG->additionalhtmlhead)) {
             $output .= "\n".$CFG->additionalhtmlhead;
         }
@@ -731,16 +741,26 @@ class core_renderer extends renderer_base {
     public function standard_footer_html() {
         global $CFG, $SCRIPT;
 
+        $output = '';
         if (during_initial_install()) {
             // Debugging info can not work before install is finished,
             // in any case we do not want any links during installation!
-            return '';
+            return $output;
+        }
+
+        // Give plugins an opportunity to add any footer elements.
+        // The callback must always return a string containing valid html footer content.
+        $pluginswithfunction = get_plugins_with_function('standard_footer_html', 'lib.php');
+        foreach ($pluginswithfunction as $plugins) {
+            foreach ($plugins as $function) {
+                $output .= $function();
+            }
         }
 
         // This function is normally called from a layout.php file in {@link core_renderer::header()}
         // but some of the content won't be known until later, so we return a placeholder
         // for now. This will be replaced with the real content in {@link core_renderer::footer()}.
-        $output = $this->unique_performance_info_token;
+        $output .= $this->unique_performance_info_token;
         if ($this->page->devicetypeinuse == 'legacy') {
             // The legacy theme is in use print the notification
             $output .= html_writer::tag('div', get_string('legacythemeinuse'), array('class'=>'legacythemeinuse'));
@@ -4055,16 +4075,16 @@ EOD;
 
     public function context_header($headerinfo = null, $headinglevel = 1) {
         global $DB, $USER, $CFG;
+        require_once($CFG->dirroot . '/user/lib.php');
         $context = $this->page->context;
+        $heading = null;
+        $imagedata = null;
+        $subheader = null;
+        $userbuttons = null;
         // Make sure to use the heading if it has been set.
         if (isset($headerinfo['heading'])) {
             $heading = $headerinfo['heading'];
-        } else {
-            $heading = null;
         }
-        $imagedata = null;
-        $subheader = null;
-        $userbuttons = null;
         // The user context currently has images and buttons. Other contexts may follow.
         if (isset($headerinfo['user']) || $context->contextlevel == CONTEXT_USER) {
             if (isset($headerinfo['user'])) {
@@ -4073,47 +4093,60 @@ EOD;
                 // Look up the user information if it is not supplied.
                 $user = $DB->get_record('user', array('id' => $context->instanceid));
             }
+
             // If the user context is set, then use that for capability checks.
             if (isset($headerinfo['usercontext'])) {
                 $context = $headerinfo['usercontext'];
             }
-            // Use the user's full name if the heading isn't set.
-            if (!isset($heading)) {
-                $heading = fullname($user);
+
+            // Only provide user information if the user is the current user, or a user which the current user can view.
+            $canviewdetails = false;
+            if ($user->id == $USER->id || user_can_view_profile($user)) {
+                $canviewdetails = true;
             }
 
-            $imagedata = $this->user_picture($user, array('size' => 100));
-            // Check to see if we should be displaying a message button.
-            if (!empty($CFG->messaging) && $USER->id != $user->id && has_capability('moodle/site:sendmessage', $context)) {
-                $iscontact = !empty(message_get_contact($user->id));
-                $contacttitle = $iscontact ? 'removefromyourcontacts' : 'addtoyourcontacts';
-                $contacturlaction = $iscontact ? 'removecontact' : 'addcontact';
-                $contactimage = $iscontact ? 'removecontact' : 'addcontact';
-                $userbuttons = array(
-                    'messages' => array(
-                        'buttontype' => 'message',
-                        'title' => get_string('message', 'message'),
-                        'url' => new moodle_url('/message/index.php', array('id' => $user->id)),
-                        'image' => 'message',
-                        'linkattributes' => array('role' => 'button'),
-                        'page' => $this->page
-                    ),
-                    'togglecontact' => array(
-                        'buttontype' => 'togglecontact',
-                        'title' => get_string($contacttitle, 'message'),
-                        'url' => new moodle_url('/message/index.php', array(
-                                'user1' => $USER->id,
-                                'user2' => $user->id,
-                                $contacturlaction => $user->id,
-                                'sesskey' => sesskey())
+            if ($canviewdetails) {
+                // Use the user's full name if the heading isn't set.
+                if (!isset($heading)) {
+                    $heading = fullname($user);
+                }
+
+                $imagedata = $this->user_picture($user, array('size' => 100));
+
+                // Check to see if we should be displaying a message button.
+                if (!empty($CFG->messaging) && $USER->id != $user->id && has_capability('moodle/site:sendmessage', $context)) {
+                    $iscontact = !empty(message_get_contact($user->id));
+                    $contacttitle = $iscontact ? 'removefromyourcontacts' : 'addtoyourcontacts';
+                    $contacturlaction = $iscontact ? 'removecontact' : 'addcontact';
+                    $contactimage = $iscontact ? 'removecontact' : 'addcontact';
+                    $userbuttons = array(
+                        'messages' => array(
+                            'buttontype' => 'message',
+                            'title' => get_string('message', 'message'),
+                            'url' => new moodle_url('/message/index.php', array('id' => $user->id)),
+                            'image' => 'message',
+                            'linkattributes' => array('role' => 'button'),
+                            'page' => $this->page
                         ),
-                        'image' => $contactimage,
-                        'linkattributes' => \core_message\helper::togglecontact_link_params($user, $iscontact),
-                        'page' => $this->page
-                    ),
-                );
+                        'togglecontact' => array(
+                            'buttontype' => 'togglecontact',
+                            'title' => get_string($contacttitle, 'message'),
+                            'url' => new moodle_url('/message/index.php', array(
+                                    'user1' => $USER->id,
+                                    'user2' => $user->id,
+                                    $contacturlaction => $user->id,
+                                    'sesskey' => sesskey())
+                            ),
+                            'image' => $contactimage,
+                            'linkattributes' => \core_message\helper::togglecontact_link_params($user, $iscontact),
+                            'page' => $this->page
+                        ),
+                    );
 
-                $this->page->requires->string_for_js('changesmadereallygoaway', 'moodle');
+                    $this->page->requires->string_for_js('changesmadereallygoaway', 'moodle');
+                }
+            } else {
+                $heading = null;
             }
         }
 
index 4dd84f8..d1f1adc 100644 (file)
@@ -125,6 +125,12 @@ class flexible_table {
      */
     private $prefs = array();
 
+    /** @var $sheettitle */
+    protected $sheettitle;
+
+    /** @var $filename */
+    protected $filename;
+
     /**
      * Constructor
      * @param string $uniqueid all tables have to have a unique id, this is used
@@ -180,7 +186,7 @@ class flexible_table {
         } else if (is_null($this->exportclass) && !empty($this->download)) {
             $this->exportclass = new table_dataformat_export_format($this, $this->download);
             if (!$this->exportclass->document_started()) {
-                $this->exportclass->start_document($this->filename);
+                $this->exportclass->start_document($this->filename, $this->sheettitle);
             }
         }
         return $this->exportclass;
@@ -1741,11 +1747,14 @@ class table_dataformat_export_format extends table_default_export_format_parent
      * Start document
      *
      * @param string $filename
+     * @param string $sheettitle
      */
-    public function start_document($filename) {
-        $this->filename = $filename;
+    public function start_document($filename, $sheettitle) {
         $this->documentstarted = true;
         $this->dataformat->set_filename($filename);
+        $this->dataformat->send_http_headers();
+        $this->dataformat->set_sheettitle($sheettitle);
+        $this->dataformat->start_output();
     }
 
     /**
@@ -1755,7 +1764,6 @@ class table_dataformat_export_format extends table_default_export_format_parent
      */
     public function start_table($sheettitle) {
         $this->dataformat->set_sheettitle($sheettitle);
-        $this->dataformat->send_http_headers();
     }
 
     /**
@@ -1765,7 +1773,13 @@ class table_dataformat_export_format extends table_default_export_format_parent
      */
     public function output_headers($headers) {
         $this->columns = $headers;
-        $this->dataformat->write_header($headers);
+        if (method_exists($this->dataformat, 'write_header')) {
+            error_log('The function write_header() does not support multiple sheets. In order to support multiple sheets you ' .
+                'must implement start_output() and start_sheet() and remove write_header() in your dataformat.');
+            $this->dataformat->write_header($headers);
+        } else {
+            $this->dataformat->start_sheet($headers);
+        }
     }
 
     /**
@@ -1782,15 +1796,21 @@ class table_dataformat_export_format extends table_default_export_format_parent
      * Finish export
      */
     public function finish_table() {
-        $this->dataformat->write_footer($this->columns);
+        if (method_exists($this->dataformat, 'write_footer')) {
+            error_log('The function write_footer() does not support multiple sheets. In order to support multiple sheets you ' .
+                'must implement close_sheet() and close_output() and remove write_footer() in your dataformat.');
+            $this->dataformat->write_footer($this->columns);
+        } else {
+            $this->dataformat->close_sheet($this->columns);
+        }
     }
 
     /**
      * Finish download
      */
     public function finish_document() {
-        exit;
+        $this->dataformat->close_output();
+        exit();
     }
-
 }
 
index 1d65ea1..a6c2cf3 100644 (file)
 
     </div>
 
+    <div class="signuppanel">
     {{#hasinstructions}}
-        <div class="signuppanel">
-            <h2>{{#str}} firsttime {{/str}}</h2>
-            <div class="subcontent">
-                {{{instructions}}}
-                {{#cansignup}}
-                    <div class="signupform">
-                        <form action="{{signupurl}}" method="get" id="signup">
-                            <div>
-                                <input type="submit" value={{#quote}}{{#str}} startsignup {{/str}}{{/quote}} />
-                            </div>
-                        </form>
-                    </div>
-                {{/cansignup}}
-            </div>
+        <h2>{{#str}} firsttime {{/str}}</h2>
+        <div class="subcontent">
+            {{{instructions}}}
+            {{#cansignup}}
+                <div class="signupform">
+                    <form action="{{signupurl}}" method="get" id="signup">
+                        <div>
+                            <input type="submit" value={{#quote}}{{#str}} startsignup {{/str}}{{/quote}} />
+                        </div>
+                    </form>
+                </div>
+            {{/cansignup}}
         </div>
     {{/hasinstructions}}
 
             <div class="potentialidplist">
                 {{#identityproviders}}
                     <div class="potentialidp">
-                        <a href="{{url}}" title={{#quote}}{{name}}{{/quote}}>
-                            {{#iconurl}}
-                                <img src="{{iconurl}}" alt="" width="24" height="24"/>
-                            {{/iconurl}}
-                            {{name}}
+                        <a href="{{url}}" title={{#quote}}{{name}}{{/quote}} class="btn">
+                        {{#iconurl}}
+                            <img src="{{iconurl}}" alt="" width="24" height="24"/>
+                        {{/iconurl}}
+                        {{name}}
                         </a>
                     </div>
                 {{/identityproviders}}
             </div>
         </div>
     {{/hasidentityproviders}}
+    </div>
 </div>
 
 {{#js}}
index f5dbf5f..f6d581d 100644 (file)
@@ -154,6 +154,7 @@ class core_admintree_testcase extends advanced_testcase {
     public function test_config_logging() {
         global $DB;
         $this->resetAfterTest();
+        $this->setAdminUser();
 
         $DB->delete_records('config_log', array());
 
index 5767dd1..31a3618 100644 (file)
@@ -198,7 +198,6 @@ Feature: Initials bar
     And I log in as "teacher"
     And I am on "Course 1" course homepage
     And I follow "Participants"
-    And I select "Brief" from the "mode" singleselect
     And ".initialbarall.letter.active" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
     And ".initialbarall.letter.active" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
     And ".letter.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
@@ -224,66 +223,6 @@ Feature: Initials bar
     And I should not see "Cstudent Cstudent"
     And I am on "Course 1" course homepage
     And I follow "Participants"
-    And I select "Brief" from the "mode" singleselect
-    And ".initialbarall.letter.active" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
-    And ".initialbarall.letter.active" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
-    And ".letter.active.B" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
-    And ".letter.active.A" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
-    And I should not see "Astudent Astudent"
-    And I should see "Bstudent Astudent"
-    And I should not see "Cstudent Cstudent"
-    And I click on "All" "link" in the ".initialbar.firstinitial" "css_element"
-    And ".initialbarall.letter.active" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
-    And ".initialbarall.letter.active" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
-    And ".letter.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
-    And ".letter.active.A" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
-    And I should see "Astudent Astudent"
-    And I should see "Bstudent Astudent"
-    And I should not see "Cstudent Cstudent"
-    And I click on "All" "link" in the ".initialbar.lastinitial" "css_element"
-    And ".initialbarall.letter.active" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
-    And ".initialbarall.letter.active" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
-    And ".letter.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
-    And ".letter.active.A" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
-    And I should see "Astudent Astudent"
-    And I should see "Bstudent Astudent"
-    And I should see "Cstudent Cstudent"
-
-  @javascript
-  Scenario: Filter users on course participants page
-    Given the following "activities" exist:
-      | activity | course | idnumber | name           | intro                       | assignsubmission_onlinetext_enabled | assignsubmission_file_enabled |
-      | assign   | C1     | assign1  | TestAssignment | Test assignment description | 0                                   | 0                             |
-    And I log in as "teacher"
-    And I am on "Course 1" course homepage
-    And I follow "Participants"
-    And I select "User details" from the "mode" singleselect
-    And ".initialbarall.letter.active" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
-    And ".initialbarall.letter.active" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
-    And ".letter.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
-    And ".letter.active.A" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
-    And I should see "Astudent Astudent"
-    And I should see "Bstudent Astudent"
-    And I should see "Cstudent Cstudent"
-    And I click on "A" "link" in the ".initialbar.lastinitial" "css_element"
-    And ".initialbarall.letter.active" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
-    And ".initialbarall.letter.active" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
-    And ".letter.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
-    And ".letter.active.A" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
-    And I should see "Astudent Astudent"
-    And I should see "Bstudent Astudent"
-    And I should not see "Cstudent Cstudent"
-    And I click on "B" "link" in the ".initialbar.firstinitial" "css_element"
-    And ".initialbarall.letter.active" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
-    And ".initialbarall.letter.active" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
-    And ".letter.active.B" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
-    And ".letter.active.A" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
-    And I should not see "Astudent Astudent"
-    And I should see "Bstudent Astudent"
-    And I should not see "Cstudent Cstudent"
-    And I am on "Course 1" course homepage
-    And I follow "Participants"
-    And I select "User details" from the "mode" singleselect
     And ".initialbarall.letter.active" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
     And ".initialbarall.letter.active" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
     And ".letter.active.B" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
index 25f79ae..f41a06f 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-debug.js and b/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-debug.js differ
index bb94ef7..79172f7 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-min.js and b/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-min.js differ
index 25f79ae..f41a06f 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception.js and b/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception.js differ
index a1b5842..e9581f7 100644 (file)
@@ -150,7 +150,7 @@ Y.extend(EXCEPTION, M.core.notification.info, {
                             "<div class='stacktrace-file'>$3</div>" +
                             "<div class='stacktrace-call'>$1</div>");
                 }
-                return lines.join('');
+                return lines.join("\n");
             },
             value: ''
         },
index e506a18..32e5310 100644 (file)
@@ -67,42 +67,9 @@ class login_forgot_password_form extends moodleform {
      * @return array errors occuring during validation.
      */
     function validation($data, $files) {
-        global $CFG, $DB;
 
         $errors = parent::validation($data, $files);
-
-        if ((!empty($data['username']) and !empty($data['email'])) or (empty($data['username']) and empty($data['email']))) {
-            $errors['username'] = get_string('usernameoremail');
-            $errors['email']    = get_string('usernameoremail');
-
-        } else if (!empty($data['email'])) {
-            if (!validate_email($data['email'])) {
-                $errors['email'] = get_string('invalidemail');
-
-            } else if ($DB->count_records('user', array('email'=>$data['email'])) > 1) {
-                $errors['email'] = get_string('forgottenduplicate');
-
-            } else {
-                if ($user = get_complete_user_data('email', $data['email'])) {
-                    if (empty($user->confirmed)) {
-                        $errors['email'] = get_string('confirmednot');
-                    }
-                }
-                if (!$user and empty($CFG->protectusernames)) {
-                    $errors['email'] = get_string('emailnotfound');
-                }
-            }
-
-        } else {
-            if ($user = get_complete_user_data('username', $data['username'])) {
-                if (empty($user->confirmed)) {
-                    $errors['email'] = get_string('confirmednot');
-                }
-            }
-            if (!$user and empty($CFG->protectusernames)) {
-                $errors['username'] = get_string('usernamenotfound');
-            }
-        }
+        $errors += core_login_validate_forgot_password_data($data);
 
         return $errors;
     }
index dec075c..ef92109 100644 (file)
 require('../config.php');
 require_once('lib.php');
 
-// Try to prevent searching for sites that allow sign-up.
-if (!isset($CFG->additionalhtmlhead)) {
-    $CFG->additionalhtmlhead = '';
-}
-$CFG->additionalhtmlhead .= '<meta name="robots" content="noindex" />';
-
 redirect_if_major_upgrade_required();
 
 $testsession = optional_param('testsession', 0, PARAM_INT); // test session works properly
index 614b2f3..9ea72fd 100644 (file)
@@ -35,125 +35,26 @@ define('PWRESET_STATUS_ALREADYSENT', 4);
  *  Where they have supplied identifier, the function will check their status, and send email as appropriate.
  */
 function core_login_process_password_reset_request() {
-    global $DB, $OUTPUT, $CFG, $PAGE;
-    $systemcontext = context_system::instance();
+    global $OUTPUT, $PAGE;
     $mform = new login_forgot_password_form();
 
     if ($mform->is_cancelled()) {
         redirect(get_login_url());
 
     } else if ($data = $mform->get_data()) {
-        // Requesting user has submitted form data.
-        // Next find the user account in the database which the requesting user claims to own.
+
+        $username = $email = '';
         if (!empty($data->username)) {
-            // Username has been specified - load the user record based on that.
-            $username = core_text::strtolower($data->username); // Mimic the login page process.
-            $userparams = array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'deleted' => 0, 'suspended' => 0);
-            $user = $DB->get_record('user', $userparams);
+            $username = $data->username;
         } else {
-            // Try to load the user record based on email address.
-            // this is tricky because
-            // 1/ the email is not guaranteed to be unique - TODO: send email with all usernames to select the account for pw reset
-            // 2/ mailbox may be case sensitive, the email domain is case insensitive - let's pretend it is all case-insensitive.
-
-            $select = $DB->sql_like('email', ':email', false, true, false, '|') .
-                    " AND mnethostid = :mnethostid AND deleted=0 AND suspended=0";
-            $params = array('email' => $DB->sql_like_escape($data->email, '|'), 'mnethostid' => $CFG->mnet_localhost_id);
-            $user = $DB->get_record_select('user', $select, $params, '*', IGNORE_MULTIPLE);
-        }
-
-        // Target user details have now been identified, or we know that there is no such account.
-        // Send email address to account's email address if appropriate.
-        $pwresetstatus = PWRESET_STATUS_NOEMAILSENT;
-        if ($user and !empty($user->confirmed)) {
-            $userauth = get_auth_plugin($user->auth);
-            if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)
-              or !has_capability('moodle/user:changeownpassword', $systemcontext, $user->id)) {
-                if (send_password_change_info($user)) {
-                    $pwresetstatus = PWRESET_STATUS_OTHEREMAILSENT;
-                } else {
-                    print_error('cannotmailconfirm');
-                }
-            } else {
-                // The account the requesting user claims to be is entitled to change their password.
-                // Next, check if they have an existing password reset in progress.
-                $resetinprogress = $DB->get_record('user_password_resets', array('userid' => $user->id));
-                if (empty($resetinprogress)) {
-                    // Completely new reset request - common case.
-                    $resetrecord = core_login_generate_password_reset($user);
-                    $sendemail = true;
-                } else if ($resetinprogress->timerequested < (time() - $CFG->pwresettime)) {
-                    // Preexisting, but expired request - delete old record & create new one.
-                    // Uncommon case - expired requests are cleaned up by cron.
-                    $DB->delete_records('user_password_resets', array('id' => $resetinprogress->id));
-                    $resetrecord = core_login_generate_password_reset($user);
-                    $sendemail = true;
-                } else if (empty($resetinprogress->timererequested)) {
-                    // Preexisting, valid request. This is the first time user has re-requested the reset.
-                    // Re-sending the same email once can actually help in certain circumstances
-                    // eg by reducing the delay caused by greylisting.
-                    $resetinprogress->timererequested = time();
-                    $DB->update_record('user_password_resets', $resetinprogress);
-                    $resetrecord = $resetinprogress;
-                    $sendemail = true;
-                } else {
-                    // Preexisting, valid request. User has already re-requested email.
-                    $pwresetstatus = PWRESET_STATUS_ALREADYSENT;
-                    $sendemail = false;
-                }
-
-                if ($sendemail) {
-                    $sendresult = send_password_change_confirmation_email($user, $resetrecord);
-                    if ($sendresult) {
-                        $pwresetstatus = PWRESET_STATUS_TOKENSENT;
-                    } else {
-                        print_error('cannotmailconfirm');
-                    }
-                }
-            }
+            $email = $data->email;
         }
+        list($status, $notice, $url) = core_login_process_password_reset($username, $email);
 
         // Any email has now been sent.
         // Next display results to requesting user if settings permit.
         echo $OUTPUT->header();
-
-        if (!empty($CFG->protectusernames)) {
-            // Neither confirm, nor deny existance of any username or email address in database.
-            // Print general (non-commital) message.
-            notice(get_string('emailpasswordconfirmmaybesent'), $CFG->wwwroot.'/index.php');
-            die; // Never reached.
-        } else if (empty($user)) {
-            // Protect usernames is off, and we couldn't find the user with details specified.
-            // Print failure advice.
-            notice(get_string('emailpasswordconfirmnotsent'), $CFG->wwwroot.'/forgot_password.php');
-            die; // Never reached.
-        } else if (empty($user->email)) {
-            // User doesn't have an email set - can't send a password change confimation email.
-            notice(get_string('emailpasswordconfirmnoemail'), $CFG->wwwroot.'/index.php');
-            die; // Never reached.
-        } else if ($pwresetstatus == PWRESET_STATUS_ALREADYSENT) {
-            // User found, protectusernames is off, but user has already (re) requested a reset.
-            // Don't send a 3rd reset email.
-            $stremailalreadysent = get_string('emailalreadysent');
-            notice($stremailalreadysent, $CFG->wwwroot.'/index.php');
-            die; // Never reached.
-        } else if ($pwresetstatus == PWRESET_STATUS_NOEMAILSENT) {
-            // User found, protectusernames is off, but user is not confirmed.
-            // Pretend we sent them an email.
-            // This is a big usability problem - need to tell users why we didn't send them an email.
-            // Obfuscate email address to protect privacy.
-            $protectedemail = preg_replace('/([^@]*)@(.*)/', '******@$2', $user->email);
-            $stremailpasswordconfirmsent = get_string('emailpasswordconfirmsent', '', $protectedemail);
-            notice($stremailpasswordconfirmsent, $CFG->wwwroot.'/index.php');
-            die; // Never reached.
-        } else {
-            // Confirm email sent. (Obfuscate email address to protect privacy).
-            $protectedemail = preg_replace('/([^@]*)@(.*)/', '******@$2', $user->email);
-            // This is a small usability problem - may be obfuscating the email address which the user has just supplied.
-            $stremailresetconfirmsent = get_string('emailresetconfirmsent', '', $protectedemail);
-            notice($stremailresetconfirmsent, $CFG->wwwroot.'/index.php');
-            die; // Never reached.
-        }
+        notice($notice, $url);
         die; // Never reached.
     }
 
@@ -169,6 +70,131 @@ function core_login_process_password_reset_request() {
     echo $OUTPUT->footer();
 }
 
+/**
+ * Process the password reset for the given user (via username or email).
+ *
+ * @param  string $username the user name
+ * @param  string $email    the user email
+ * @return array an array containing fields indicating the reset status, a info notice and redirect URL.
+ * @since  Moodle 3.4
+ */
+function core_login_process_password_reset($username, $email) {
+    global $CFG, $DB;
+
+    if (empty($username) && empty($email)) {
+        print_error('cannotmailconfirm');
+    }
+
+    // Next find the user account in the database which the requesting user claims to own.
+    if (!empty($username)) {
+        // Username has been specified - load the user record based on that.
+        $username = core_text::strtolower($username); // Mimic the login page process.
+        $userparams = array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'deleted' => 0, 'suspended' => 0);
+        $user = $DB->get_record('user', $userparams);
+    } else {
+        // Try to load the user record based on email address.
+        // this is tricky because
+        // 1/ the email is not guaranteed to be unique - TODO: send email with all usernames to select the account for pw reset
+        // 2/ mailbox may be case sensitive, the email domain is case insensitive - let's pretend it is all case-insensitive.
+
+        $select = $DB->sql_like('email', ':email', false, true, false, '|') .
+                " AND mnethostid = :mnethostid AND deleted=0 AND suspended=0";
+        $params = array('email' => $DB->sql_like_escape($email, '|'), 'mnethostid' => $CFG->mnet_localhost_id);
+        $user = $DB->get_record_select('user', $select, $params, '*', IGNORE_MULTIPLE);
+    }
+
+    // Target user details have now been identified, or we know that there is no such account.
+    // Send email address to account's email address if appropriate.
+    $pwresetstatus = PWRESET_STATUS_NOEMAILSENT;
+    if ($user and !empty($user->confirmed)) {
+        $systemcontext = context_system::instance();
+
+        $userauth = get_auth_plugin($user->auth);
+        if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)
+          or !has_capability('moodle/user:changeownpassword', $systemcontext, $user->id)) {
+            if (send_password_change_info($user)) {
+                $pwresetstatus = PWRESET_STATUS_OTHEREMAILSENT;
+            } else {
+                print_error('cannotmailconfirm');
+            }
+        } else {
+            // The account the requesting user claims to be is entitled to change their password.
+            // Next, check if they have an existing password reset in progress.
+            $resetinprogress = $DB->get_record('user_password_resets', array('userid' => $user->id));
+            if (empty($resetinprogress)) {
+                // Completely new reset request - common case.
+                $resetrecord = core_login_generate_password_reset($user);
+                $sendemail = true;
+            } else if ($resetinprogress->timerequested < (time() - $CFG->pwresettime)) {
+                // Preexisting, but expired request - delete old record & create new one.
+                // Uncommon case - expired requests are cleaned up by cron.
+                $DB->delete_records('user_password_resets', array('id' => $resetinprogress->id));
+                $resetrecord = core_login_generate_password_reset($user);
+                $sendemail = true;
+            } else if (empty($resetinprogress->timererequested)) {
+                // Preexisting, valid request. This is the first time user has re-requested the reset.
+                // Re-sending the same email once can actually help in certain circumstances
+                // eg by reducing the delay caused by greylisting.
+                $resetinprogress->timererequested = time();
+                $DB->update_record('user_password_resets', $resetinprogress);
+                $resetrecord = $resetinprogress;
+                $sendemail = true;
+            } else {
+                // Preexisting, valid request. User has already re-requested email.
+                $pwresetstatus = PWRESET_STATUS_ALREADYSENT;
+                $sendemail = false;
+            }
+
+            if ($sendemail) {
+                $sendresult = send_password_change_confirmation_email($user, $resetrecord);
+                if ($sendresult) {
+                    $pwresetstatus = PWRESET_STATUS_TOKENSENT;
+                } else {
+                    print_error('cannotmailconfirm');
+                }
+            }
+        }
+    }
+
+    $url = $CFG->wwwroot.'/index.php';
+    if (!empty($CFG->protectusernames)) {
+        // Neither confirm, nor deny existance of any username or email address in database.
+        // Print general (non-commital) message.
+        $status = 'emailpasswordconfirmmaybesent';
+        $notice = get_string($status);
+    } else if (empty($user)) {
+        // Protect usernames is off, and we couldn't find the user with details specified.
+        // Print failure advice.
+        $status = 'emailpasswordconfirmnotsent';
+        $notice = get_string($status);
+        $url = $CFG->wwwroot.'/forgot_password.php';
+    } else if (empty($user->email)) {
+        // User doesn't have an email set - can't send a password change confimation email.
+        $status = 'emailpasswordconfirmnoemail';
+        $notice = get_string($status);
+    } else if ($pwresetstatus == PWRESET_STATUS_ALREADYSENT) {
+        // User found, protectusernames is off, but user has already (re) requested a reset.
+        // Don't send a 3rd reset email.
+        $status = 'emailalreadysent';
+        $notice = get_string($status);
+    } else if ($pwresetstatus == PWRESET_STATUS_NOEMAILSENT) {
+        // User found, protectusernames is off, but user is not confirmed.
+        // Pretend we sent them an email.
+        // This is a big usability problem - need to tell users why we didn't send them an email.
+        // Obfuscate email address to protect privacy.
+        $protectedemail = preg_replace('/([^@]*)@(.*)/', '******@$2', $user->email);
+        $status = 'emailpasswordconfirmsent';
+        $notice = get_string($status, '', $protectedemail);
+    } else {
+        // Confirm email sent. (Obfuscate email address to protect privacy).
+        $protectedemail = preg_replace('/([^@]*)@(.*)/', '******@$2', $user->email);
+        // This is a small usability problem - may be obfuscating the email address which the user has just supplied.
+        $status = 'emailresetconfirmsent';
+        $notice = get_string($status, '', $protectedemail);
+    }
+    return array($status, $notice, $url);
+}
+
 /**
  * This function processes a user's submitted token to validate the request to set a new password.
  * If the user's token is validated, they are prompted to set a new password.
@@ -311,3 +337,52 @@ function core_login_get_return_url() {
     }
     return $urltogo;
 }
+
+/**
+ * Validates the forgot password form data.
+ *
+ * This is used by the forgot_password_form and by the core_auth_request_password_rest WS.
+ * @param  array $data array containing the data to be validated (email and username)
+ * @return array array of errors compatible with mform
+ * @since  Moodle 3.4
+ */
+function core_login_validate_forgot_password_data($data) {
+    global $CFG, $DB;
+
+    $errors = array();
+
+    if ((!empty($data['username']) and !empty($data['email'])) or (empty($data['username']) and empty($data['email']))) {
+        $errors['username'] = get_string('usernameoremail');
+        $errors['email']    = get_string('usernameoremail');
+
+    } else if (!empty($data['email'])) {
+        if (!validate_email($data['email'])) {
+            $errors['email'] = get_string('invalidemail');
+
+        } else if ($DB->count_records('user', array('email' => $data['email'])) > 1) {
+            $errors['email'] = get_string('forgottenduplicate');
+
+        } else {
+            if ($user = get_complete_user_data('email', $data['email'])) {
+                if (empty($user->confirmed)) {
+                    $errors['email'] = get_string('confirmednot');
+                }
+            }
+            if (!$user and empty($CFG->protectusernames)) {
+                $errors['email'] = get_string('emailnotfound');
+            }
+        }
+
+    } else {
+        if ($user = get_complete_user_data('username', $data['username'])) {
+            if (empty($user->confirmed)) {
+                $errors['email'] = get_string('confirmednot');
+            }
+        }
+        if (!$user and empty($CFG->protectusernames)) {
+            $errors['username'] = get_string('usernamenotfound');
+        }
+    }
+
+    return $errors;
+}
\ No newline at end of file
index dc6774d..03321e4 100644 (file)
@@ -28,12 +28,6 @@ require('../config.php');
 require_once($CFG->dirroot . '/user/editlib.php');
 require_once($CFG->libdir . '/authlib.php');
 
-// Try to prevent searching for sites that allow sign-up.
-if (!isset($CFG->additionalhtmlhead)) {
-    $CFG->additionalhtmlhead = '';
-}
-$CFG->additionalhtmlhead .= '<meta name="robots" content="noindex" />';
-
 if (!$authplugin = signup_is_enabled()) {
     print_error('notlocalisederrormessage', 'error', '', 'Sorry, you may not use this page.');
 }
index 07980d8..f7b9e21 100644 (file)
@@ -39,14 +39,14 @@ class login_signup_form extends moodleform implements renderable, templatable {
         $mform->addElement('header', 'createuserandpass', get_string('createuserandpass'), '');
 
 
-        $mform->addElement('text', 'username', get_string('username'), 'maxlength="100" size="12"');
+        $mform->addElement('text', 'username', get_string('username'), 'maxlength="100" size="12" autocapitalize="none"');
         $mform->setType('username', PARAM_RAW);
         $mform->addRule('username', get_string('missingusername'), 'required', null, 'client');
 
         if (!empty($CFG->passwordpolicy)){
             $mform->addElement('static', 'passwordpolicyinfo', '', print_password_policy());
         }
-        $mform->addElement('passwordunmask', 'password', get_string('password'), 'maxlength="32" size="12"');
+        $mform->addElement('password', 'password', get_string('password'), 'maxlength="32" size="12"');
         $mform->setType('password', core_user::get_property_type('password'));
         $mform->addRule('password', get_string('missingpassword'), 'required', null, 'client');
 
diff --git a/login/tests/lib_test.php b/login/tests/lib_test.php
new file mode 100644 (file)
index 0000000..5b55f40
--- /dev/null
@@ -0,0 +1,225 @@
+<?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 login lib.
+ *
+ * @package    core
+ * @copyright  2017 Juan Leyva
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/login/lib.php');
+
+/**
+ * Login lib testcase.
+ *
+ * @package    core
+ * @copyright  2017 Juan Leyva
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_login_lib_testcase extends advanced_testcase {
+
+    public function test_core_login_process_password_reset_one_time_without_username_protection() {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $CFG->protectusernames = 0;
+        $user = $this->getDataGenerator()->create_user(array('auth' => 'manual'));
+
+        $sink = $this->redirectEmails();
+
+        list($status, $notice, $url) = core_login_process_password_reset($user->username, null);
+        $this->assertSame('emailresetconfirmsent', $status);
+        $emails = $sink->get_messages();
+        $this->assertCount(1, $emails);
+        $email = reset($emails);
+        $this->assertSame($user->email, $email->to);
+        $this->assertNotEmpty($email->header);
+        $this->assertNotEmpty($email->body);
+        $this->assertRegExp('/A password reset was requested for your account/', $email->body);
+        $sink->clear();
+    }
+
+    public function test_core_login_process_password_reset_two_consecutive_times_without_username_protection() {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $CFG->protectusernames = 0;
+        $user = $this->getDataGenerator()->create_user(array('auth' => 'manual'));
+
+        $sink = $this->redirectEmails();
+
+        list($status, $notice, $url) = core_login_process_password_reset($user->username, null);
+        $this->assertSame('emailresetconfirmsent', $status);
+        // Request for a second time.
+        list($status, $notice, $url) = core_login_process_password_reset($user->username, null);
+        $this->assertSame('emailresetconfirmsent', $status);
+        $emails = $sink->get_messages();
+        $this->assertCount(2, $emails); // Two emails sent (one per each request).
+        $email = array_pop($emails);
+        $this->assertSame($user->email, $email->to);
+        $this->assertNotEmpty($email->header);
+        $this->assertNotEmpty($email->body);
+        $this->assertRegExp('/A password reset was requested for your account/', $email->body);
+        $sink->clear();
+    }
+
+    public function test_core_login_process_password_reset_three_consecutive_times_without_username_protection() {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $CFG->protectusernames = 0;
+        $user = $this->getDataGenerator()->create_user(array('auth' => 'manual'));
+
+        $sink = $this->redirectEmails();
+
+        list($status, $notice, $url) = core_login_process_password_reset($user->username, null);
+        $this->assertSame('emailresetconfirmsent', $status);
+        // Request for a second time.
+        list($status, $notice, $url) = core_login_process_password_reset($user->username, null);
+        $this->assertSame('emailresetconfirmsent', $status);
+        // Third time.
+        list($status, $notice, $url) = core_login_process_password_reset($user->username, null);
+        $this->assertSame('emailalreadysent', $status);
+        $emails = $sink->get_messages();
+        $this->assertCount(2, $emails); // Third time email is not sent.
+    }
+
+    public function test_core_login_process_password_reset_one_time_with_username_protection() {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $CFG->protectusernames = 1;
+        $user = $this->getDataGenerator()->create_user(array('auth' => 'manual'));
+
+        $sink = $this->redirectEmails();
+
+        list($status, $notice, $url) = core_login_process_password_reset($user->username, null);
+        $this->assertSame('emailpasswordconfirmmaybesent', $status);   // Generic message not giving clues.
+        $emails = $sink->get_messages();
+        $this->assertCount(1, $emails);
+        $email = reset($emails);
+        $this->assertSame($user->email, $email->to);
+        $this->assertNotEmpty($email->header);
+        $this->assertNotEmpty($email->body);
+        $this->assertRegExp('/A password reset was requested for your account/', $email->body);
+        $sink->clear();
+    }
+
+    public function test_core_login_process_password_reset_with_preexisting_expired_request_without_username_protection() {
+        global $CFG, $DB;
+
+        $this->resetAfterTest();
+        $CFG->protectusernames = 0;
+        $user = $this->getDataGenerator()->create_user(array('auth' => 'manual'));
+
+        $sink = $this->redirectEmails();
+
+        list($status, $notice, $url) = core_login_process_password_reset($user->username, null);
+        $this->assertSame('emailresetconfirmsent', $status);
+        // Request again.
+        list($status, $notice, $url) = core_login_process_password_reset($user->username, null);
+        $this->assertSame('emailresetconfirmsent', $status);
+
+        $resetrequests = $DB->get_records('user_password_resets');
+        $request = reset($resetrequests);
+        $request->timerequested = time() - YEARSECS;
+        $DB->update_record('user_password_resets', $request);
+
+        // Request again - third time - but it shuld be expired so we should get an email.
+        list($status, $notice, $url) = core_login_process_password_reset($user->username, null);
+        $this->assertSame('emailresetconfirmsent', $status);
+        $emails = $sink->get_messages();
+        $this->assertCount(3, $emails); // Normal process, the previous request was deleted.
+        $email = reset($emails);
+        $this->assertSame($user->email, $email->to);
+        $this->assertNotEmpty($email->header);
+        $this->assertNotEmpty($email->body);
+        $this->assertRegExp('/A password reset was requested for your account/', $email->body);
+        $sink->clear();
+    }
+
+    public function test_core_login_process_password_reset_disabled_auth() {
+        $this->resetAfterTest();
+        $user = $this->getDataGenerator()->create_user(array('auth' => 'oauth2'));
+
+        $sink = $this->redirectEmails();
+
+        core_login_process_password_reset($user->username, null);
+        $emails = $sink->get_messages();
+        $this->assertCount(1, $emails);
+        $email = reset($emails);
+        $this->assertSame($user->email, $email->to);
+        $this->assertNotEmpty($email->header);
+        $this->assertNotEmpty($email->body);
+        $this->assertRegExp('/Unfortunately your account on this site is disabled/', $email->body);
+        $sink->clear();
+    }
+
+    public function test_core_login_process_password_reset_auth_not_supporting_email_reset() {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $CFG->auth = $CFG->auth . ',mnet';
+        $user = $this->getDataGenerator()->create_user(array('auth' => 'mnet'));
+
+        $sink = $this->redirectEmails();
+
+        core_login_process_password_reset($user->username, null);
+        $emails = $sink->get_messages();
+        $this->assertCount(1, $emails);
+        $email = reset($emails);
+        $this->assertSame($user->email, $email->to);
+        $this->assertNotEmpty($email->header);
+        $this->assertNotEmpty($email->body);
+        $this->assertRegExp('/Unfortunately passwords cannot be reset on this site/', $email->body);
+        $sink->clear();
+    }
+
+    public function test_core_login_process_password_reset_missing_parameters() {
+        $this->expectException('moodle_exception');
+        $this->expectExceptionMessage(get_string('cannotmailconfirm', 'error'));
+        core_login_process_password_reset(null, null);
+    }
+
+    public function test_core_login_process_password_reset_invalid_username_with_username_protection() {
+        global $CFG;
+        $this->resetAfterTest();
+        $CFG->protectusernames = 1;
+        list($status, $notice, $url) = core_login_process_password_reset('72347234nasdfasdf/Ds', null);
+        $this->assertEquals('emailpasswordconfirmmaybesent', $status);
+    }
+
+    public function test_core_login_process_password_reset_invalid_username_without_username_protection() {
+        global $CFG;
+        $this->resetAfterTest();
+        $CFG->protectusernames = 0;
+        list($status, $notice, $url) = core_login_process_password_reset('72347234nasdfasdf/Ds', null);
+        $this->assertEquals('emailpasswordconfirmnotsent', $status);
+    }
+
+    public function test_core_login_process_password_reset_invalid_email_without_username_protection() {
+        global $CFG;
+        $this->resetAfterTest();
+        $CFG->protectusernames = 0;
+        list($status, $notice, $url) = core_login_process_password_reset(null, 'fakeemail@nofd.zdy');
+        $this->assertEquals('emailpasswordconfirmnotsent', $status);
+    }
+}
index 309cd8a..c501bf8 100644 (file)
@@ -24,7 +24,7 @@
 
 
 $string['acceptedfiletypes'] = 'Accepted file types';
-$string['acceptedfiletypes_help'] = 'Accepted file types can be restricted by entering a semicolon-separated list of mimetypes, for example \'video/mp4; audio/mp3; image/png; image/jpeg\'. You may also limit to extensions by including the dot, for example \'.png; .jpg\' If the field is left empty, then all file types are allowed.';
+$string['acceptedfiletypes_help'] = 'Accepted file types can be restricted by entering a comma-separated list of mimetypes, e.g. video/mp4, audio/mp3, image/png, image/jpeg, or file extensions including a dot, e.g. .png, .jpg. If the field is left empty, then all file types are allowed.';
 $string['configmaxbytes'] = 'Maximum file size';
 $string['countfiles'] = '{$a} files';
 $string['default'] = 'Enabled by default';
index f4edefa..634778e 100644 (file)
@@ -104,14 +104,17 @@ class data_field_latlong extends data_field_base {
         foreach ($latlongsrs as $latlong) {
             $latitude = format_float($latlong->la, 4);
             $longitude = format_float($latlong->lo, 4);
-            $options[$latlong->la . ',' . $latlong->lo] = $latitude . ' ' . $longitude;
+            if ($latitude && $longitude) {
+                $options[$latlong->la . ',' . $latlong->lo] = $latitude . ' ' . $longitude;
+            }
         }
         $latlongsrs->close();
 
         $classes = array('class' => 'accesshide');
         $return = html_writer::label(get_string('latlong', 'data'), 'menuf_'.$this->field->id, false, $classes);
         $classes = array('class' => 'custom-select');
-        $return .= html_writer::select($options, 'f_'.$this->field->id, $value, null, $classes);
+        $return .= html_writer::select($options, 'f_'.$this->field->id, $value, array('' => get_string('menuchoose', 'data')),
+            $classes);
        return $return;
     }
 
index d0b2ff6..72320c9 100644 (file)
@@ -55,7 +55,7 @@ $string['confirmdeleteentry'] = 'Are you sure you want to delete this entry?';
 $string['confirmdeleteitem'] = 'Are you sure you want to delete this element?';
 $string['confirmdeletetemplate'] = 'Are you sure you want to delete this template?';
 $string['confirmusetemplate'] = 'Are you sure you want to use this template?';
-$string['continue_the_form'] = 'Continue the form';
+$string['continue_the_form'] = 'Continue answering the questions...';
 $string['count_of_nums'] = 'Count of numbers';
 $string['courseid'] = 'courseid';
 $string['creating_templates'] = 'Save these questions as a new template';
index 5696c4a..5922e55 100644 (file)
@@ -26,6 +26,7 @@
 require(__DIR__.'/../../config.php');
 require_once($CFG->dirroot.'/mod/forum/lib.php');
 require_once($CFG->dirroot.'/rating/lib.php');
+require_once($CFG->dirroot.'/user/lib.php');
 
 $courseid  = optional_param('course', null, PARAM_INT); // Limit the posts to just this course
 $userid = optional_param('id', $USER->id, PARAM_INT);        // User id whose posts we want to view
@@ -134,29 +135,8 @@ if (empty($result->posts)) {
     // In either case we need to decide whether we can show personal information
     // about the requested user to the current user so we will execute some checks
 
-    // First check the obvious, its the current user, a specific course has been
-    // provided (require_login has been called), or they have a course contact role.
-    // True to any of those and the current user can see the details of the
-    // requested user.
-    $canviewuser = ($iscurrentuser || $isspecificcourse || empty($CFG->forceloginforprofiles) || has_coursecontact_role($userid));
-    // Next we'll check the caps, if the current user has the view details and a
-    // specific course has been requested, or if they have the view all details
-    $canviewuser = ($canviewuser || ($isspecificcourse && has_capability('moodle/user:viewdetails', $coursecontext) || has_capability('moodle/user:viewalldetails', $usercontext)));
-
-    // If none of the above was true the next step is to check a shared relation
-    // through some course
-    if (!$canviewuser) {
-        // Get all of the courses that the users have in common
-        $sharedcourses = enrol_get_shared_courses($USER->id, $user->id, true);
-        foreach ($sharedcourses as $sharedcourse) {
-            // Check the view cap within the course context
-            if (has_capability('moodle/user:viewdetails', context_course::instance($sharedcourse->id))) {
-                $canviewuser = true;
-                break;
-            }
-        }
-        unset($sharedcourses);
-    }
+    // TODO - Remove extra cap check once MDL-59172 is resolved.
+    $canviewuser = user_can_view_profile($user, null, $usercontext) || has_capability('moodle/user:viewalldetails', $usercontext);
 
     // Prepare the page title
     $pagetitle = get_string('noposts', 'mod_forum');
@@ -237,8 +217,10 @@ if (empty($result->posts)) {
     $PAGE->set_title($pagetitle);
     if ($isspecificcourse) {
         $PAGE->set_heading($pageheading);
-    } else {
+    } else if ($canviewuser) {
         $PAGE->set_heading(fullname($user));
+    } else {
+        $PAGE->set_heading($SITE->fullname);
     }
     echo $OUTPUT->header();
     if (!$isspecificcourse) {
index 7289885..dd47a65 100644 (file)
@@ -201,7 +201,7 @@ $string['glossary:manageentries'] = 'Manage entries';
 $string['glossary:rate'] = 'Rate entries';
 $string['glossary:view'] = 'View entries';
 $string['glossarytype'] = 'Glossary type';
-$string['glossarytype_help'] = 'A main glossary is a glossary in which entries from secondary glossaries can be imported. There can only be one main glossary in a course. if glossary entry import is not required, all glossaries in the course can be secondary glossaries.';
+$string['glossarytype_help'] = 'A main glossary is a glossary in which entries from secondary glossaries can be imported. There can only be one main glossary in a course. If glossary entry import is not required, all glossaries in the course can be secondary glossaries.';
 $string['glossary:view'] = 'View glossary';
 $string['glossary:viewallratings'] = 'View all raw ratings given by individuals';
 $string['glossary:viewanyrating'] = 'View total ratings that anyone received';
index a331db6..bceb426 100644 (file)
@@ -42,16 +42,23 @@ $string['addaquestionpage'] = 'Add a question page';
 $string['addaquestionpagehere'] = 'Add a question page here';
 $string['addbranchtable'] = 'Add a content page';
 $string['addcluster'] = 'Add a cluster';
+$string['addessay'] = 'Create an Essay question page';
 $string['addedabranchtable'] = 'Added a content page';
 $string['addedanendofbranch'] = 'Added an end of branch';
 $string['addedaquestionpage'] = 'Added a question page';
 $string['addedcluster'] = 'Added a cluster';
 $string['addedendofcluster'] = 'Added an end of cluster';
+$string['addendofbranch'] = 'Add end of branch';
 $string['addendofcluster'] = 'Add an end of cluster';
+$string['addmatching'] = 'Create a Matching question page';
+$string['addmultichoice'] = 'Create a Multichoice question page';
 $string['addnewgroupoverride'] = 'Add group override';
 $string['addnewuseroverride'] = 'Add user override';
+$string['addnumerical'] = 'Create a Numerical question page';
 $string['additionalattemptsremaining'] = 'Completed, You can re-attempt this lesson';
 $string['addpage'] = 'Add a page';
+$string['addshortanswer'] = 'Create a Short answer question page';
+$string['addtruefalse'] = 'Create a True/false question page';
 $string['allowofflineattempts'] = 'Allow lesson to be attempted offline using the mobile app';
 $string['allowofflineattempts_help'] = 'If enabled, a mobile app user can download the lesson and attempt it offline.
 All the possible answers and correct responses will be downloaded as well.
@@ -162,11 +169,22 @@ $string['displayscorewithessays'] = '<p>You earned {$a->score} out of {$a->tempm
 $string['displayscorewithoutessays'] = 'Your score is {$a->score} (out of {$a->grade}).';
 $string['duplicatepagenamed'] = 'Duplicate page: {$a}';
 $string['edit'] = 'Edit';
+$string['editbranchtable'] = 'Editing a content page';
+$string['editcluster'] = 'Editing a cluster';
+$string['editendofcluster'] = 'Editing an end of cluster page';
+$string['editendofbranch'] = 'Editing an end of branch page';
+$string['editessay'] = 'Editing an Essay question page';
 $string['editingquestionpage'] = 'Editing {$a} question page';
 $string['editlessonsettings'] = 'Edit lesson settings';
+$string['editmatching'] = 'Editing a Matching question page';
+$string['editmultichoice'] = 'Editing a Multichoice question page';
+$string['editnumerical'] = 'Editing a Numerical question page';
 $string['editoverride'] = 'Edit override';
 $string['editpage'] = 'Edit page contents';
 $string['editpagecontent'] = 'Edit page contents';
+$string['editquestion'] = 'Editing a question page';
+$string['editshortanswer'] = 'Editing a Short answer question page';
+$string['edittruefalse'] = 'Editing a True/false question page';
 $string['email'] = 'Email';
 $string['emailallgradedessays'] = 'Email ALL graded essays';
 $string['emailgradedessays'] = 'Email graded essays';
index e0b143b..a62b189 100644 (file)
@@ -1247,7 +1247,13 @@ abstract class lesson_add_page_form_base extends moodleform {
         $mform = $this->_form;
         $editoroptions = $this->_customdata['editoroptions'];
 
-        $mform->addElement('header', 'qtypeheading', get_string('createaquestionpage', 'lesson', get_string($this->qtypestring, 'lesson')));
+        if ($this->qtypestring != 'selectaqtype') {
+            if ($this->_customdata['edit']) {
+                $mform->addElement('header', 'qtypeheading', get_string('edit'. $this->qtypestring, 'lesson'));
+            } else {
+                $mform->addElement('header', 'qtypeheading', get_string('add'. $this->qtypestring, 'lesson'));
+            }
+        }
 
         if (!empty($this->_customdata['returnto'])) {
             $mform->addElement('hidden', 'returnto', $this->_customdata['returnto']);
@@ -1576,7 +1582,9 @@ class lesson extends lesson_base {
         $DB->delete_records("lesson_timer", array("lessonid"=>$this->properties->id));
         $DB->delete_records("lesson_branch", array("lessonid"=>$this->properties->id));
         if ($events = $DB->get_records('event', array("modulename"=>'lesson', "instance"=>$this->properties->id))) {
+            $coursecontext = context_course::instance($cm->course);
             foreach($events as $event) {
+                $event->context = $coursecontext;
                 $event = calendar_event::load($event);
                 $event->delete();
             }
index c7e4342..23f9c58 100644 (file)
@@ -324,7 +324,11 @@ class lesson_add_page_form_branchtable extends lesson_add_page_form_base {
 
         $jumptooptions = lesson_page_type_branchtable::get_jumptooptions($firstpage, $lesson);
 
-        $mform->setDefault('qtypeheading', get_string('addabranchtable', 'lesson'));
+        if ($this->_customdata['edit']) {
+            $mform->setDefault('qtypeheading', get_string('editbranchtable', 'lesson'));
+        } else {
+            $mform->setDefault('qtypeheading', get_string('addabranchtable', 'lesson'));
+        }
 
         $mform->addElement('hidden', 'firstpage');
         $mform->setType('firstpage', PARAM_BOOL);
index d74eda3..97bb040 100644 (file)
@@ -23,6 +23,8 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or late
  **/
 
+define('NO_OUTPUT_BUFFERING', true);
+
 require_once(__DIR__ . '/../../config.php');
 require_once($CFG->dirroot.'/mod/lesson/locallib.php');
 require_once($CFG->libdir . '/grade/constants.php');
index d6f05f7..5ddc63c 100644 (file)
@@ -22,6 +22,7 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+define('NO_OUTPUT_BUFFERING', true);
 
 require_once(__DIR__ . '/../../config.php');
 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
index b7bea72..790be61 100644 (file)
@@ -256,7 +256,7 @@ class quiz_statistics_report extends quiz_default_report {
             if ($quizstats->s()) {
                 $this->output_quiz_structure_analysis_table($questionstats);
             }
-            $this->table->finish_output();
+            $this->table->export_class_instance()->finish_document();
 
         } else {
             // On-screen display of overview report.
index 0dd1f1f..7629859 100644 (file)
@@ -93,8 +93,11 @@ function resource_display_embed($resource, $cm, $course, $file) {
         $code = $mediamanager->embed_url($moodleurl, $title, 0, 0, $embedoptions);
 
     } else {
+        // We need a way to discover if we are loading remote docs inside an iframe.
+        $moodleurl->param('embed', 1);
+
         // anything else - just try object tag enlarged as much as possible
-        $code = resourcelib_embed_general($fullurl, $title, $clicktoopen, $mimetype);
+        $code = resourcelib_embed_general($moodleurl, $title, $clicktoopen, $mimetype);
     }
 
     resource_print_header($resource, $cm, $course);
@@ -525,7 +528,11 @@ function resource_set_mainfile($data) {
 
     $context = context_module::instance($cmid);
     if ($draftitemid) {
-        file_save_draft_area_files($draftitemid, $context->id, 'mod_resource', 'content', 0, array('subdirs'=>true));
+        $options = array('subdirs' => true, 'embed' => false);
+        if ($data->display == RESOURCELIB_DISPLAY_EMBED) {
+            $options['embed'] = true;
+        }
+        file_save_draft_area_files($draftitemid, $context->id, 'mod_resource', 'content', 0, $options);
     }
     $files = $fs->get_area_files($context->id, 'mod_resource', 'content', 0, 'sortorder', false);
     if (count($files) == 1) {
index 673de72..1a4c200 100644 (file)
@@ -282,6 +282,8 @@ $string['submissiongrade'] = 'Grade for submission';
 $string['submissiongrade_help'] = 'This setting specifies the maximum grade that may be obtained for submitted work.';
 $string['submissiongradeof'] = 'Grade for submission (of {$a})';
 $string['submissionlastmodified'] = 'Last modified';
+$string['submissionrequiredcontent'] = 'You need to enter some text or add a file.';
+$string['submissionrequiredfile'] = 'You need to add a file or enter some text.';
 $string['submissionsettings'] = 'Submission settings';
 $string['submissionstart'] = 'Open for submissions from';
 $string['submissionstartevent'] = '{$a} (opens for submissions)';
index 398cd4a..f6c8270 100644 (file)
@@ -88,6 +88,12 @@ class workshop_submission_form extends moodleform {
             }
         }
 
+        $getfiles = file_get_drafarea_files($data['attachment_filemanager']);
+        if (empty($getfiles->list) and html_is_blank($data['content_editor']['text'])) {
+            $errors['content_editor'] = get_string('submissionrequiredcontent', 'mod_workshop');
+            $errors['attachment_filemanager'] = get_string('submissionrequiredfile', 'mod_workshop');
+        }
+
         if (isset($data['attachment_filemanager']) and isset($this->_customdata['workshop']->submissionfiletypes)) {
             $whitelist = workshop::normalize_file_extensions($this->_customdata['workshop']->submissionfiletypes);
             if ($whitelist) {
index 7d80148..bdc119e 100644 (file)
         <testsuite name="core_block_testsuite">
             <directory suffix="_test.php">blocks/tests</directory>
         </testsuite>
+        <testsuite name="core_login_testsuite">
+            <directory suffix="_test.php">login/tests</directory>
+        </testsuite>
 
         <!--Plugin suites: use admin/tool/phpunit/cli/util.php to build phpunit.xml from phpunit.xml.dist with up-to-date list of plugins in current install-->
 <!--@plugin_suites_start@-->
index 9148b62..3d6d542 100644 (file)
@@ -36,5 +36,5 @@ $preview = optional_param('preview', null, PARAM_ALPHANUM);
 // Offline means download the file from the repository and serve it, even if it was an external link.
 // The repository may have to export the file to an offline format.
 $offline = optional_param('offline', 0, PARAM_BOOL);
-
-file_pluginfile($relativepath, $forcedownload, $preview, $offline);
+$embed = optional_param('embed', 0, PARAM_BOOL);
+file_pluginfile($relativepath, $forcedownload, $preview, $offline, $embed);
index 18aacbd..63a09d3 100644 (file)
@@ -24,7 +24,7 @@
  */
 
 $string['answerhowmany'] = 'One or multiple answers?';
-$string['answerhowmany_desc'] = 'Should the default for new multichoice questions be to require single or multiple answers?';
+$string['answerhowmany_desc'] = 'Whether the default should be one response (i.e. radio buttons) or multiple responses (i.e. checkboxes).';
 $string['answernumbering'] = 'Number the choices?';
 $string['answernumbering123'] = '1., 2., 3., ...';
 $string['answernumberingabc'] = 'a., b., c., ...';
@@ -32,7 +32,7 @@ $string['answernumberingABCD'] = 'A., B., C., ...';
 $string['answernumberingiii'] = 'i., ii., iii., ...';
 $string['answernumberingIIII'] = 'I., II., III., ...';
 $string['answernumberingnone'] = 'No numbering';
-$string['answernumbering_desc'] = 'Set the default numbering style for new multichoice questions.';
+$string['answernumbering_desc'] = 'The default numbering style.';
 $string['answersingleno'] = 'Multiple answers allowed';
 $string['answersingleyes'] = 'One answer only';
 $string['choiceno'] = 'Choice {$a}';
@@ -67,7 +67,7 @@ $string['pluginnamesummary'] = 'Allows the selection of a single or multiple res
 $string['selectmulti'] = 'Select one or more:';
 $string['selectone'] = 'Select one:';
 $string['shuffleanswers'] = 'Shuffle the choices?';
-$string['shuffleanswers_desc'] = 'Should the default for new nultichoice questions be to shuffle answers?';
+$string['shuffleanswers_desc'] = 'Whether options should be randomly shuffled for each attempt by default.';
 $string['shuffleanswers_help'] = 'If enabled, the order of the answers is randomly shuffled for each attempt, provided that "Shuffle within questions" in the activity settings is also enabled.';
 $string['singleanswer'] = 'Choose one answer.';
 $string['toomanyselected'] = 'You have selected too many options.';
index 3c571e1..6309bbb 100644 (file)
@@ -17,7 +17,6 @@ Feature: In a report, admin can filter log data
       | student1 | C1 | student |
     And I log in as "admin"
 
-  @javascript
   Scenario: Filter log report for standard and legacy log reader
     Given I navigate to "Manage log stores" node in "Site administration > Plugins > Logging"
     And I click on "Enable" "link" in the "Legacy log" "table_row"
@@ -42,7 +41,6 @@ Feature: In a report, admin can filter log data
     And I should see "user login"
     And I should not see "Nothing to display"
 
-  @javascript
   Scenario: Filter log report for standard log reader
     Given I am on "Course 1" course homepage
     And I navigate to course participants
@@ -57,7 +55,6 @@ Feature: In a report, admin can filter log data
     And I press "Get these logs"
     Then I should see "User logged in as another user"
 
-  @javascript
   Scenario: Filter log report for legacy log reader
     Given I navigate to "Manage log stores" node in "Site administration > Plugins > Logging"
     And I click on "Enable" "link" in the "Legacy log" "table_row"
index 356468d..e7afee2 100644 (file)
@@ -33,7 +33,6 @@ Feature: User can view activity log.
     And I press "Save changes"
     And I log out
 
-  @javascript
   Scenario: View Todays' and all log report for user
     Given I log in as "teacher1"
     And I am on "Course 1" course homepage
@@ -45,7 +44,6 @@ Feature: User can view activity log.
     And I follow "All logs"
     Then I should see "Assignment: Test assignment name"
 
-  @javascript
   Scenario: No log reader enabled should be visible when no log store enabled.
     Given I log in as "admin"
     And I navigate to "Manage log stores" node in "Site administration > Plugins > Logging"
index 0c340e8..401c71d 100644 (file)
@@ -131,6 +131,33 @@ class repository_googledocs extends repository {
         }
     }
 
+    /**
+     * Print the login in a popup.
+     *
+     * @param array|null $attr Custom attributes to be applied to popup div.
+     */
+    public function print_login_popup($attr = null) {
+        global $OUTPUT, $PAGE;
+
+        $client = $this->get_user_oauth_client(false);
+        $url = new moodle_url($client->get_login_url());
+        $state = $url->get_param('state') . '&reloadparent=true';
+        $url->param('state', $state);
+
+        $PAGE->set_pagelayout('embedded');
+        echo $OUTPUT->header();
+
+        $repositoryname = get_string('pluginname', 'repository_googledocs');
+
+        $button = new single_button($url, get_string('logintoaccount', 'repository', $repositoryname), 'post', true);
+        $button->add_action(new popup_action('click', $url, 'Login'));
+        $button->class = 'mdl-align';
+        $button = $OUTPUT->render($button);
+        echo html_writer::div($button, '', $attr);
+
+        echo $OUTPUT->footer();
+    }
+
     /**
      * Build the breadcrumb from a path.
      *
@@ -613,8 +640,15 @@ class repository_googledocs extends repository {
                                                    $storedfile->get_filename(),
                                                    $forcedownload);
             $url->param('sesskey', sesskey());
-            $userauth = $this->get_user_oauth_client($url);
+            $param = ($options['embed'] == true) ? false : $url;
+            $userauth = $this->get_user_oauth_client($param);
             if (!$userauth->is_logged_in()) {
+                if ($options['embed'] == true) {
+                    // Due to Same-origin policy, we cannot redirect to googledocs login page.
+                    // If the requested file is embed and the user is not logged in, add option to log in using a popup.
+                    $this->print_login_popup(['style' => 'margin-top: 250px']);
+                    exit;
+                }
                 redirect($userauth->get_login_url());
             }
             if ($userauth === false) {
index 4a341fa..796bdc0 100644 (file)
@@ -127,6 +127,33 @@ class repository_onedrive extends repository {
         }
     }
 
+    /**
+     * Print the login in a popup.
+     *
+     * @param array|null $attr Custom attributes to be applied to popup div.
+     */
+    public function print_login_popup($attr = null) {
+        global $OUTPUT, $PAGE;
+
+        $client = $this->get_user_oauth_client(false);
+        $url = new moodle_url($client->get_login_url());
+        $state = $url->get_param('state') . '&reloadparent=true';
+        $url->param('state', $state);
+
+        $PAGE->set_pagelayout('embedded');
+        echo $OUTPUT->header();
+
+        $repositoryname = get_string('pluginname', 'repository_onedrive');
+
+        $button = new single_button($url, get_string('logintoaccount', 'repository', $repositoryname), 'post', true);
+        $button->add_action(new popup_action('click', $url, 'Login'));
+        $button->class = 'mdl-align';
+        $button = $OUTPUT->render($button);
+        echo html_writer::div($button, '', $attr);
+
+        echo $OUTPUT->footer();
+    }
+
     /**
      * Build the breadcrumb from a path.
      *
@@ -564,8 +591,16 @@ class repository_onedrive extends repository {
                                                    $storedfile->get_filename(),
                                                    $forcedownload);
             $url->param('sesskey', sesskey());
-            $userauth = $this->get_user_oauth_client($url);
+            $param = ($options['embed'] == true) ? false : $url;
+            $userauth = $this->get_user_oauth_client($param);
+
             if (!$userauth->is_logged_in()) {
+                if ($options['embed'] == true) {
+                    // Due to Same-origin policy, we cannot redirect to onedrive login page.
+                    // If the requested file is embed and the user is not logged in, add option to log in using a popup.
+                    $this->print_login_popup(['style' => 'margin-top: 250px']);
+                    exit;
+                }
                 redirect($userauth->get_login_url());
             }
             if ($userauth === false) {
@@ -703,8 +738,10 @@ class repository_onedrive extends repository {
      * @return boolean
      */
     protected function set_file_sharing_anyone_with_link_can_read(\repository_onedrive\rest $client, $fileid) {
+
+        $type = (isset($this->options['embed']) && $this->options['embed'] == true) ? 'embed' : 'view';
         $updateread = [
-            'type' => 'view',
+            'type' => $type,
             'scope' => 'anonymous'
         ];
         $params = ['fileid' => $fileid];
index 83fb5e3..19cb9fa 100644 (file)
@@ -69,6 +69,23 @@ $repo->callback();
 // manually.
 $strhttpsbug = json_encode(get_string('cannotaccessparentwin', 'repository'));
 $strrefreshnonjs = get_string('refreshnonjsfilepicker', 'repository');
+$reloadparent = optional_param('reloadparent', false, PARAM_BOOL);
+// If this request is coming from a popup, close window and reload parent window.
+if ($reloadparent == true) {
+    $js = <<<EOD
+<html>
+<head>
+    <script type="text/javascript">
+        window.opener.location.reload();
+        window.close();
+    </script>
+</head>
+<body></body>
+</html>
+EOD;
+    die($js);
+}
+
 $js =<<<EOD
 <html>
 <head>
index 2dd55d2..45c0ffa 100644 (file)
@@ -303,7 +303,7 @@ class core_tag_collection {
     public static function change_sortorder($tagcoll, $direction) {
         global $DB;
         if ($direction != -1 && $direction != 1) {
-            throw coding_exception('Second argument in tag_coll_change_sortorder() can be only 1 or -1');
+            throw new coding_exception('Second argument in tag_coll_change_sortorder() can be only 1 or -1');
         }
         $tagcolls = self::get_collections();
         $keys = array_keys($tagcolls);
index 57e81c4..a628ff8 100644 (file)
@@ -29,6 +29,7 @@ require_once(__DIR__ . '/lib.php');
 $THEME->name = 'boost';
 $THEME->sheets = [];
 $THEME->editor_sheets = [];
+$THEME->usefallback = true;
 $THEME->scss = function($theme) {
     return theme_boost_get_main_scss_content($theme);
 };
index cb149d1..a48811d 100644 (file)
@@ -236,8 +236,6 @@ img.resize {
 }
 
 .action-menu .userpicture {
-    width: auto;
-    height: auto;
     margin-left: 1rem;
 }
 
index 6a60df6..2ee0b34 100644 (file)
@@ -505,24 +505,14 @@ a.skip:active {
     margin-bottom: 5px;
     margin-top: 15px;
 }
-.loginbox .signuppanel .subcontent {
-    text-align: left;
-}
 .loginbox .loginsub {
     margin-left: 0;
     margin-right: 0;
 }
 .loginbox .guestsub,
-.loginbox .forgotsub,
-.loginbox .potentialidps {
+.loginbox .forgotsub {
     margin: 5px 12%;
 }
-.loginbox .potentialidps .potentialidplist {
-    margin-left: 40%;
-}
-.loginbox .potentialidps .potentialidplist div {
-    text-align: left;
-}
 .loginbox .loginform {
     margin-top: 1em;
     text-align: left;
@@ -558,7 +548,9 @@ a.skip:active {
     margin-bottom: -2000px;
     padding-bottom: 2000px;
 }
-
+.loginbox .potentialidp {
+    margin-bottom: 0.3em;
+}
 .loginbox .potentialidp .smallicon {
     vertical-align: text-bottom;
     margin: 0 .3em;
index 147abba..88cc4a8 100644 (file)
@@ -2796,24 +2796,14 @@ a.skip:active {
   margin-bottom: 5px;
   margin-top: 15px;
 }
-.loginbox .signuppanel .subcontent {
-  text-align: left;
-}
 .loginbox .loginsub {
   margin-left: 0;
   margin-right: 0;
 }
 .loginbox .guestsub,
-.loginbox .forgotsub,
-.loginbox .potentialidps {
+.loginbox .forgotsub {
   margin: 5px 12%;
 }
-.loginbox .potentialidps .potentialidplist {
-  margin-left: 40%;
-}
-.loginbox .potentialidps .potentialidplist div {
-  text-align: left;
-}
 .loginbox .loginform {
   margin-top: 1em;
   text-align: left;
@@ -2851,6 +2841,9 @@ a.skip:active {
   margin-bottom: -2000px;
   padding-bottom: 2000px;
 }
+.loginbox .potentialidp {
+  margin-bottom: 0.3em;
+}
 .loginbox .potentialidp .smallicon {
   vertical-align: text-bottom;
   margin: 0 .3em;
index 82c9073..09991ee 100644 (file)
@@ -28,6 +28,7 @@ $THEME->parents = array('clean', 'bootstrapbase');
 $THEME->doctype = 'html5';
 $THEME->sheets = array('custom');
 $THEME->lessfile = 'moodle';
+$THEME->usefallback = true;
 $THEME->parents_exclude_sheets = array('bootstrapbase' => array('moodle'), 'clean' => array('custom'));
 $THEME->lessvariablescallback = 'theme_more_less_variables';
 $THEME->extralesscallback = 'theme_more_extra_less';
index cc2714f..4977095 100644 (file)
@@ -52,17 +52,32 @@ if ($slashargument = min_get_slash_argument()) {
 
     list($themename, $rev, $type) = explode('/', $slashargument, 3);
     $themename = min_clean_param($themename, 'SAFEDIR');
-    $rev       = min_clean_param($rev, 'INT');
+    $rev       = min_clean_param($rev, 'RAW');
     $type      = min_clean_param($type, 'SAFEDIR');
 
 } else {
     $themename = min_optional_param('theme', 'standard', 'SAFEDIR');
-    $rev       = min_optional_param('rev', 0, 'INT');
+    $rev       = min_optional_param('rev', 0, 'RAW');
     $type      = min_optional_param('type', 'all', 'SAFEDIR');
     $chunk     = min_optional_param('chunk', null, 'INT');
     $usesvg    = (bool)min_optional_param('svg', '1', 'INT');
 }
 
+// Check if we received a theme sub revision which allows us
+// to handle local caching on a per theme basis.
+$values = explode('_', $rev);
+$rev = min_clean_param(array_shift($values), 'INT');
+$themesubrev = array_shift($values);
+
+if (is_null($themesubrev)) {
+    // Default to the current theme subrevision if one isn't
+    // provided in the URL.
+    $themesubrev = theme_get_sub_revision_for_theme($themename);
+} else {
+    $themesubrev = min_clean_param($themesubrev, 'INT');
+}
+
+// Check that type fits into the expected values.
 if ($type === 'editor') {
     // The editor CSS is never chunked.
     $chunk = null;
@@ -82,28 +97,17 @@ if (file_exists("$CFG->dirroot/theme/$themename/config.php")) {
 }
 
 $candidatedir = "$CFG->localcachedir/theme/$rev/$themename/css";
-$etag = "$rev/$themename/$type";
-$candidatename = $type;
-if (!$usesvg) {
-    // Add to the sheet name, one day we'll be able to just drop this.
-    $candidatedir .= '/nosvg';
-    $etag .= '/nosvg';
-}
+$candidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $themesubrev, $usesvg);
+$chunkedcandidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $themesubrev, $usesvg, $chunk);
+$etag = theme_styles_get_etag($themename, $rev, $type, $themesubrev, $usesvg, $chunk);
 
-if ($chunk !== null) {
-    $etag .= '/chunk'.$chunk;
-    $candidatename .= '.'.$chunk;
-}
-$candidatesheet = "$candidatedir/$candidatename.css";
-$etag = sha1($etag);
-
-if (file_exists($candidatesheet)) {
+if (file_exists($chunkedcandidatesheet)) {
     if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
         // We do not actually need to verify the etag value because our files
         // never change in cache because we increment the rev counter.
-        css_send_unmodified(filemtime($candidatesheet), $etag);
+        css_send_unmodified(filemtime($chunkedcandidatesheet), $etag);
     }
-    css_send_cached_css($candidatesheet, $etag);
+    css_send_cached_css($chunkedcandidatesheet, $etag);
 }
 
 // Ok, now we need to start normal moodle script, we need to load all libs and $DB.
@@ -119,95 +123,214 @@ $theme->force_svg_use($usesvg);
 $theme->set_rtl_mode($type === 'all-rtl' ? true : false);
 
 $themerev = theme_get_revision();
+$currentthemesubrev = theme_get_sub_revision_for_theme($themename);
 
 $cache = true;
-if ($themerev <= 0 or $themerev != $rev) {
+// If the client is requesting a revision that doesn't match both
+// the global theme revision and the theme specific revision then
+// tell the browser not to cache this style sheet because it's
+// likely being regenerated.
+if ($themerev <= 0 or $themerev != $rev or $themesubrev != $currentthemesubrev) {
     $rev = $themerev;
     $cache = false;
 
     $candidatedir = "$CFG->localcachedir/theme/$rev/$themename/css";
-    $etag = "$rev/$themename/$type";
-    $candidatename = $type;
-    if (!$usesvg) {
-        // Add to the sheet name, one day we'll be able to just drop this.
-        $candidatedir .= '/nosvg';
-        $etag .= '/nosvg';
-    }
-
-    if ($chunk !== null) {
-        $etag .= '/chunk'.$chunk;
-        $candidatename .= '.'.$chunk;
-    }
-    $candidatesheet = "$candidatedir/$candidatename.css";
-    $etag = sha1($etag);
+    $candidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $themesubrev, $usesvg);
+    $chunkedcandidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $themesubrev, $usesvg, $chunk);
+    $etag = theme_styles_get_etag($themename, $rev, $type, $themesubrev, $usesvg, $chunk);
 }
 
 make_localcache_directory('theme', false);
 
 if ($type === 'editor') {
     $csscontent = $theme->get_css_content_editor();
-    css_store_css($theme, "$candidatedir/editor.css", $csscontent, false);
+    css_store_css($theme, $candidatesheet, $csscontent, false);
+
+    if ($cache) {
+        css_send_cached_css($candidatesheet, $etag);
+    } else {
+        css_send_uncached_css(file_get_contents($candidatesheet));
+    }
+
+}
+
+if (($fallbacksheet = theme_styles_fallback_content($theme)) && !$theme->has_css_cached_content()) {
+    // The theme is not yet available and a fallback is available.
+    // Return the fallback immediately, specifying the Content-Length, then generate in the background.
+    $css = file_get_contents($fallbacksheet);
+    css_send_temporary_css($css);
+
+    // The fallback content has now been sent.
+    // There will be an attempt to generate the content, but it should not be served.
+    // The Content-Length above means that the client will disregard it anyway.
+    $sendaftergeneration = false;
 
+    // There may be another client currently holding a lock and generating the stylesheet.
+    // Use a very low lock timeout as the connection will be ended immediately afterwards.
+    $locktimeout = 1;
 } else {
-    // Fetch a lock whilst the CSS is fetched as this can be slow and CPU intensive.
-    // Each client should wait for one to finish the compilation before starting the compiler.
-    $lockfactory = \core\lock\lock_config::get_lock_factory('core_theme_get_css_content');
-    $lock = $lockfactory->get_lock($themename, rand(90, 120));
-
-    if (file_exists($candidatesheet)) {
-        // The file was built while we waited for the lock, we release the lock and serve the file.
-        if ($lock) {
-            $lock->release();
-        }
+    // There is no fallback content to be issued here, therefore the generated content must be output.
+    $sendaftergeneration = true;
 
-        if ($cache) {
-            css_send_cached_css($candidatesheet, $etag);
-        } else {
+    // Use a realistic lock timeout as the intention is to avoid lock contention.
+    $locktimeout = rand(90, 120);
+}
+
+// Attempt to fetch the lock.
+$lockfactory = \core\lock\lock_config::get_lock_factory('core_theme_get_css_content');
+$lock = $lockfactory->get_lock($themename, $locktimeout);
+
+if ($sendaftergeneration || $lock) {
+    // Either the lock was successful, or the lock was unsuccessful but the content *must* be sent.
+    if (!file_exists($chunkedcandidatesheet)) {
+        // The content does not exist locally.
+        // Generate and save it.
+        $candidatesheet = theme_styles_generate_and_store($theme, $rev, $themesubrev, $candidatedir);
+    }
+
+    if ($lock) {
+        $lock->release();
+    }
+
+    if ($sendaftergeneration) {
+        if (!$cache) {
+            // Do not pollute browser caches if invalid revision requested,
+            // let's ignore legacy IE breakage here too.
             css_send_uncached_css(file_get_contents($candidatesheet));
+
+        } else if ($chunk !== null and file_exists($chunkedcandidatesheet)) {
+            // Greetings stupid legacy IEs!
+            css_send_cached_css($chunkedcandidatesheet, $etag);
+
+        } else {
+            // Real browsers - this is the expected result!
+            css_send_cached_css($candidatesheet, $etag);
         }
     }
+}
 
-    // The lock is still held, and the sheet still does not exist.
-    // Compile the CSS content.
+/**
+ * Generate the theme CSS and store it.
+ *
+ * @param   theme_config    $theme The theme to be generated
+ * @param   int             $rev The theme revision
+ * @param   int             $themesubrev The theme sub-revision
+ * @param   string          $candidatedir The directory that it should be stored in
+ * @return  string          The path that the primary (non-chunked) CSS was written to
+ */
+function theme_styles_generate_and_store($theme, $rev, $themesubrev, $candidatedir) {
+    global $CFG;
+
+    // Generate the content first.
     if (!$csscontent = $theme->get_css_cached_content()) {
         $csscontent = $theme->get_css_content();
         $theme->set_css_content_cache($csscontent);
     }
 
+    if ($theme->get_rtl_mode()) {
+        $type = "all-rtl";
+    } else {
+        $type = "all";
+    }
+
+    // Determine the candidatesheet path.
+    // Note: Do not pass any value for chunking as this is calcualted during css storage.
+    $candidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $themesubrev, $theme->use_svg_icons());
+
+    // Determine the chunking URL.
+    // Note, this will be removed when support for IE9 is removed.
     $relroot = preg_replace('|^http.?://[^/]+|', '', $CFG->wwwroot);
-    if (!empty($slashargument)) {
-        if ($usesvg) {
-            $chunkurl = "{$relroot}/theme/styles.php/{$themename}/{$rev}/$type";
+    if (!empty(min_get_slash_argument())) {
+        if ($theme->use_svg_icons()) {
+            $chunkurl = "{$relroot}/theme/styles.php/{$theme->name}/{$rev}/$type";
         } else {
-            $chunkurl = "{$relroot}/theme/styles.php/_s/{$themename}/{$rev}/$type";
+            $chunkurl = "{$relroot}/theme/styles.php/_s/{$theme->name}/{$rev}/$type";
         }
     } else {
-        if ($usesvg) {
-            $chunkurl = "{$relroot}/theme/styles.php?theme={$themename}&rev={$rev}&type=$type";
+        if ($theme->use_svg_icons()) {
+            $chunkurl = "{$relroot}/theme/styles.php?theme={$theme->name}&rev={$rev}&type=$type";
         } else {
-            $chunkurl = "{$relroot}/theme/styles.php?theme={$themename}&rev={$rev}&type=$type&svg=0";
+            $chunkurl = "{$relroot}/theme/styles.php?theme={$theme->name}&rev={$rev}&type=$type&svg=0";
         }
     }
 
-    css_store_css($theme, "$candidatedir/$type.css", $csscontent, true, $chunkurl);
+    // Store the CSS.
+    css_store_css($theme, $candidatesheet, $csscontent, true, $chunkurl);
 
-    if ($lock) {
-        // Now that the CSS has been generated and/or stored, release the lock.
-        // This will allow waiting clients to use the newly generated and stored CSS.
-        $lock->release();
+    // Store the fallback CSS in the temp directory.
+    // This file is used as a fallback when waiting for a theme to compile and is not versioned in any way.
+    $fallbacksheet = make_temp_directory("theme/{$theme->name}")
+        . "/"
+        . theme_styles_get_filename($type, 0, $theme->use_svg_icons());
+    css_store_css($theme, $fallbacksheet, $csscontent, true, $chunkurl);
+
+    return $candidatesheet;
+}
+
+/**
+ * Fetch the preferred fallback content location if available.
+ *
+ * @param   theme_config    $theme The theme to be generated
+ * @return  string          The path to the fallback sheet on disk
+ */
+function theme_styles_fallback_content($theme) {
+    global $CFG;
+
+    if (!$theme->usefallback) {
+        // This theme does not support fallbacks.
+        return false;
     }
+
+    $type = $theme->get_rtl_mode() ? 'all-rtl' : 'all';
+    $filename = theme_styles_get_filename($type);
+
+    $fallbacksheet = "{$CFG->tempdir}/theme/{$theme->name}/{$filename}";
+    if (file_exists($fallbacksheet)) {
+        return $fallbacksheet;
+    }
+
+    return false;
 }
 
-if (!$cache) {
-    // Do not pollute browser caches if invalid revision requested,
-    // let's ignore legacy IE breakage here too.
-    css_send_uncached_css($csscontent);
+/**
+ * Get the filename for the specified configuration.
+ *
+ * @param   string  $type The requested sheet type
+ * @param   int     $themesubrev The theme sub-revision
+ * @param   bool    $usesvg Whether SVGs are allowed
+ * @param   int     $chunk The chunk number if specified
+ * @return  string  The filename for this sheet
+ */
+function theme_styles_get_filename($type, $themesubrev = 0, $usesvg = true, $chunk = null) {
+    $filename = $type;
+    $filename .= ($themesubrev > 0) ? "_{$themesubrev}" : '';
+    $filename .= $usesvg ? '' : '-nosvg';
+    $filename .= $chunk ? ".{$chunk}" : '';
 
-} else if ($chunk !== null and file_exists($candidatesheet)) {
-    // Greetings stupid legacy IEs!
-    css_send_cached_css($candidatesheet, $etag);
+    return "{$filename}.css";
+}
 
-} else {
-    // Real browsers - this is the expected result!
-    css_send_cached_css_content($csscontent, $etag);
+/**
+ * Determine the correct etag for the specified configuration.
+ *
+ * @param   string  $themename The name of the theme
+ * @param   int     $rev The revision number
+ * @param   string  $type The requested sheet type
+ * @param   int     $themesubrev The theme sub-revision
+ * @param   bool    $usesvg Whether SVGs are allowed
+ * @param   int     $chunk The chunk number if specified
+ * @return  string  The etag to use for this request
+ */
+function theme_styles_get_etag($themename, $rev, $type, $themesubrev, $usesvg, $chunk) {
+    $etag = [$rev, $themename, $type, $themesubrev];
+
+    if (!$usesvg) {
+        $etag[] = 'nosvg';
+    }
+
+    if ($chunk) {
+        $etag[] = "chunk{$chunk}";
+    }
+
+    return sha1(implode('/', $etag));
 }
diff --git a/user/classes/participants_table.php b/user/classes/participants_table.php
new file mode 100644 (file)
index 0000000..1009f9d
--- /dev/null
@@ -0,0 +1,276 @@
+<?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/>.
+
+/**
+ * Contains the class used for the displaying the participants table.
+ *
+ * @package    core_user
+ * @copyright  2017 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_user;
+
+defined('MOODLE_INTERNAL') || die;
+
+global $CFG;
+
+require_once($CFG->libdir . '/tablelib.php');
+require_once($CFG->dirroot . '/user/lib.php');
+
+/**
+ * Class for the displaying the participants table.
+ *
+ * @package    core_user
+ * @copyright  2017 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class participants_table extends \table_sql {
+
+    /**
+     * @var int $courseid The course id
+     */
+    protected $courseid;
+
+    /**
+     * @var int|false False if groups not used, int if groups used, 0 for all groups.
+     */
+    protected $currentgroup;
+
+    /**
+     * @var int $accesssince The time the user last accessed the site
+     */
+    protected $accesssince;
+
+    /**
+     * @var int $roleid The role we are including, 0 means all enrolled users
+     */
+    protected $roleid;
+
+    /**
+     * @var string $search The string being searched.
+     */
+    protected $search;
+
+    /**
+     * @var bool $selectall Has the user selected all users on the page?
+     */
+    protected $selectall;
+
+    /**
+     * @var string[] The list of countries.
+     */
+    protected $countries;
+
+    /**
+     * @var string[] Extra fields to display.
+     */
+    protected $extrafields;
+
+    /**
+     * Sets up the table.
+     *
+     * @param int $courseid
+     * @param int|false $currentgroup False if groups not used, int if groups used, 0 for all groups.
+     * @param int $accesssince The time the user last accessed the site
+     * @param int $roleid The role we are including, 0 means all enrolled users
+     * @param string $search The string being searched
+     * @param bool $bulkoperations Is the user allowed to perform bulk operations?
+     * @param bool $selectall Has the user selected all users on the page?
+     */
+    public function __construct($courseid, $currentgroup, $accesssince, $roleid, $search,
+            $bulkoperations, $selectall) {
+        global $CFG;
+
+        parent::__construct('user-index-participants-' . $courseid);
+
+        // Get the context.
+        $context = \context_course::instance($courseid, MUST_EXIST);
+
+        // Define the headers and columns.
+        $headers = [];
+        $columns = [];
+
+        if ($bulkoperations) {
+            $headers[] = get_string('select');
+            $columns[] = 'select';
+        }
+
+        $headers[] = get_string('fullname');
+        $columns[] = 'fullname';
+
+        $extrafields = get_extra_user_fields($context);
+        foreach ($extrafields as $field) {
+            $headers[] = get_user_field_name($field);
+            $columns[] = $field;
+        }
+
+        // Get the list of fields we have to hide.
+        $hiddenfields = array();
+        if (!has_capability('moodle/course:viewhiddenuserfields', $context)) {
+            $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields));
+        }
+
+        // Do not show the columns if it exists in the hiddenfields array.
+        if (!isset($hiddenfields['city'])) {
+            $headers[] = get_string('city');
+            $columns[] = 'city';
+        }
+        if (!isset($hiddenfields['country'])) {
+            $headers[] = get_string('country');
+            $columns[] = 'country';
+        }
+        if (!isset($hiddenfields['lastaccess'])) {
+            if ($courseid == SITEID) {
+                $headers[] = get_string('lastsiteaccess');
+            } else {
+                $headers[] = get_string('lastcourseaccess');
+            }
+            $columns[] = 'lastaccess';
+        }
+
+        $this->define_columns($columns);
+        $this->define_headers($headers);
+
+        $this->no_sorting('select');
+
+        $this->set_attribute('id', 'participants');
+
+        // Set the variables we need to use later.
+        $this->courseid = $courseid;
+        $this->currentgroup = $currentgroup;
+        $this->accesssince = $accesssince;
+        $this->roleid = $roleid;
+        $this->search = $search;
+        $this->selectall = $selectall;
+        $this->countries = get_string_manager()->get_list_of_countries();
+        $this->extrafields = $extrafields;
+    }
+
+    /**
+     * Generate the select column.
+     *
+     * @param \stdClass $data
+     * @return string
+     */
+    public function col_select($data) {
+        if ($this->selectall) {
+            $checked = 'checked="true"';
+        } else {
+            $checked = '';
+        }
+        return '<input type="checkbox" class="usercheckbox" name="user' . $data->id . '" ' . $checked . '/>';
+    }
+
+    /**
+     * Generate the fullname column.
+     *
+     * @param \stdClass $data
+     * @return string
+     */
+    public function col_fullname($data) {
+        global $OUTPUT;
+
+        return $OUTPUT->user_picture($data, array('size' => 35, 'courseid' => $this->courseid)) . ' ' . fullname($data);
+    }
+
+    /**
+     * Generate the city column.
+     *
+     * @param \stdClass $data
+     * @return string
+     */
+    public function col_city($data) {
+        return $data->city;