Merge branch 'MDL-62600-master' of git://github.com/mickhawkins/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Wed, 15 Aug 2018 00:53:30 +0000 (08:53 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Wed, 15 Aug 2018 00:53:30 +0000 (08:53 +0800)
42 files changed:
admin/tool/dataprivacy/classes/metadata_registry.php
admin/tool/policy/classes/output/page_viewalldoc.php
admin/tool/policy/lang/en/tool_policy.php
admin/tool/policy/lib.php
admin/tool/policy/templates/page_viewalldoc.mustache
admin/tool/policy/tests/behat/managepolicies.feature
admin/tool/policy/viewall.php
blocks/myoverview/amd/build/event_list.min.js
blocks/myoverview/amd/src/event_list.js
blocks/myoverview/templates/timeline-view-courses.mustache
cohort/edit.php
cohort/index.php
config-dist.php
course/renderer.php
files/classes/privacy/provider.php
lang/en/files.php
lib/amd/build/form-autocomplete.min.js
lib/amd/src/form-autocomplete.js
lib/filelib.php
lib/moodlelib.php
lib/outputcomponents.php
lib/outputrenderers.php
lib/tests/filelib_test.php
lib/tests/moodle_url_test.php [new file with mode: 0644]
lib/tests/weblib_test.php
lib/upgrade.txt
lib/weblib.php
lib/xhprof/xhprof_moodle.php
mod/assign/submission/onlinetext/locallib.php
mod/data/field/radiobutton/field.class.php
mod/forum/classes/output/email/renderer.php
mod/forum/classes/output/emaildigestbasic/renderer_textemail.php
mod/forum/classes/output/emaildigestfull/renderer_textemail.php
mod/forum/classes/output/forum_post.php
mod/forum/tests/mail_test.php
mod/lesson/index.php
mod/lesson/locallib.php
mod/lesson/tests/locallib_test.php
pluginfile.php
theme/boost/templates/core_form/element-radio.mustache
theme/upgrade.txt
tokenpluginfile.php [new file with mode: 0644]

index 6282cc4..ca50157 100644 (file)
@@ -70,7 +70,8 @@ class metadata_registry {
                     $internaldata['compliant'] = false;
                 }
                 // Check to see if we are an external plugin.
-                $componentshortname = explode('_', $component);
+                // Plugin names can contain _ characters, limit to 2 to just remove initial plugintype.
+                $componentshortname = explode('_', $component, 2);
                 $shortname = array_pop($componentshortname);
                 if (isset($contributedplugins[$plugintype][$shortname])) {
                     $internaldata['external'] = true;
index 66ea3aa..801aac0 100644 (file)
@@ -48,12 +48,15 @@ use tool_policy\policy_version;
  */
 class page_viewalldoc implements renderable, templatable {
 
+    /** @var string Return url */
+    private $returnurl;
+
     /**
      * Prepare the page for rendering.
      *
      */
-    public function __construct() {
-
+    public function __construct($returnurl) {
+        $this->returnurl = $returnurl;
         $this->prepare_global_page_access();
         $this->prepare_policies();
     }
@@ -99,6 +102,9 @@ class page_viewalldoc implements renderable, templatable {
         ];
 
         $data->policies = array_values($this->policies);
+        if (!empty($this->returnurl)) {
+            $data->returnurl = $this->returnurl;
+        }
 
         array_walk($data->policies, function($item, $key) {
             $item->policytypestr = get_string('policydoctype'.$item->type, 'tool_policy');
index 3a75285..8f2a1d5 100644 (file)
@@ -50,6 +50,7 @@ $string['agreedyesonbehalfwithlinkall'] = 'Consent given on behalf of user; clic
 $string['agreedyeswithlink'] = 'Consent given; click to withdraw user consent for {$a}';
 $string['agreedyeswithlinkall'] = 'Consent given; click to withdraw user consent for all policies';
 $string['agreepolicies'] = 'Please agree to the following policies';
+$string['backtoprevious'] = 'Go back to previous page';
 $string['backtotop'] = 'Back to top';
 $string['consentbulk'] = 'Consent';
 $string['consentdetails'] = 'Give consent on behalf of user';
index 62919d3..563254d 100644 (file)
@@ -96,17 +96,17 @@ function tool_policy_before_standard_html_head() {
 /**
  * Callback to add footer elements.
  *
- * @return str valid html footer content
+ * @return string HTML footer content
  */
 function tool_policy_standard_footer_html() {
-    global $CFG;
+    global $CFG, $PAGE;
 
     $output = '';
     if (!empty($CFG->sitepolicyhandler)
             && $CFG->sitepolicyhandler == 'tool_policy') {
         $policies = api::get_current_versions_ids();
         if (!empty($policies)) {
-            $url = (new moodle_url('/admin/tool/policy/viewall.php'))->out();
+            $url = new moodle_url('/admin/tool/policy/viewall.php', ['returnurl' => $PAGE->url]);
             $output .= html_writer::link($url, get_string('userpolicysettings', 'tool_policy'));
             $output = html_writer::div($output, 'policiesfooter');
         }
index 2f9df36..3e4664c 100644 (file)
     -
 
     Context variables required for this template:
+    * returnurl - url to the previous page
     * policies - policy array
 
     Example context (json):
     {
+        "returnurl": "#",
         "policies": [
             {
                 "id": "2",
     }
 }}
 
+{{#returnurl}}
+<div class="text-right m-b-1">
+    <a href="{{returnurl}}">{{# str }} backtoprevious, tool_policy {{/ str }}</a>
+</div>
+{{/returnurl}}
+
 <a id="top"></a>
 <div id="policies_index">
 <h1>{{# str }} listactivepolicies, tool_policy {{/ str }}</h1>
index f25bd4d..4db68e3 100644 (file)
@@ -256,3 +256,21 @@ Feature: Manage policies
       | Policy1 Site policy, All users | Inactive      | v1         | 1 of 4 (25%) |
     And I should not see "v2"
     And I log out
+
+  Scenario: Current user can go back to previous page in List of active policies page
+    Given the following policies exist:
+      | Name       | Revision | Content    | Summary     | Status   |
+      | Policy1    | v1       | full text2 | short text2 | active   |
+    And I log in as "user1"
+    And I press "Next"
+    And I set the field "I agree to the Policy1" to "1"
+    And I press "Next"
+    And I follow "Preferences" in the user menu
+    And I should see "Preferences"
+    And I should see "Policies"
+    # User should see a redirect back to previous page link.
+    And I click on "Policies" "link"
+    And I should see "List of active policies"
+    And I should see "Go back to previous page"
+    When I click on "Go back to previous page" "link"
+    Then I should see "Preferences"
index 9f5528c..e1f3f60 100644 (file)
@@ -34,7 +34,9 @@ define('NO_SITEPOLICY_CHECK', true);
 // @codingStandardsIgnoreLine See the {@link page_viewalldoc} for the access control checks.
 require(__DIR__.'/../../../config.php');
 
-$viewallpage = new page_viewalldoc();
+$returnurl = optional_param('returnurl', '', PARAM_LOCALURL); // A return URL.
+
+$viewallpage = new page_viewalldoc($returnurl);
 
 $output = $PAGE->get_renderer('tool_policy');
 
index 0d04b59..343b46e 100644 (file)
Binary files a/blocks/myoverview/amd/build/event_list.min.js and b/blocks/myoverview/amd/build/event_list.min.js differ
index 9d96fc5..8a1bd91 100644 (file)
@@ -101,6 +101,8 @@ define(['jquery', 'core/notification', 'core/templates',
         if (!hasLoadedAll(root)) {
             // Only enable the button if we've got more events to load.
             viewMoreButton.prop('disabled', false);
+        } else {
+            viewMoreButton.addClass('hidden');
         }
     };
 
index a569405..6f35022 100644 (file)
@@ -86,7 +86,7 @@
             // If there was only one hidden block then we have no more to show now
             // so we can disable the button.
             if (blocks && blocks.length == 1) {
-                button.prop('disabled', true);
+                button.addClass('hidden');
             }
 
             if (data) {
index 8cae07a..48285fc 100644 (file)
@@ -23,9 +23,9 @@
  */
 
 require('../config.php');
-require($CFG->dirroot.'/course/lib.php');
-require($CFG->dirroot.'/cohort/lib.php');
-require($CFG->dirroot.'/cohort/edit_form.php');
+require_once($CFG->dirroot.'/course/lib.php');
+require_once($CFG->dirroot.'/cohort/lib.php');
+require_once($CFG->dirroot.'/cohort/edit_form.php');
 
 $id        = optional_param('id', 0, PARAM_INT);
 $contextid = optional_param('contextid', 0, PARAM_INT);
index 42db128..24fa3e7 100644 (file)
@@ -23,7 +23,7 @@
  */
 
 require('../config.php');
-require($CFG->dirroot.'/cohort/lib.php');
+require_once($CFG->dirroot.'/cohort/lib.php');
 require_once($CFG->libdir.'/adminlib.php');
 
 $contextid = optional_param('contextid', 0, PARAM_INT);
index afb9c1c..fa43382 100644 (file)
@@ -380,6 +380,12 @@ $CFG->admin = 'admin';
 //   profilingallowme, profilingallowall, profilinglifetime
 //       $CFG->earlyprofilingenabled = true;
 //
+// Disable database storage for profile data.
+// When using an exernal plugin to store profiling data it is often
+// desirable to not store the data in the database.
+//
+//      $CFG->disableprofilingtodatabase = true;
+//
 // Force displayed usernames
 //   A little hack to anonymise user names for all students.  If you set these
 //   then all non-teachers will always see these for every person.
index 01677a5..60aa87a 100644 (file)
@@ -69,62 +69,10 @@ class core_course_renderer extends plugin_renderer_base {
     }
 
     /**
-     * Adds the item in course settings navigation to toggle modchooser
-     *
-     * Theme can overwrite as an empty function to exclude it (for example if theme does not
-     * use modchooser at all)
-     *
      * @deprecated since 3.2
      */
     protected function add_modchoosertoggle() {
-        debugging('core_course_renderer::add_modchoosertoggle() is deprecated.', DEBUG_DEVELOPER);
-
-        global $CFG;
-
-        // Only needs to be done once per page.
-        if (!$this->page->requires->should_create_one_time_item_now('core_course_modchoosertoggle')) {
-            return;
-        }
-
-        if ($this->page->state > moodle_page::STATE_PRINTING_HEADER ||
-                $this->page->course->id == SITEID ||
-                !$this->page->user_is_editing() ||
-                !($context = context_course::instance($this->page->course->id)) ||
-                !has_capability('moodle/course:manageactivities', $context) ||
-                !course_ajax_enabled($this->page->course) ||
-                !($coursenode = $this->page->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE)) ||
-                !($turneditingnode = $coursenode->get('turneditingonoff'))) {
-            // Too late, or we are on site page, or we could not find the
-            // adjacent nodes in course settings menu, or we are not allowed to edit.
-            return;
-        }
-
-        if ($this->page->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) {
-            // We are on the course page, retain the current page params e.g. section.
-            $modchoosertoggleurl = clone($this->page->url);
-        } else {
-            // Edit on the main course page.
-            $modchoosertoggleurl = new moodle_url('/course/view.php', array('id' => $this->page->course->id,
-                'return' => $this->page->url->out_as_local_url(false)));
-        }
-        $modchoosertoggleurl->param('sesskey', sesskey());
-        if ($usemodchooser = get_user_preferences('usemodchooser', $CFG->modchooserdefault)) {
-            $modchoosertogglestring = get_string('modchooserdisable', 'moodle');
-            $modchoosertoggleurl->param('modchooser', 'off');
-        } else {
-            $modchoosertogglestring = get_string('modchooserenable', 'moodle');
-            $modchoosertoggleurl->param('modchooser', 'on');
-        }
-        $modchoosertoggle = navigation_node::create($modchoosertogglestring, $modchoosertoggleurl, navigation_node::TYPE_SETTING, null, 'modchoosertoggle');
-
-        // Insert the modchoosertoggle after the settings node 'turneditingonoff' (navigation_node only has function to insert before, so we insert before and then swap).
-        $coursenode->add_node($modchoosertoggle, 'turneditingonoff');
-        $turneditingnode->remove();
-        $coursenode->add_node($turneditingnode, 'modchoosertoggle');
-
-        $modchoosertoggle->add_class('modchoosertoggle');
-        $modchoosertoggle->add_class('visibleifjs');
-        user_preference_allow_ajax_update('usemodchooser', PARAM_BOOL);
+        throw new coding_exception('core_course_renderer::add_modchoosertoggle() can not be used anymore.');
     }
 
     /**
index 2bf2dbb..865d75f 100644 (file)
@@ -27,6 +27,8 @@ namespace core_files\privacy;
 defined('MOODLE_INTERNAL') || die();
 
 use core_privacy\local\metadata\collection;
+use core_privacy\local\request\contextlist;
+use core_privacy\local\request\approved_contextlist;
 
 /**
  * Data provider class.
@@ -41,7 +43,10 @@ use core_privacy\local\metadata\collection;
  */
 class provider implements
     \core_privacy\local\metadata\provider,
-    \core_privacy\local\request\subsystem\plugin_provider {
+    \core_privacy\local\request\subsystem\plugin_provider,
+
+    // We store a userkey for token-based file access.
+    \core_privacy\local\request\subsystem\provider {
 
     /**
      * Returns metadata.
@@ -65,7 +70,95 @@ class provider implements
             'timemodified' => 'privacy:metadata:files:timemodified',
         ], 'privacy:metadata:files');
 
+        $collection->add_subsystem_link('core_userkey', [], 'privacy:metadata:core_userkey');
+
         return $collection;
     }
 
+    /**
+     * Get the list of contexts that contain user information for the specified user.
+     *
+     * This is currently just the user context.
+     *
+     * @param int $userid The user to search.
+     * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
+     */
+    public static function get_contexts_for_userid(int $userid) : contextlist {
+        $sql = "SELECT ctx.id
+                  FROM {user_private_key} k
+                  JOIN {user} u ON k.userid = u.id
+                  JOIN {context} ctx ON ctx.instanceid = u.id AND ctx.contextlevel = :contextlevel
+                 WHERE k.userid = :userid AND k.script = :script";
+        $params = [
+            'userid' => $userid,
+            'contextlevel' => CONTEXT_USER,
+            'script' => 'core_files',
+        ];
+        $contextlist = new contextlist();
+        $contextlist->add_from_sql($sql, $params);
+
+        return $contextlist;
+    }
+
+    /**
+     * Export all user data for the specified user, in the specified contexts.
+     *
+     * @param approved_contextlist $contextlist The approved contexts to export information for.
+     */
+    public static function export_user_data(approved_contextlist $contextlist) {
+        // If the user has data, then only the CONTEXT_USER should be present so get the first context.
+        $contexts = $contextlist->get_contexts();
+        if (count($contexts) == 0) {
+            return;
+        }
+
+        // Sanity check that context is at the user context level, then get the userid.
+        $context = reset($contexts);
+        if ($context->contextlevel !== CONTEXT_USER) {
+            return;
+        }
+
+        // Export associated userkeys.
+        $subcontext = [
+            get_string('files'),
+        ];
+        \core_userkey\privacy\provider::export_userkeys($context, $subcontext, 'core_files');
+    }
+
+    /**
+     * Delete all use data which matches the specified deletion_criteria.
+     *
+     * @param context $context A user context.
+     */
+    public static function delete_data_for_all_users_in_context(\context $context) {
+        // Sanity check that context is at the user context level, then get the userid.
+        if ($context->contextlevel !== CONTEXT_USER) {
+            return;
+        }
+
+        // Delete all the userkeys.
+        \core_userkey\privacy\provider::delete_userkeys('core_files', $context->instanceid);
+    }
+
+    /**
+     * Delete all user data for the specified user, in the specified contexts.
+     *
+     * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
+     */
+    public static function delete_data_for_user(approved_contextlist $contextlist) {
+        // If the user has data, then only the user context should be present so get the first context.
+        $contexts = $contextlist->get_contexts();
+        if (count($contexts) == 0) {
+            return;
+        }
+
+        // Sanity check that context is at the user context level, then get the userid.
+        $context = reset($contexts);
+        if ($context->contextlevel !== CONTEXT_USER) {
+            return;
+        }
+
+        // Delete all the userkeys for core_files..
+        \core_userkey\privacy\provider::delete_userkeys('core_files', $context->instanceid);
+    }
 }
index 6cb55ac..b2e305b 100644 (file)
@@ -37,3 +37,4 @@ $string['privacy:metadata:files:source'] = 'The source of the file';
 $string['privacy:metadata:files:timecreated'] = 'The time when the file was created';
 $string['privacy:metadata:files:timemodified'] = 'The time when the file was last modified';
 $string['privacy:metadata:files:userid'] = 'The user who created the file';
+$string['privacy:metadata:core_userkey'] = 'A private token is generated and stored. This token can be used to access Moodle files without requiring you to log in.';
index c59dc5d..7755fdc 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js and b/lib/amd/build/form-autocomplete.min.js differ
index afe6e3f..b4c0053 100644 (file)
@@ -409,7 +409,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                 // Only create the item if it's new.
                 if (!found) {
                     var option = $('<option>');
-                    option.append(tag);
+                    option.append(document.createTextNode(tag));
                     option.attr('value', tag);
                     originalSelect.append(option);
                     option.prop('selected', true);
index a80f3a8..ded335b 100644 (file)
@@ -455,30 +455,42 @@ function file_prepare_draft_area(&$draftitemid, $contextid, $component, $fileare
  * Passing a new option reverse = true in the $options var will make the function to convert actual URLs in $text to encoded URLs
  * in the @@PLUGINFILE@@ form.
  *
- * @category files
- * @global stdClass $CFG
- * @param string $text The content that may contain ULRs in need of rewriting.
- * @param string $file The script that should be used to serve these files. pluginfile.php, draftfile.php, etc.
- * @param int $contextid This parameter and the next two identify the file area to use.
- * @param string $component
- * @param string $filearea helps identify the file area.
- * @param int $itemid helps identify the file area.
- * @param array $options text and file options ('forcehttps'=>false), use reverse = true to reverse the behaviour of the function.
- * @return string the processed text.
+ * @param   string  $text The content that may contain ULRs in need of rewriting.
+ * @param   string  $file The script that should be used to serve these files. pluginfile.php, draftfile.php, etc.
+ * @param   int     $contextid This parameter and the next two identify the file area to use.
+ * @param   string  $component
+ * @param   string  $filearea helps identify the file area.
+ * @param   int     $itemid helps identify the file area.
+ * @param   array   $options
+ *          bool    $options.forcehttps Force the user of https
+ *          bool    $options.reverse Reverse the behaviour of the function
+ *          bool    $options.includetoken Use a token for authentication
+ *          string  The processed text.
  */
 function file_rewrite_pluginfile_urls($text, $file, $contextid, $component, $filearea, $itemid, array $options=null) {
-    global $CFG;
+    global $CFG, $USER;
 
     $options = (array)$options;
     if (!isset($options['forcehttps'])) {
         $options['forcehttps'] = false;
     }
 
-    if (!$CFG->slasharguments) {
-        $file = $file . '?file=';
+    $baseurl = "{$CFG->wwwroot}/{$file}";
+    if (!empty($options['includetoken'])) {
+        $token = get_user_key('core_files', $USER->id);
+        $finalfile = basename($file);
+        $tokenfile = "token{$finalfile}";
+        $file = substr($file, 0, strlen($file) - strlen($finalfile)) . $tokenfile;
+        $baseurl = "{$CFG->wwwroot}/{$file}";
+
+        if (!$CFG->slasharguments) {
+            $baseurl .= "?token={$token}&file=";
+        } else {
+            $baseurl .= "/{$token}";
+        }
     }
 
-    $baseurl = "$CFG->wwwroot/$file/$contextid/$component/$filearea/";
+    $baseurl .= "/{$contextid}/{$component}/{$filearea}/";
 
     if ($itemid !== null) {
         $baseurl .= "$itemid/";
index e26ef63..4962aa4 100644 (file)
@@ -3112,9 +3112,10 @@ function validate_user_key($keyvalue, $script, $instance) {
  * @uses PARAM_ALPHANUM
  * @param string $script unique script identifier
  * @param int $instance optional instance id
+ * @param string $keyvalue The key. If not supplied, this will be fetched from the current session.
  * @return int Instance ID
  */
-function require_user_key_login($script, $instance=null) {
+function require_user_key_login($script, $instance = null, $keyvalue = null) {
     global $DB;
 
     if (!NO_MOODLE_COOKIES) {
@@ -3124,7 +3125,9 @@ function require_user_key_login($script, $instance=null) {
     // Extra safety.
     \core\session\manager::write_close();
 
-    $keyvalue = required_param('key', PARAM_ALPHANUM);
+    if (null === $keyvalue) {
+        $keyvalue = required_param('key', PARAM_ALPHANUM);
+    }
 
     $key = validate_user_key($keyvalue, $script, $instance);
 
index db773a6..dc90aea 100644 (file)
@@ -206,6 +206,11 @@ class user_picture implements renderable {
      */
     public $includefullname = false;
 
+    /**
+     * @var bool Include user authentication token.
+     */
+    public $includetoken = false;
+
     /**
      * User picture constructor.
      *
@@ -403,7 +408,8 @@ class user_picture implements renderable {
                 $path .= $page->theme->name.'/';
             }
             // Set the image URL to the URL for the uploaded file and return.
-            $url = moodle_url::make_pluginfile_url($contextid, 'user', 'icon', NULL, $path, $filename);
+            $url = moodle_url::make_pluginfile_url(
+                    $contextid, 'user', 'icon', null, $path, $filename, false, $this->includetoken);
             $url->param('rev', $this->user->picture);
             return $url;
         }
index 600e4b3..10b589d 100644 (file)
@@ -2503,6 +2503,7 @@ class core_renderer extends renderer_base {
      *     - class = image class attribute (default 'userpicture')
      *     - visibletoscreenreaders=true (whether to be visible to screen readers)
      *     - includefullname=false (whether to include the user's full name together with the user picture)
+     *     - includetoken = false
      * @return string HTML fragment
      */
     public function user_picture(stdClass $user, array $options = null) {
index 6931a59..d417ad3 100644 (file)
@@ -1052,6 +1052,79 @@ EOF;
         $this->assertEquals($originaltext, $finaltext);
     }
 
+    /**
+     * Test file_rewrite_pluginfile_urls with includetoken.
+     */
+    public function test_file_rewrite_pluginfile_urls_includetoken() {
+        global $USER, $CFG;
+
+        $CFG->slasharguments = true;
+
+        $this->resetAfterTest();
+
+        $syscontext = context_system::instance();
+        $originaltext = 'Fake test with an image <img src="@@PLUGINFILE@@/image.png">';
+        $options = ['includetoken' => true];
+
+        // Rewrite the content. This will generate a new token.
+        $finaltext = file_rewrite_pluginfile_urls(
+                $originaltext, 'pluginfile.php', $syscontext->id, 'user', 'private', 0, $options);
+
+        $token = get_user_key('core_files', $USER->id);
+        $expectedurl = new \moodle_url("/tokenpluginfile.php/{$token}/{$syscontext->id}/user/private/0/image.png");
+        $expectedtext = "Fake test with an image <img src=\"{$expectedurl}\">";
+        $this->assertEquals($expectedtext, $finaltext);
+
+        // Do it again - the second time will use an existing token.
+        $finaltext = file_rewrite_pluginfile_urls(
+                $originaltext, 'pluginfile.php', $syscontext->id, 'user', 'private', 0, $options);
+        $this->assertEquals($expectedtext, $finaltext);
+
+        // Now undo.
+        $options['reverse'] = true;
+        $finaltext = file_rewrite_pluginfile_urls($finaltext, 'pluginfile.php', $syscontext->id, 'user', 'private', 0, $options);
+
+        // Compare the final text is the same that the original.
+        $this->assertEquals($originaltext, $finaltext);
+    }
+
+    /**
+     * Test file_rewrite_pluginfile_urls with includetoken with slasharguments disabled..
+     */
+    public function test_file_rewrite_pluginfile_urls_includetoken_no_slashargs() {
+        global $USER, $CFG;
+
+        $CFG->slasharguments = false;
+
+        $this->resetAfterTest();
+
+        $syscontext = context_system::instance();
+        $originaltext = 'Fake test with an image <img src="@@PLUGINFILE@@/image.png">';
+        $options = ['includetoken' => true];
+
+        // Rewrite the content. This will generate a new token.
+        $finaltext = file_rewrite_pluginfile_urls(
+                $originaltext, 'pluginfile.php', $syscontext->id, 'user', 'private', 0, $options);
+
+        $token = get_user_key('core_files', $USER->id);
+        $expectedurl = new \moodle_url("/tokenpluginfile.php");
+        $expectedurl .= "?token={$token}&file=/{$syscontext->id}/user/private/0/image.png";
+        $expectedtext = "Fake test with an image <img src=\"{$expectedurl}\">";
+        $this->assertEquals($expectedtext, $finaltext);
+
+        // Do it again - the second time will use an existing token.
+        $finaltext = file_rewrite_pluginfile_urls(
+                $originaltext, 'pluginfile.php', $syscontext->id, 'user', 'private', 0, $options);
+        $this->assertEquals($expectedtext, $finaltext);
+
+        // Now undo.
+        $options['reverse'] = true;
+        $finaltext = file_rewrite_pluginfile_urls($finaltext, 'pluginfile.php', $syscontext->id, 'user', 'private', 0, $options);
+
+        // Compare the final text is the same that the original.
+        $this->assertEquals($originaltext, $finaltext);
+    }
+
     /**
      * Helpter function to create draft files
      *
diff --git a/lib/tests/moodle_url_test.php b/lib/tests/moodle_url_test.php
new file mode 100644 (file)
index 0000000..c5a9be4
--- /dev/null
@@ -0,0 +1,347 @@
+<?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/>.
+
+/**
+ * Tests for moodle_url.
+ *
+ * @package     core
+ * @copyright   2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Tests for moodle_url.
+ *
+ * @copyright   2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_moodle_url_testcase extends advanced_testcase {
+    /**
+     * Test basic moodle_url construction.
+     */
+    public function test_moodle_url_constructor() {
+        global $CFG;
+
+        $url = new moodle_url('/index.php');
+        $this->assertSame($CFG->wwwroot.'/index.php', $url->out());
+
+        $url = new moodle_url('/index.php', array());
+        $this->assertSame($CFG->wwwroot.'/index.php', $url->out());
+
+        $url = new moodle_url('/index.php', array('id' => 2));
+        $this->assertSame($CFG->wwwroot.'/index.php?id=2', $url->out());
+
+        $url = new moodle_url('/index.php', array('id' => 'two'));
+        $this->assertSame($CFG->wwwroot.'/index.php?id=two', $url->out());
+
+        $url = new moodle_url('/index.php', array('id' => 1, 'cid' => '2'));
+        $this->assertSame($CFG->wwwroot.'/index.php?id=1&amp;cid=2', $url->out());
+        $this->assertSame($CFG->wwwroot.'/index.php?id=1&cid=2', $url->out(false));
+
+        $url = new moodle_url('/index.php', null, 'test');
+        $this->assertSame($CFG->wwwroot.'/index.php#test', $url->out());
+
+        $url = new moodle_url('/index.php', array('id' => 2), 'test');
+        $this->assertSame($CFG->wwwroot.'/index.php?id=2#test', $url->out());
+    }
+
+    /**
+     * Tests moodle_url::get_path().
+     */
+    public function test_moodle_url_get_path() {
+        $url = new moodle_url('http://www.example.org:447/my/file/is/here.txt?really=1');
+        $this->assertSame('/my/file/is/here.txt', $url->get_path());
+
+        $url = new moodle_url('http://www.example.org/');
+        $this->assertSame('/', $url->get_path());
+
+        $url = new moodle_url('http://www.example.org/pluginfile.php/slash/arguments');
+        $this->assertSame('/pluginfile.php/slash/arguments', $url->get_path());
+        $this->assertSame('/pluginfile.php', $url->get_path(false));
+    }
+
+    public function test_moodle_url_round_trip() {
+        $strurl = 'http://moodle.org/course/view.php?id=5';
+        $url = new moodle_url($strurl);
+        $this->assertSame($strurl, $url->out(false));
+
+        $strurl = 'http://moodle.org/user/index.php?contextid=53&sifirst=M&silast=D';
+        $url = new moodle_url($strurl);
+        $this->assertSame($strurl, $url->out(false));
+    }
+
+    /**
+     * Test Moodle URL objects created with a param with empty value.
+     */
+    public function test_moodle_url_empty_param_values() {
+        $strurl = 'http://moodle.org/course/view.php?id=0';
+        $url = new moodle_url($strurl, array('id' => 0));
+        $this->assertSame($strurl, $url->out(false));
+
+        $strurl = 'http://moodle.org/course/view.php?id';
+        $url = new moodle_url($strurl, array('id' => false));
+        $this->assertSame($strurl, $url->out(false));
+
+        $strurl = 'http://moodle.org/course/view.php?id';
+        $url = new moodle_url($strurl, array('id' => null));
+        $this->assertSame($strurl, $url->out(false));
+
+        $strurl = 'http://moodle.org/course/view.php?id';
+        $url = new moodle_url($strurl, array('id' => ''));
+        $this->assertSame($strurl, $url->out(false));
+
+        $strurl = 'http://moodle.org/course/view.php?id';
+        $url = new moodle_url($strurl);
+        $this->assertSame($strurl, $url->out(false));
+    }
+
+    /**
+     * Test set good scheme on Moodle URL objects.
+     */
+    public function test_moodle_url_set_good_scheme() {
+        $url = new moodle_url('http://moodle.org/foo/bar');
+        $url->set_scheme('myscheme');
+        $this->assertSame('myscheme://moodle.org/foo/bar', $url->out());
+    }
+
+    /**
+     * Test set bad scheme on Moodle URL objects.
+     *
+     * @expectedException coding_exception
+     */
+    public function test_moodle_url_set_bad_scheme() {
+        $url = new moodle_url('http://moodle.org/foo/bar');
+        $url->set_scheme('not a valid $ scheme');
+    }
+
+    public function test_moodle_url_round_trip_array_params() {
+        $strurl = 'http://example.com/?a%5B1%5D=1&a%5B2%5D=2';
+        $url = new moodle_url($strurl);
+        $this->assertSame($strurl, $url->out(false));
+
+        $url = new moodle_url('http://example.com/?a[1]=1&a[2]=2');
+        $this->assertSame($strurl, $url->out(false));
+
+        // For un-keyed array params, we expect 0..n keys to be returned.
+        $strurl = 'http://example.com/?a%5B0%5D=0&a%5B1%5D=1';
+        $url = new moodle_url('http://example.com/?a[]=0&a[]=1');
+        $this->assertSame($strurl, $url->out(false));
+    }
+
+    public function test_compare_url() {
+        $url1 = new moodle_url('index.php', array('var1' => 1, 'var2' => 2));
+        $url2 = new moodle_url('index2.php', array('var1' => 1, 'var2' => 2, 'var3' => 3));
+
+        $this->assertFalse($url1->compare($url2, URL_MATCH_BASE));
+        $this->assertFalse($url1->compare($url2, URL_MATCH_PARAMS));
+        $this->assertFalse($url1->compare($url2, URL_MATCH_EXACT));
+
+        $url2 = new moodle_url('index.php', array('var1' => 1, 'var3' => 3));
+
+        $this->assertTrue($url1->compare($url2, URL_MATCH_BASE));
+        $this->assertFalse($url1->compare($url2, URL_MATCH_PARAMS));
+        $this->assertFalse($url1->compare($url2, URL_MATCH_EXACT));
+
+        $url2 = new moodle_url('index.php', array('var1' => 1, 'var2' => 2, 'var3' => 3));
+
+        $this->assertTrue($url1->compare($url2, URL_MATCH_BASE));
+        $this->assertTrue($url1->compare($url2, URL_MATCH_PARAMS));
+        $this->assertFalse($url1->compare($url2, URL_MATCH_EXACT));
+
+        $url2 = new moodle_url('index.php', array('var2' => 2, 'var1' => 1));
+
+        $this->assertTrue($url1->compare($url2, URL_MATCH_BASE));
+        $this->assertTrue($url1->compare($url2, URL_MATCH_PARAMS));
+        $this->assertTrue($url1->compare($url2, URL_MATCH_EXACT));
+
+        $url1->set_anchor('test');
+        $this->assertTrue($url1->compare($url2, URL_MATCH_BASE));
+        $this->assertTrue($url1->compare($url2, URL_MATCH_PARAMS));
+        $this->assertFalse($url1->compare($url2, URL_MATCH_EXACT));
+
+        $url2->set_anchor('test');
+        $this->assertTrue($url1->compare($url2, URL_MATCH_BASE));
+        $this->assertTrue($url1->compare($url2, URL_MATCH_PARAMS));
+        $this->assertTrue($url1->compare($url2, URL_MATCH_EXACT));
+    }
+
+    public function test_out_as_local_url() {
+        global $CFG;
+        // Test http url.
+        $url1 = new moodle_url('/lib/tests/weblib_test.php');
+        $this->assertSame('/lib/tests/weblib_test.php', $url1->out_as_local_url());
+
+        // Test https url.
+        $httpswwwroot = str_replace("http://", "https://", $CFG->wwwroot);
+        $url2 = new moodle_url($httpswwwroot.'/login/profile.php');
+        $this->assertSame('/login/profile.php', $url2->out_as_local_url());
+
+        // Test http url matching wwwroot.
+        $url3 = new moodle_url($CFG->wwwroot);
+        $this->assertSame('', $url3->out_as_local_url());
+
+        // Test http url matching wwwroot ending with slash (/).
+        $url3 = new moodle_url($CFG->wwwroot.'/');
+        $this->assertSame('/', $url3->out_as_local_url());
+    }
+
+    /**
+     * @expectedException coding_exception
+     * @return void
+     */
+    public function test_out_as_local_url_error() {
+        $url2 = new moodle_url('http://www.google.com/lib/tests/weblib_test.php');
+        $url2->out_as_local_url();
+    }
+
+    /**
+     * You should get error with modified url
+     *
+     * @expectedException coding_exception
+     * @return void
+     */
+    public function test_modified_url_out_as_local_url_error() {
+        global $CFG;
+
+        $modifiedurl = $CFG->wwwroot.'1';
+        $url3 = new moodle_url($modifiedurl.'/login/profile.php');
+        $url3->out_as_local_url();
+    }
+
+    /**
+     * Try get local url from external https url and you should get error
+     *
+     * @expectedException coding_exception
+     */
+    public function test_https_out_as_local_url_error() {
+        $url4 = new moodle_url('https://www.google.com/lib/tests/weblib_test.php');
+        $url4->out_as_local_url();
+    }
+
+    public function test_moodle_url_get_scheme() {
+        // Should return the scheme only.
+        $url = new moodle_url('http://www.example.org:447/my/file/is/here.txt?really=1');
+        $this->assertSame('http', $url->get_scheme());
+
+        // Should work for secure URLs.
+        $url = new moodle_url('https://www.example.org:447/my/file/is/here.txt?really=1');
+        $this->assertSame('https', $url->get_scheme());
+
+        // Should return an empty string if no scheme is specified.
+        $url = new moodle_url('www.example.org:447/my/file/is/here.txt?really=1');
+        $this->assertSame('', $url->get_scheme());
+    }
+
+    public function test_moodle_url_get_host() {
+        // Should return the host part only.
+        $url = new moodle_url('http://www.example.org:447/my/file/is/here.txt?really=1');
+        $this->assertSame('www.example.org', $url->get_host());
+    }
+
+    public function test_moodle_url_get_port() {
+        // Should return the port if one provided.
+        $url = new moodle_url('http://www.example.org:447/my/file/is/here.txt?really=1');
+        $this->assertSame(447, $url->get_port());
+
+        // Should return an empty string if port not specified.
+        $url = new moodle_url('http://www.example.org/some/path/here.php');
+        $this->assertSame('', $url->get_port());
+    }
+
+    /**
+     * Test the make_pluginfile_url function.
+     *
+     * @dataProvider make_pluginfile_url_provider
+     * @param   bool    $slashargs
+     * @param   array   $args Args to be provided to make_pluginfile_url
+     * @param   string  $expected The expected result
+     */
+    public function test_make_pluginfile_url($slashargs, $args, $expected) {
+        global $CFG;
+
+        $this->resetAfterTest();
+
+        $CFG->slasharguments = $slashargs;
+        $url = call_user_func_array('moodle_url::make_pluginfile_url', $args);
+        $this->assertRegexp($expected, $url->out(true));
+    }
+
+    /**
+     * Data provider for make_pluginfile_url tests.
+     *
+     * @return  array[]
+     */
+    public function make_pluginfile_url_provider() {
+        $baseurl = "https://www.example.com/moodle/pluginfile.php";
+        $tokenbaseurl = "https://www.example.com/moodle/tokenpluginfile.php";
+        return [
+            'Standard with slashargs' => [
+                'slashargs' => true,
+                'args' => [
+                    1,
+                    'mod_forum',
+                    'posts',
+                    422,
+                    '/my/location/',
+                    'file.png',
+                ],
+                'expected' => "@{$baseurl}/1/mod_forum/posts/422/my/location/file.png@",
+            ],
+            'Standard without slashargs' => [
+                'slashargs' => false,
+                'args' => [
+                    1,
+                    'mod_forum',
+                    'posts',
+                    422,
+                    '/my/location/',
+                    'file.png',
+                ],
+                'expected' => "@{$baseurl}\?file=%2F1%2Fmod_forum%2Fposts%2F422%2Fmy%2Flocation%2Ffile.png@",
+            ],
+            'Token included with slashargs' => [
+                'slashargs' => true,
+                'args' => [
+                    1,
+                    'mod_forum',
+                    'posts',
+                    422,
+                    '/my/location/',
+                    'file.png',
+                    false,
+                    true,
+                ],
+                'expected' => "@{$tokenbaseurl}/[^/]*/1/mod_forum/posts/422/my/location/file.png@",
+            ],
+            'Token included without slashargs' => [
+                'slashargs' => false,
+                'args' => [
+                    1,
+                    'mod_forum',
+                    'posts',
+                    422,
+                    '/my/location/',
+                    'file.png',
+                    false,
+                    true,
+                ],
+                'expected' => "@{$tokenbaseurl}\?file=%2F1%2Fmod_forum%2Fposts%2F422%2Fmy%2Flocation%2Ffile.png&amp;token=[a-z0-9]*@",
+            ],
+        ];
+    }
+}
index 2bf88cb..4446672 100644 (file)
@@ -244,238 +244,6 @@ class core_weblib_testcase extends advanced_testcase {
         $this->assertSame('this is a link [ http://someaddress.com/query ]', wikify_links('this is a <a href="http://someaddress.com/query">link</a>'));
     }
 
-    /**
-     * Test basic moodle_url construction.
-     */
-    public function test_moodle_url_constructor() {
-        global $CFG;
-
-        $url = new moodle_url('/index.php');
-        $this->assertSame($CFG->wwwroot.'/index.php', $url->out());
-
-        $url = new moodle_url('/index.php', array());
-        $this->assertSame($CFG->wwwroot.'/index.php', $url->out());
-
-        $url = new moodle_url('/index.php', array('id' => 2));
-        $this->assertSame($CFG->wwwroot.'/index.php?id=2', $url->out());
-
-        $url = new moodle_url('/index.php', array('id' => 'two'));
-        $this->assertSame($CFG->wwwroot.'/index.php?id=two', $url->out());
-
-        $url = new moodle_url('/index.php', array('id' => 1, 'cid' => '2'));
-        $this->assertSame($CFG->wwwroot.'/index.php?id=1&amp;cid=2', $url->out());
-        $this->assertSame($CFG->wwwroot.'/index.php?id=1&cid=2', $url->out(false));
-
-        $url = new moodle_url('/index.php', null, 'test');
-        $this->assertSame($CFG->wwwroot.'/index.php#test', $url->out());
-
-        $url = new moodle_url('/index.php', array('id' => 2), 'test');
-        $this->assertSame($CFG->wwwroot.'/index.php?id=2#test', $url->out());
-    }
-
-    /**
-     * Tests moodle_url::get_path().
-     */
-    public function test_moodle_url_get_path() {
-        $url = new moodle_url('http://www.example.org:447/my/file/is/here.txt?really=1');
-        $this->assertSame('/my/file/is/here.txt', $url->get_path());
-
-        $url = new moodle_url('http://www.example.org/');
-        $this->assertSame('/', $url->get_path());
-
-        $url = new moodle_url('http://www.example.org/pluginfile.php/slash/arguments');
-        $this->assertSame('/pluginfile.php/slash/arguments', $url->get_path());
-        $this->assertSame('/pluginfile.php', $url->get_path(false));
-    }
-
-    public function test_moodle_url_round_trip() {
-        $strurl = 'http://moodle.org/course/view.php?id=5';
-        $url = new moodle_url($strurl);
-        $this->assertSame($strurl, $url->out(false));
-
-        $strurl = 'http://moodle.org/user/index.php?contextid=53&sifirst=M&silast=D';
-        $url = new moodle_url($strurl);
-        $this->assertSame($strurl, $url->out(false));
-    }
-
-    /**
-     * Test Moodle URL objects created with a param with empty value.
-     */
-    public function test_moodle_url_empty_param_values() {
-        $strurl = 'http://moodle.org/course/view.php?id=0';
-        $url = new moodle_url($strurl, array('id' => 0));
-        $this->assertSame($strurl, $url->out(false));
-
-        $strurl = 'http://moodle.org/course/view.php?id';
-        $url = new moodle_url($strurl, array('id' => false));
-        $this->assertSame($strurl, $url->out(false));
-
-        $strurl = 'http://moodle.org/course/view.php?id';
-        $url = new moodle_url($strurl, array('id' => null));
-        $this->assertSame($strurl, $url->out(false));
-
-        $strurl = 'http://moodle.org/course/view.php?id';
-        $url = new moodle_url($strurl, array('id' => ''));
-        $this->assertSame($strurl, $url->out(false));
-
-        $strurl = 'http://moodle.org/course/view.php?id';
-        $url = new moodle_url($strurl);
-        $this->assertSame($strurl, $url->out(false));
-    }
-
-    /**
-     * Test set good scheme on Moodle URL objects.
-     */
-    public function test_moodle_url_set_good_scheme() {
-        $url = new moodle_url('http://moodle.org/foo/bar');
-        $url->set_scheme('myscheme');
-        $this->assertSame('myscheme://moodle.org/foo/bar', $url->out());
-    }
-
-    /**
-     * Test set bad scheme on Moodle URL objects.
-     *
-     * @expectedException coding_exception
-     */
-    public function test_moodle_url_set_bad_scheme() {
-        $url = new moodle_url('http://moodle.org/foo/bar');
-        $url->set_scheme('not a valid $ scheme');
-    }
-
-    public function test_moodle_url_round_trip_array_params() {
-        $strurl = 'http://example.com/?a%5B1%5D=1&a%5B2%5D=2';
-        $url = new moodle_url($strurl);
-        $this->assertSame($strurl, $url->out(false));
-
-        $url = new moodle_url('http://example.com/?a[1]=1&a[2]=2');
-        $this->assertSame($strurl, $url->out(false));
-
-        // For un-keyed array params, we expect 0..n keys to be returned.
-        $strurl = 'http://example.com/?a%5B0%5D=0&a%5B1%5D=1';
-        $url = new moodle_url('http://example.com/?a[]=0&a[]=1');
-        $this->assertSame($strurl, $url->out(false));
-    }
-
-    public function test_compare_url() {
-        $url1 = new moodle_url('index.php', array('var1' => 1, 'var2' => 2));
-        $url2 = new moodle_url('index2.php', array('var1' => 1, 'var2' => 2, 'var3' => 3));
-
-        $this->assertFalse($url1->compare($url2, URL_MATCH_BASE));
-        $this->assertFalse($url1->compare($url2, URL_MATCH_PARAMS));
-        $this->assertFalse($url1->compare($url2, URL_MATCH_EXACT));
-
-        $url2 = new moodle_url('index.php', array('var1' => 1, 'var3' => 3));
-
-        $this->assertTrue($url1->compare($url2, URL_MATCH_BASE));
-        $this->assertFalse($url1->compare($url2, URL_MATCH_PARAMS));
-        $this->assertFalse($url1->compare($url2, URL_MATCH_EXACT));
-
-        $url2 = new moodle_url('index.php', array('var1' => 1, 'var2' => 2, 'var3' => 3));
-
-        $this->assertTrue($url1->compare($url2, URL_MATCH_BASE));
-        $this->assertTrue($url1->compare($url2, URL_MATCH_PARAMS));
-        $this->assertFalse($url1->compare($url2, URL_MATCH_EXACT));
-
-        $url2 = new moodle_url('index.php', array('var2' => 2, 'var1' => 1));
-
-        $this->assertTrue($url1->compare($url2, URL_MATCH_BASE));
-        $this->assertTrue($url1->compare($url2, URL_MATCH_PARAMS));
-        $this->assertTrue($url1->compare($url2, URL_MATCH_EXACT));
-
-        $url1->set_anchor('test');
-        $this->assertTrue($url1->compare($url2, URL_MATCH_BASE));
-        $this->assertTrue($url1->compare($url2, URL_MATCH_PARAMS));
-        $this->assertFalse($url1->compare($url2, URL_MATCH_EXACT));
-
-        $url2->set_anchor('test');
-        $this->assertTrue($url1->compare($url2, URL_MATCH_BASE));
-        $this->assertTrue($url1->compare($url2, URL_MATCH_PARAMS));
-        $this->assertTrue($url1->compare($url2, URL_MATCH_EXACT));
-    }
-
-    public function test_out_as_local_url() {
-        global $CFG;
-        // Test http url.
-        $url1 = new moodle_url('/lib/tests/weblib_test.php');
-        $this->assertSame('/lib/tests/weblib_test.php', $url1->out_as_local_url());
-
-        // Test https url.
-        $httpswwwroot = str_replace("http://", "https://", $CFG->wwwroot);
-        $url2 = new moodle_url($httpswwwroot.'/login/profile.php');
-        $this->assertSame('/login/profile.php', $url2->out_as_local_url());
-
-        // Test http url matching wwwroot.
-        $url3 = new moodle_url($CFG->wwwroot);
-        $this->assertSame('', $url3->out_as_local_url());
-
-        // Test http url matching wwwroot ending with slash (/).
-        $url3 = new moodle_url($CFG->wwwroot.'/');
-        $this->assertSame('/', $url3->out_as_local_url());
-    }
-
-    /**
-     * @expectedException coding_exception
-     * @return void
-     */
-    public function test_out_as_local_url_error() {
-        $url2 = new moodle_url('http://www.google.com/lib/tests/weblib_test.php');
-        $url2->out_as_local_url();
-    }
-
-    /**
-     * You should get error with modified url
-     *
-     * @expectedException coding_exception
-     * @return void
-     */
-    public function test_modified_url_out_as_local_url_error() {
-        global $CFG;
-
-        $modifiedurl = $CFG->wwwroot.'1';
-        $url3 = new moodle_url($modifiedurl.'/login/profile.php');
-        $url3->out_as_local_url();
-    }
-
-    /**
-     * Try get local url from external https url and you should get error
-     *
-     * @expectedException coding_exception
-     */
-    public function test_https_out_as_local_url_error() {
-        $url4 = new moodle_url('https://www.google.com/lib/tests/weblib_test.php');
-        $url4->out_as_local_url();
-    }
-
-    public function test_moodle_url_get_scheme() {
-        // Should return the scheme only.
-        $url = new moodle_url('http://www.example.org:447/my/file/is/here.txt?really=1');
-        $this->assertSame('http', $url->get_scheme());
-
-        // Should work for secure URLs.
-        $url = new moodle_url('https://www.example.org:447/my/file/is/here.txt?really=1');
-        $this->assertSame('https', $url->get_scheme());
-
-        // Should return an empty string if no scheme is specified.
-        $url = new moodle_url('www.example.org:447/my/file/is/here.txt?really=1');
-        $this->assertSame('', $url->get_scheme());
-    }
-
-    public function test_moodle_url_get_host() {
-        // Should return the host part only.
-        $url = new moodle_url('http://www.example.org:447/my/file/is/here.txt?really=1');
-        $this->assertSame('www.example.org', $url->get_host());
-    }
-
-    public function test_moodle_url_get_port() {
-        // Should return the port if one provided.
-        $url = new moodle_url('http://www.example.org:447/my/file/is/here.txt?really=1');
-        $this->assertSame(447, $url->get_port());
-
-        // Should return an empty string if port not specified.
-        $url = new moodle_url('http://www.example.org/some/path/here.php');
-        $this->assertSame('', $url->get_port());
-    }
-
     public function test_clean_text() {
         $text = "lala <applet>xx</applet>";
         $this->assertSame($text, clean_text($text, FORMAT_PLAIN));
index b5bc0f5..e9a5c7e 100644 (file)
@@ -3,6 +3,14 @@ information provided here is intended especially for developers.
 
 === 3.6 ===
 
+* A new token-based version of pluginfile.php has been added which can be used for out-of-session file serving by
+  setting the `$includetoken` parameter to true on the `moodle_url::make_pluginfile_url()`, and
+  `moodle_url::make_file_url()` functions.
+* The following picture functions have been updated to support use of the new token-based file serving:
+    - print_group_picture
+    - get_group_picture_url
+* The `user_picture` class has a new public `$includetoken` property which can be set to make use of the new token-based
+  file serving.
 * Custom AJAX handlers for the form autocomplete fields can now optionally return string in their processResults()
   callback. If a string is returned, it is displayed instead of the list of suggested items. This can be used, for
   example, to inform the user that there are too many items matching the current search criteria.
index b253d3d..6b6c00a 100644 (file)
@@ -773,17 +773,41 @@ class moodle_url {
      * @param string $pathname
      * @param string $filename
      * @param bool $forcedownload
+     * @param boolean $includetoken Whether to use a user token when displaying this group image.
+     *                If the group picture is included in an e-mail or some other location where the audience is a specific
+     *                user who will not be logged in when viewing, then we use a token to authenticate the user.
      * @return moodle_url
      */
     public static function make_pluginfile_url($contextid, $component, $area, $itemid, $pathname, $filename,
-                                               $forcedownload = false) {
-        global $CFG;
-        $urlbase = "$CFG->wwwroot/pluginfile.php";
-        if ($itemid === null) {
-            return self::make_file_url($urlbase, "/$contextid/$component/$area".$pathname.$filename, $forcedownload);
+                                               $forcedownload = false, $includetoken = false) {
+        global $CFG, $USER;
+
+        $path = [];
+
+        if ($includetoken) {
+            $urlbase = "$CFG->wwwroot/tokenpluginfile.php";
+            $token = get_user_key('core_files', $USER->id);
+            if ($CFG->slasharguments) {
+                $path[] = $token;
+            }
         } else {
-            return self::make_file_url($urlbase, "/$contextid/$component/$area/$itemid".$pathname.$filename, $forcedownload);
+            $urlbase = "$CFG->wwwroot/pluginfile.php";
+        }
+        $path[] = $contextid;
+        $path[] = $component;
+        $path[] = $area;
+
+        if ($itemid !== null) {
+            $path[] = $itemid;
         }
+
+        $path = "/" . implode('/', $path) . "{$pathname}{$filename}";
+
+        $url = self::make_file_url($urlbase, $path, $forcedownload, $includetoken);
+        if ($includetoken && empty($CFG->slasharguments)) {
+            $url->param('token', $token);
+        }
+        return $url;
     }
 
     /**
@@ -2468,15 +2492,18 @@ function print_collapsible_region_end($return = false) {
  * @param boolean $large Default small picture, or large.
  * @param boolean $return If false print picture, otherwise return the output as string
  * @param boolean $link Enclose image in a link to view specified course?
+ * @param boolean $includetoken Whether to use a user token when displaying this group image.
+ *                If the group picture is included in an e-mail or some other location where the audience is a specific
+ *                user who will not be logged in when viewing, then we use a token to authenticate the user.
  * @return string|void Depending on the setting of $return
  */
-function print_group_picture($group, $courseid, $large=false, $return=false, $link=true) {
+function print_group_picture($group, $courseid, $large = false, $return = false, $link = true, $includetoken = false) {
     global $CFG;
 
     if (is_array($group)) {
         $output = '';
         foreach ($group as $g) {
-            $output .= print_group_picture($g, $courseid, $large, true, $link);
+            $output .= print_group_picture($g, $courseid, $large, true, $link, $includetoken);
         }
         if ($return) {
             return $output;
@@ -2486,7 +2513,7 @@ function print_group_picture($group, $courseid, $large=false, $return=false, $li
         }
     }
 
-    $pictureurl = get_group_picture_url($group, $courseid, $large);
+    $pictureurl = get_group_picture_url($group, $courseid, $large, $includetoken);
 
     // If there is no picture, do nothing.
     if (!isset($pictureurl)) {
@@ -2519,9 +2546,12 @@ function print_group_picture($group, $courseid, $large=false, $return=false, $li
  * @param  stdClass $group A group object.
  * @param  int $courseid The course ID for the group.
  * @param  bool $large A large or small group picture? Default is small.
+ * @param  boolean $includetoken Whether to use a user token when displaying this group image.
+ *                 If the group picture is included in an e-mail or some other location where the audience is a specific
+ *                 user who will not be logged in when viewing, then we use a token to authenticate the user.
  * @return moodle_url Returns the url for the group picture.
  */
-function get_group_picture_url($group, $courseid, $large = false) {
+function get_group_picture_url($group, $courseid, $large = false, $includetoken = false) {
     global $CFG;
 
     $context = context_course::instance($courseid);
@@ -2542,7 +2572,8 @@ function get_group_picture_url($group, $courseid, $large = false) {
         $file = 'f2';
     }
 
-    $grouppictureurl = moodle_url::make_pluginfile_url($context->id, 'group', 'icon', $group->id, '/', $file);
+    $grouppictureurl = moodle_url::make_pluginfile_url(
+            $context->id, 'group', 'icon', $group->id, '/', $file, false, $includetoken);
     $grouppictureurl->param('rev', $group->picture);
     return $grouppictureurl;
 }
index c0d82a8..82a1d1a 100644 (file)
@@ -882,14 +882,29 @@ class moodle_xhprofrun implements iXHProfRuns {
         $rec = new stdClass();
         $rec->runid = $this->runid;
         $rec->url = $this->url;
-        $rec->data = base64_encode(gzcompress(serialize($xhprof_data), 9));
         $rec->totalexecutiontime = $this->totalexecutiontime;
         $rec->totalcputime = $this->totalcputime;
         $rec->totalcalls = $this->totalcalls;
         $rec->totalmemory = $this->totalmemory;
         $rec->timecreated = $this->timecreated;
 
-        $DB->insert_record('profiling', $rec);
+        // Send to database with compressed and endoded data.
+        if (empty($CFG->disableprofilingtodatabase)) {
+            $rec->data = base64_encode(gzcompress(serialize($xhprof_data), 9));
+            $DB->insert_record('profiling', $rec);
+        }
+
+        // Send raw data to plugins.
+        $rec->data = $xhprof_data;
+
+        // Allow a plugin to take the trace data and process it.
+        if ($pluginsfunction = get_plugins_with_function('store_profiling_data')) {
+            foreach ($pluginsfunction as $plugintype => $plugins) {
+                foreach ($plugins as $pluginfunction) {
+                    $pluginfunction($rec);
+                }
+            }
+        }
 
         if (PHPUNIT_TEST) {
             // Calculate export variables.
index 85ea4dc..b2365b3 100644 (file)
@@ -395,7 +395,9 @@ class assign_submission_onlinetext extends assign_submission_plugin {
         $files = array();
         $onlinetextsubmission = $this->get_onlinetext_submission($submission->id);
 
-        if ($onlinetextsubmission) {
+        // Note that this check is the same logic as the result from the is_empty function but we do
+        // not call it directly because we already have the submission record.
+        if ($onlinetextsubmission && !empty($onlinetextsubmission->onlinetext)) {
             $finaltext = $this->assignment->download_rewrite_pluginfile_urls($onlinetextsubmission->onlinetext, $user, $this);
             $formattedtext = format_text($finaltext,
                                          $onlinetextsubmission->onlineformat,
@@ -575,8 +577,13 @@ class assign_submission_onlinetext extends assign_submission_plugin {
      */
     public function is_empty(stdClass $submission) {
         $onlinetextsubmission = $this->get_onlinetext_submission($submission->id);
+        $wordcount = 0;
+
+        if (isset($onlinetextsubmission->onlinetext)) {
+            $wordcount = count_words(trim($onlinetextsubmission->onlinetext));
+        }
 
-        return empty($onlinetextsubmission->onlinetext);
+        return $wordcount == 0;
     }
 
     /**
@@ -592,7 +599,13 @@ class assign_submission_onlinetext extends assign_submission_plugin {
         if (!isset($data->onlinetext_editor)) {
             return true;
         }
-        return !strlen((string)$data->onlinetext_editor['text']);
+        $wordcount = 0;
+
+        if (isset($data->onlinetext_editor['text'])) {
+            $wordcount = count_words(trim((string)$data->onlinetext_editor['text']));
+        }
+
+        return $wordcount == 0;
     }
 
     /**
index 2cf8827..416ccc6 100644 (file)
@@ -103,7 +103,8 @@ class data_field_radiobutton extends data_field_base {
         }
         $return = html_writer::label(get_string('fieldtypelabel', "datafield_" . $this->type),
             'menuf_' . $this->field->id, false, array('class' => 'accesshide'));
-        $return .= html_writer::select($options, 'f_'.$this->field->id, $value, null, array('class' => 'custom-select'));
+        $return .= html_writer::select($options, 'f_'.$this->field->id, $value,
+            array('' => 'choosedots'), array('class' => 'custom-select'));
         return $return;
     }
 
index f38b76c..fa5f012 100644 (file)
@@ -56,9 +56,16 @@ class renderer extends \mod_forum_renderer {
      */
     public function format_message_text($cm, $post) {
         $context = \context_module::instance($cm->id);
-        $message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php',
+        $message = file_rewrite_pluginfile_urls(
+            $post->message,
+            'pluginfile.php',
             $context->id,
-            'mod_forum', 'post', $post->id);
+            'mod_forum',
+            'post',
+            $post->id,
+            [
+                'includetoken' => true,
+            ]);
         $options = new \stdClass();
         $options->para = true;
         $options->context = $context;
index 1756031..3f4adfc 100644 (file)
@@ -53,9 +53,17 @@ class renderer_textemail extends \mod_forum\output\email\renderer_textemail {
      * @return string
      */
     public function format_message_text($cm, $post) {
-        $message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php',
-            \context_module::instance($cm->id)->id,
-            'mod_forum', 'post', $post->id);
+        $context = \context_module::instance($cm->id);
+        $message = file_rewrite_pluginfile_urls(
+            $post->message,
+            'pluginfile.php',
+            $context->id,
+            'mod_forum',
+            'post',
+            $post->id,
+            [
+                'includetoken' => true,
+            ]);
         return format_text_email($message, $post->messageformat);
     }
 }
index 119c4dc..cdf8579 100644 (file)
@@ -53,9 +53,17 @@ class renderer_textemail extends \mod_forum\output\email\renderer_textemail {
      * @return string
      */
     public function format_message_text($cm, $post) {
-        $message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php',
-            \context_module::instance($cm->id)->id,
-            'mod_forum', 'post', $post->id);
+        $context = \context_module::instance($cm->id);
+        $message = file_rewrite_pluginfile_urls(
+            $post->message,
+            'pluginfile.php',
+            $context->id,
+            'mod_forum',
+            'post',
+            $post->id,
+            [
+                'includetoken' => true,
+            ]);
         return format_text_email($message, $post->messageformat);
     }
 }
index 14f9026..a31d3ea 100644 (file)
@@ -135,7 +135,7 @@ class forum_post implements \renderable, \templatable {
      *
      * @param \mod_forum_renderer $renderer The render to be used for formatting the message and attachments
      * @param bool $plaintext Whethe the target is a plaintext target
-     * @return stdClass Data ready for use in a mustache template
+     * @return array Data ready for use in a mustache template
      */
     public function export_for_template(\renderer_base $renderer, $plaintext = false) {
         if ($plaintext) {
@@ -149,7 +149,7 @@ class forum_post implements \renderable, \templatable {
      * Export this data so it can be used as the context for a mustache template.
      *
      * @param \mod_forum_renderer $renderer The render to be used for formatting the message and attachments
-     * @return stdClass Data ready for use in a mustache template
+     * @return array Data ready for use in a mustache template
      */
     protected function export_for_template_text(\mod_forum_renderer $renderer) {
         return array(
@@ -180,9 +180,9 @@ class forum_post implements \renderable, \templatable {
             'discussionlink'                => $this->get_discussionlink(),
 
             'authorlink'                    => $this->get_authorlink(),
-            'authorpicture'                 => $this->get_author_picture(),
+            'authorpicture'                 => $this->get_author_picture($renderer),
 
-            'grouppicture'                  => $this->get_group_picture(),
+            'grouppicture'                  => $this->get_group_picture($renderer),
         );
     }
 
@@ -190,7 +190,7 @@ class forum_post implements \renderable, \templatable {
      * Export this data so it can be used as the context for a mustache template.
      *
      * @param \mod_forum_renderer $renderer The render to be used for formatting the message and attachments
-     * @return stdClass Data ready for use in a mustache template
+     * @return array Data ready for use in a mustache template
      */
     protected function export_for_template_html(\mod_forum_renderer $renderer) {
         return array(
@@ -221,9 +221,9 @@ class forum_post implements \renderable, \templatable {
             'discussionlink'                => $this->get_discussionlink(),
 
             'authorlink'                    => $this->get_authorlink(),
-            'authorpicture'                 => $this->get_author_picture(),
+            'authorpicture'                 => $this->get_author_picture($renderer),
 
-            'grouppicture'                  => $this->get_group_picture(),
+            'grouppicture'                  => $this->get_group_picture($renderer),
         );
     }
 
@@ -543,20 +543,20 @@ class forum_post implements \renderable, \templatable {
     /**
      * The HTML for the author's user picture.
      *
+     * @param   \renderer_base $renderer
      * @return string
      */
-    public function get_author_picture() {
-        global $OUTPUT;
-
-        return $OUTPUT->user_picture($this->author, array('courseid' => $this->course->id));
+    public function get_author_picture(\renderer_base $renderer) {
+        return $renderer->user_picture($this->author, array('courseid' => $this->course->id));
     }
 
     /**
      * The HTML for a group picture.
      *
+     * @param   \renderer_base $renderer
      * @return string
      */
-    public function get_group_picture() {
+    public function get_group_picture(\renderer_base $renderer) {
         if (isset($this->userfrom->groups)) {
             $groups = $this->userfrom->groups[$this->forum->id];
         } else {
@@ -564,7 +564,7 @@ class forum_post implements \renderable, \templatable {
         }
 
         if ($this->get_is_firstpost()) {
-            return print_group_picture($groups, $this->course->id, false, true, true);
+            return print_group_picture($groups, $this->course->id, false, true, true, true);
         }
     }
 }
index e15ceaa..e96ba94 100644 (file)
@@ -1042,7 +1042,7 @@ class mod_forum_mail_testcase extends advanced_testcase {
             '<div class="attachments">( *\n *)?<a href',
             '<div class="subject">\n.*HTML text and image', '>Moodle Forum',
             '<p>Welcome to Moodle, '
-                .'<img src="https://www.example.com/moodle/pluginfile.php/\d+/mod_forum/post/\d+/'
+            .'<img src="https://www.example.com/moodle/tokenpluginfile.php/[^/]*/\d+/mod_forum/post/\d+/'
                 .'Screen%20Shot%202016-03-22%20at%205\.54\.36%20AM%20%281%29\.png"'
                 .' alt="" width="200" height="393" class="img-responsive" />!</p>',
             '>Love Moodle', '>1\d1');
index a6b05d1..e2c3a63 100644 (file)
@@ -85,7 +85,8 @@ if ($usesections) {
     $table->head  = array ($strname, $strgrade, $strdeadline);
     $table->align = array ("left", "center", "center");
 }
-
+// Get all deadlines.
+$deadlines = lesson_get_user_deadline($course->id);
 foreach ($lessons as $lesson) {
     if (!$lesson->visible) {
         //Show dimmed if the mod is hidden
@@ -97,12 +98,13 @@ foreach ($lessons as $lesson) {
     $cm = get_coursemodule_from_instance('lesson', $lesson->id);
     $context = context_module::instance($cm->id);
 
-    if ($lesson->deadline == 0) {
+    $deadline = $deadlines[$lesson->id]->userdeadline;
+    if ($deadline == 0) {
         $due = $strnodeadline;
-    } else if ($lesson->deadline > $timenow) {
-        $due = userdate($lesson->deadline);
+    } else if ($deadline > $timenow) {
+        $due = userdate($deadline);
     } else {
-        $due = "<font color=\"red\">".userdate($lesson->deadline)."</font>";
+        $due = "<font color=\"red\">" . userdate($deadline) . "</font>";
     }
 
     if ($usesections) {
index 0d8d407..d775cc2 100644 (file)
@@ -1183,6 +1183,44 @@ function lesson_get_user_detailed_report_data(lesson $lesson, $userid, $attempt)
     return array($answerpages, $userstats);
 }
 
+/**
+ * Return user's deadline for all lessons in a course, hereby taking into account group and user overrides.
+ *
+ * @param int $courseid the course id.
+ * @return object An object with of all lessonsids and close unixdates in this course,
+ * taking into account the most lenient overrides, if existing and 0 if no close date is set.
+ */
+function lesson_get_user_deadline($courseid) {
+    global $DB, $USER;
+
+    // For teacher and manager/admins return lesson's deadline.
+    if (has_capability('moodle/course:update', context_course::instance($courseid))) {
+        $sql = "SELECT lesson.id, lesson.deadline AS userdeadline
+                  FROM {lesson} lesson
+                 WHERE lesson.course = :courseid";
+
+        $results = $DB->get_records_sql($sql, array('courseid' => $courseid));
+        return $results;
+    }
+
+    $sql = "SELECT a.id,
+                   COALESCE(v.userclose, v.groupclose, a.deadline, 0) AS userdeadline
+              FROM (
+                      SELECT lesson.id as lessonid,
+                             MAX(leo.deadline) AS userclose, MAX(qgo.deadline) AS groupclose
+                        FROM {lesson} lesson
+                   LEFT JOIN {lesson_overrides} leo on lesson.id = leo.lessonid AND leo.userid = :userid
+                   LEFT JOIN {groups_members} gm ON gm.userid = :useringroupid
+                   LEFT JOIN {lesson_overrides} qgo on lesson.id = qgo.lessonid AND qgo.groupid = gm.groupid
+                       WHERE lesson.course = :courseid
+                    GROUP BY lesson.id
+                   ) v
+              JOIN {lesson} a ON a.id = v.lessonid";
+
+    $results = $DB->get_records_sql($sql, array('userid' => $USER->id, 'useringroupid' => $USER->id, 'courseid' => $courseid));
+    return $results;
+
+}
 
 /**
  * Abstract class that page type's MUST inherit from.
index dedbb03..e12e6e8 100644 (file)
@@ -76,4 +76,144 @@ class mod_lesson_locallib_testcase extends advanced_testcase {
             }
         }
     }
+
+    /**
+     * Test test_lesson_get_user_deadline().
+     */
+    public function test_lesson_get_user_deadline() {
+        global $DB;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $basetimestamp = time(); // The timestamp we will base the enddates on.
+
+        // Create generator, course and lessons.
+        $student1 = $this->getDataGenerator()->create_user();
+        $student2 = $this->getDataGenerator()->create_user();
+        $student3 = $this->getDataGenerator()->create_user();
+        $teacher = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $lessongenerator = $this->getDataGenerator()->get_plugin_generator('mod_lesson');
+
+        // Both lessons close in two hours.
+        $lesson1 = $lessongenerator->create_instance(array('course' => $course->id, 'deadline' => $basetimestamp + 7200));
+        $lesson2 = $lessongenerator->create_instance(array('course' => $course->id, 'deadline' => $basetimestamp + 7200));
+        $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+        $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+
+        $student1id = $student1->id;
+        $student2id = $student2->id;
+        $student3id = $student3->id;
+        $teacherid = $teacher->id;
+
+        // Users enrolments.
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
+        $this->getDataGenerator()->enrol_user($student1id, $course->id, $studentrole->id, 'manual');
+        $this->getDataGenerator()->enrol_user($student2id, $course->id, $studentrole->id, 'manual');
+        $this->getDataGenerator()->enrol_user($student3id, $course->id, $studentrole->id, 'manual');
+        $this->getDataGenerator()->enrol_user($teacherid, $course->id, $teacherrole->id, 'manual');
+
+        // Create groups.
+        $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+        $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+        $group1id = $group1->id;
+        $group2id = $group2->id;
+        $this->getDataGenerator()->create_group_member(array('userid' => $student1id, 'groupid' => $group1id));
+        $this->getDataGenerator()->create_group_member(array('userid' => $student2id, 'groupid' => $group2id));
+
+        // Group 1 gets an group override for lesson 1 to close in three hours.
+        $record1 = (object) [
+            'lessonid' => $lesson1->id,
+            'groupid' => $group1id,
+            'deadline' => $basetimestamp + 10800 // In three hours.
+        ];
+        $DB->insert_record('lesson_overrides', $record1);
+
+        // Let's test lesson 1 closes in three hours for user student 1 since member of group 1.
+        // lesson 2 closes in two hours.
+        $this->setUser($student1id);
+        $params = new stdClass();
+
+        $comparearray = array();
+        $object = new stdClass();
+        $object->id = $lesson1->id;
+        $object->userdeadline = $basetimestamp + 10800; // The overriden deadline for lesson 1.
+
+        $comparearray[$lesson1->id] = $object;
+
+        $object = new stdClass();
+        $object->id = $lesson2->id;
+        $object->userdeadline = $basetimestamp + 7200; // The unchanged deadline for lesson 2.
+
+        $comparearray[$lesson2->id] = $object;
+
+        $this->assertEquals($comparearray, lesson_get_user_deadline($course->id));
+
+        // Let's test lesson 1 closes in two hours (the original value) for user student 3 since member of no group.
+        $this->setUser($student3id);
+        $params = new stdClass();
+
+        $comparearray = array();
+        $object = new stdClass();
+        $object->id = $lesson1->id;
+        $object->userdeadline = $basetimestamp + 7200; // The original deadline for lesson 1.
+
+        $comparearray[$lesson1->id] = $object;
+
+        $object = new stdClass();
+        $object->id = $lesson2->id;
+        $object->userdeadline = $basetimestamp + 7200; // The original deadline for lesson 2.
+
+        $comparearray[$lesson2->id] = $object;
+
+        $this->assertEquals($comparearray, lesson_get_user_deadline($course->id));
+
+        // User 2 gets an user override for lesson 1 to close in four hours.
+        $record2 = (object) [
+            'lessonid' => $lesson1->id,
+            'userid' => $student2id,
+            'deadline' => $basetimestamp + 14400 // In four hours.
+        ];
+        $DB->insert_record('lesson_overrides', $record2);
+
+        // Let's test lesson 1 closes in four hours for user student 2 since personally overriden.
+        // lesson 2 closes in two hours.
+        $this->setUser($student2id);
+
+        $comparearray = array();
+        $object = new stdClass();
+        $object->id = $lesson1->id;
+        $object->userdeadline = $basetimestamp + 14400; // The overriden deadline for lesson 1.
+
+        $comparearray[$lesson1->id] = $object;
+
+        $object = new stdClass();
+        $object->id = $lesson2->id;
+        $object->userdeadline = $basetimestamp + 7200; // The unchanged deadline for lesson 2.
+
+        $comparearray[$lesson2->id] = $object;
+
+        $this->assertEquals($comparearray, lesson_get_user_deadline($course->id));
+
+        // Let's test a teacher sees the original times.
+        // lesson 1 and lesson 2 close in two hours.
+        $this->setUser($teacherid);
+
+        $comparearray = array();
+        $object = new stdClass();
+        $object->id = $lesson1->id;
+        $object->userdeadline = $basetimestamp + 7200; // The unchanged deadline for lesson 1.
+
+        $comparearray[$lesson1->id] = $object;
+
+        $object = new stdClass();
+        $object->id = $lesson2->id;
+        $object->userdeadline = $basetimestamp + 7200; // The unchanged deadline for lesson 2.
+
+        $comparearray[$lesson2->id] = $object;
+
+        $this->assertEquals($comparearray, lesson_get_user_deadline($course->id));
+    }
 }
index 3d6d542..24e6cda 100644 (file)
  */
 
 // Disable moodle specific debug messages and any errors in output.
-define('NO_DEBUG_DISPLAY', true);
+if (!defined('NO_DEBUG_DISPLAY')) {
+    define('NO_DEBUG_DISPLAY', true);
+}
 
 require_once('config.php');
 require_once('lib/filelib.php');
 
-$relativepath = get_file_argument();
+if (empty($relativepath)) {
+    $relativepath = get_file_argument();
+}
 $forcedownload = optional_param('forcedownload', 0, PARAM_BOOL);
 $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.
index 01d9cec..87b0472 100644 (file)
     </div>
     <div class="col-md-9 checkbox">
         <div class="form-check">
+        <span class="text-nowrap">
             <label class="form-check-label">
-            {{^element.hardfrozen}}{{#element.frozen}}{{#element.checked}}
-                <input type="hidden" name="{{element.name}}" value="{{element.value}}">
-            {{/element.checked}}{{/element.frozen}}{{/element.hardfrozen}}
-            <input type="radio" class="form-check-input" {{^element.frozen}}name="{{element.name}}"{{/element.frozen}}
-                id="{{element.id}}" value="{{element.value}}"
-                {{#element.checked}}checked{{/element.checked}}
-                {{#element.frozen}}disabled{{/element.frozen}}
-                {{#error}}
-                    autofocus aria-describedby="id_error_{{element.name}}"
-                {{/error}} {{{element.attributes}}} >
-            {{#text}}
-                {{{.}}}
-            {{/text}}
-            {{^text}}
-                {{{label}}}
-            {{/text}}
+                {{^element.hardfrozen}}{{#element.frozen}}{{#element.checked}}
+                    <input type="hidden" name="{{element.name}}" value="{{element.value}}">
+                {{/element.checked}}{{/element.frozen}}{{/element.hardfrozen}}
+                <input type="radio" class="form-check-input" {{^element.frozen}}name="{{element.name}}"{{/element.frozen}}
+                    id="{{element.id}}" value="{{element.value}}"
+                    {{#element.checked}}checked{{/element.checked}}
+                    {{#element.frozen}}disabled{{/element.frozen}}
+                    {{#error}}
+                        autofocus aria-describedby="id_error_{{element.name}}"
+                    {{/error}} {{{element.attributes}}} >
+                {{#text}}
+                    {{{.}}}
+                {{/text}}
+                {{^text}}
+                    {{{label}}}
+                {{/text}}
             </label>
-        </div>
-        <span class="text-nowrap">
             {{#required}}<abbr class="initialism text-danger" title="{{#str}}required{{/str}}">{{#pix}}req, core, {{#str}}required{{/str}}{{/pix}}</abbr>{{/required}}
             {{#advanced}}<abbr class="initialism text-info" title="{{#str}}advanced{{/str}}">!</abbr>{{/advanced}}
             {{{helpbutton}}}
-        </span>
-         <div class="form-control-feedback invalid-feedback" id="id_error_{{element.name}}" {{#error}} style="display: block;"{{/error}}>
+            </span>
+        </div>
+        <div class="form-control-feedback invalid-feedback" id="id_error_{{element.name}}" {{#error}} style="display: block;"{{/error}}>
             {{{error}}}
         </div>
     </div>
index ed43009..2062b8b 100644 (file)
@@ -17,6 +17,7 @@ information provided here is intended especially for theme designer.
   * css_is_colour()
   * css_is_width()
   * css_sort_by_count()
+  * core_course_renderer::add_modchoosertoggle()
   * class css_optimiser
 
 === 3.4 ===
diff --git a/tokenpluginfile.php b/tokenpluginfile.php
new file mode 100644 (file)
index 0000000..156d412
--- /dev/null
@@ -0,0 +1,44 @@
+<?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/>.
+
+/**
+ * Entry point for token-based access to pluginfile.php.
+ *
+ * @package    core
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// Disable the use of sessions/cookies - we recreate $USER for every call.
+define('NO_MOODLE_COOKIES', true);
+
+// Disable debugging for this script.
+// It is typically used to display images.
+define('NO_DEBUG_DISPLAY', true);
+
+require_once('config.php');
+
+$relativepath = get_file_argument();
+$token = optional_param('token', '', PARAM_ALPHANUM);
+if (0 == strpos($relativepath, '/token/')) {
+    $relativepath = ltrim($relativepath, '/');
+    $pathparts = explode('/', $relativepath, 2);
+    $token = $pathparts[0];
+    $relativepath = "/{$pathparts[1]}";
+}
+
+require_user_key_login('core_files', null, $token);
+require_once('pluginfile.php');