Merge branch 'wip-MDL-59336-master' of git://github.com/marinaglancy/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 4 Jul 2017 17:58:51 +0000 (19:58 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 4 Jul 2017 17:58:51 +0000 (19:58 +0200)
122 files changed:
.travis.yml
admin/category.php
admin/cli/build_theme_css.php [new file with mode: 0644]
admin/settings/security.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
auth/oauth2/classes/auth.php
backup/backup.php
backup/import.php
backup/restore.php
blocks/myoverview/classes/output/courses_view.php
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/switchrole.php
course/tests/externallib_test.php
dataformat/html/classes/writer.php
dataformat/json/classes/writer.php
dataformat/upgrade.txt
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
lang/en/admin.php
lang/en/calendar.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/mustache_javascript_helper.php
lib/classes/plugin_manager.php
lib/csslib.php
lib/dataformatlib.php
lib/filelib.php
lib/navigationlib.php
lib/outputcomponents.php
lib/outputlib.php
lib/outputrenderers.php
lib/tablelib.php
lib/tests/admintree_test.php
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/index.php
login/signup.php
login/signup_form.php
mod/data/field/latlong/field.class.php
mod/forum/user.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
pluginfile.php
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/more/config.php
theme/styles.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 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..aadcd53 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. You can use mobile app to access the content of your courses and much more. Click <a href="{$a}">here</a> for more information';
 $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'] = 'Mobile setup link';
+$string['setuplink_desc'] = 'Link to documentation page to help users setup their mobile app.';
 $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 812cf98..aa5f70c 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 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 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 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 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 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 73d8b4a..364368c 100644 (file)
@@ -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 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 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 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..ca3b355 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 = 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 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 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 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 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..22a4b59 100644 (file)
@@ -39,7 +39,7 @@ 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');
 
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 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 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..4b34663 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'] = 'Please enter text here or upload a file';
+$string['submissionrequiredfile'] = 'Please upload a file or enter text in submission content box';
 $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 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 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 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));
 }
index acf34f5..b984e3c 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2017062200.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2017062900.00;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
-$release  = '3.4dev (Build: 20170622)'; // Human-friendly version name
+$release  = '3.4dev (Build: 20170629)'; // Human-friendly version name
 
 $branch   = '34';                       // This version's branch.
 $maturity = MATURITY_ALPHA;             // This version's maturity level.