Merge branch 'MDL-67057-master' of git://github.com/andrewnicols/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Thu, 7 Nov 2019 01:20:27 +0000 (09:20 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Thu, 7 Nov 2019 01:20:27 +0000 (09:20 +0800)
49 files changed:
admin/roles/admins.php
admin/tool/dataprivacy/classes/external.php
admin/tool/dataprivacy/templates/form-user-selector-suggestion.mustache
admin/user.php
admin/user/user_bulk_cohortadd.php
admin/user/user_bulk_display.php
calendar/amd/build/calendar_threemonth.min.js
calendar/amd/build/calendar_threemonth.min.js.map
calendar/amd/build/view_manager.min.js
calendar/amd/build/view_manager.min.js.map
calendar/amd/src/calendar_threemonth.js
calendar/amd/src/view_manager.js
enrol/locallib.php
filter/emoticon/filter.php
filter/emoticon/tests/filter_test.php
grade/report/grader/lib.php
h5p/classes/player.php
lib/amd/build/toast.min.js
lib/amd/build/toast.min.js.map
lib/amd/src/toast.js
lib/authlib.php
lib/classes/filetypes.php
lib/moodlelib.php
lib/outputrenderers.php
lib/php-css-parser/CSSList/CSSList.php
lib/php-css-parser/Rule/Rule.php
lib/php-css-parser/moodle_readme.txt
lib/tests/rtlcss_test.php [new file with mode: 0644]
lib/tests/weblib_test.php
lib/weblib.php
login/index.php
mod/forum/amd/build/local/layout/fullscreen.min.js
mod/forum/amd/build/local/layout/fullscreen.min.js.map
mod/forum/amd/src/local/layout/fullscreen.js
mod/lesson/editpage.php
mod/lesson/locallib.php
pix/f/h5p-180.png [new file with mode: 0644]
pix/f/h5p-24.png [new file with mode: 0644]
pix/f/h5p-256.png [new file with mode: 0644]
pix/f/h5p-48.png [new file with mode: 0644]
pix/f/h5p-64.png [new file with mode: 0644]
pix/f/h5p-72.png [new file with mode: 0644]
pix/f/h5p-80.png [new file with mode: 0644]
pix/f/h5p-96.png [new file with mode: 0644]
pix/f/h5p.png [new file with mode: 0644]
report/security/locallib.php
tokenpluginfile.php
user/selector/lib.php
webservice/renderer.php

index 4b341b3..a4b034a 100644 (file)
@@ -36,15 +36,12 @@ if (!is_siteadmin()) {
 }
 
 $admisselector = new core_role_admins_existing_selector();
-$admisselector->set_extra_fields(array('username', 'email'));
-
 $potentialadmisselector = new core_role_admins_potential_selector();
-$potentialadmisselector->set_extra_fields(array('username', 'email'));
 
 if (optional_param('add', false, PARAM_BOOL) and confirm_sesskey()) {
     if ($userstoadd = $potentialadmisselector->get_selected_users()) {
         $user = reset($userstoadd);
-        $username = fullname($user) . " ($user->username, $user->email)";
+        $username = $potentialadmisselector->output_user($user);
         echo $OUTPUT->header();
         $yesurl = new moodle_url('/admin/roles/admins.php', array('confirmadd'=>$user->id, 'sesskey'=>sesskey()));
         echo $OUTPUT->confirm(get_string('confirmaddadmin', 'core_role', $username), $yesurl, $PAGE->url);
@@ -58,7 +55,7 @@ if (optional_param('add', false, PARAM_BOOL) and confirm_sesskey()) {
         if ($USER->id == $user->id) {
             // Can not remove self.
         } else {
-            $username = fullname($user) . " ($user->username, $user->email)";
+            $username = $admisselector->output_user($user);
             echo $OUTPUT->header();
             $yesurl = new moodle_url('/admin/roles/admins.php', array('confirmdel'=>$user->id, 'sesskey'=>sesskey()));
             echo $OUTPUT->confirm(get_string('confirmdeladmin', 'core_role', $username), $yesurl, $PAGE->url);
index 20c28f9..4c4e719 100644 (file)
@@ -713,7 +713,6 @@ class external extends external_api {
 
         list($sql, $params) = users_search_sql($query, '', false, $extrafields, $excludedusers);
         $users = $DB->get_records_select('user', $sql, $params, $sort, $fields, 0, 30);
-
         $useroptions = [];
         foreach ($users as $user) {
             $useroption = (object)[
@@ -722,9 +721,10 @@ class external extends external_api {
             ];
             $useroption->extrafields = [];
             foreach ($extrafields as $extrafield) {
+                // Sanitize the extra fields to prevent potential XSS exploit.
                 $useroption->extrafields[] = (object)[
                     'name' => $extrafield,
-                    'value' => $user->$extrafield
+                    'value' => s($user->$extrafield)
                 ];
             }
             $useroptions[$user->id] = $useroption;
index 0f8443d..92896a6 100644 (file)
@@ -47,6 +47,6 @@
 <span>
     <span>{{fullname}}</span>
     {{#extrafields}}
-        <span><small>{{value}}</small></span>
+        <span><small>{{{value}}}</small></span>
     {{/extrafields}}
 </span>
index f6ab61f..216906c 100644 (file)
             $row = array ();
             $row[] = "<a href=\"../user/view.php?id=$user->id&amp;course=$site->id\">$fullname</a>";
             foreach ($extracolumns as $field) {
-                $row[] = $user->{$field};
+                $row[] = s($user->{$field});
             }
             $row[] = $user->city;
             $row[] = $user->country;
index 6ce1d97..f5e3947 100644 (file)
@@ -138,7 +138,7 @@ foreach ($users as $user) {
             '<a href="' . $CFG->wwwroot . '/user/view.php?id=' . $user->id . '&amp;course=' . SITEID . '">' .
             $user->fullname .
             '</a>',
-            $user->email,
+            s($user->email),
             $user->city,
             $user->country,
             $user->lastaccess ? format_time(time() - $user->lastaccess) : $strnever
index dd79566..ae6cd8a 100644 (file)
@@ -72,7 +72,7 @@ foreach($users as $user) {
     $table->data[] = array (
         '<a href="'.$CFG->wwwroot.'/user/view.php?id='.$user->id.'&amp;course='.SITEID.'">'.$user->fullname.'</a>',
 //        $user->username,
-        $user->email,
+        s($user->email),
         $user->city,
         $user->country,
         $user->lastaccess ? format_time(time() - $user->lastaccess) : $strnever
index b3af839..7777a3b 100644 (file)
Binary files a/calendar/amd/build/calendar_threemonth.min.js and b/calendar/amd/build/calendar_threemonth.min.js differ
index 6591833..04406fe 100644 (file)
Binary files a/calendar/amd/build/calendar_threemonth.min.js.map and b/calendar/amd/build/calendar_threemonth.min.js.map differ
index 628c38d..79f116c 100644 (file)
Binary files a/calendar/amd/build/view_manager.min.js and b/calendar/amd/build/view_manager.min.js differ
index 5edaca0..161e799 100644 (file)
Binary files a/calendar/amd/build/view_manager.min.js.map and b/calendar/amd/build/view_manager.min.js.map differ
index 68d16be..33545c3 100644 (file)
@@ -93,6 +93,8 @@ function(
                 requestYear = nextMonth.data('nextYear');
                 requestMonth = nextMonth.data('nextMonth');
                 oldMonth = previousMonth;
+            } else {
+                return $.Deferred().resolve();
             }
 
             return CalendarViewManager.refreshMonthContent(
index 28b04d9..887645b 100644 (file)
@@ -317,7 +317,7 @@ define([
                     return arguments;
                 })
                 .then(function() {
-                    $('body').trigger(CalendarEvents.dayChanged, [year, month, day, courseId, categoryId]);
+                    $('body').trigger(CalendarEvents.dayChanged, [year, month, courseId, categoryId]);
                     return arguments;
                 });
         };
index bb4c858..0a46f0f 100644 (file)
@@ -1220,7 +1220,7 @@ class course_enrolment_manager {
         );
 
         foreach ($extrafields as $field) {
-            $details[$field] = $user->{$field};
+            $details[$field] = s($user->{$field});
         }
 
         // Last time user has accessed the site.
index 5f5b3ff..6b6651d 100644 (file)
@@ -104,14 +104,14 @@ class filter_emoticon extends moodle_text_filter {
         }
 
         // Detect all zones that we should not handle (including the nested tags).
-        $processing = preg_split('/(<\/?(?:span|script)[^>]*>)/is', $text, 0, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
+        $processing = preg_split('/(<\/?(?:span|script|pre)[^>]*>)/is', $text, 0, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
 
         // Initialize the results.
         $resulthtml = "";
         $exclude = 0;
 
         // Define the patterns that mark the start of the forbidden zones.
-        $excludepattern = array('/^<script/is', '/^<span[^>]+class="nolink[^"]*"/is');
+        $excludepattern = array('/^<script/is', '/^<span[^>]+class="nolink[^"]*"/is', '/^<pre/is');
 
         // Loop through the fragments.
         foreach ($processing as $fragment) {
@@ -126,13 +126,15 @@ class filter_emoticon extends moodle_text_filter {
             }
             if ($exclude > 0) {
                 // If we are ignoring the fragment, then we must check if we may have reached the end of the zone.
-                if (strpos($fragment, '</span') !== false || strpos($fragment, '</script') !== false) {
+                if (strpos($fragment, '</span') !== false || strpos($fragment, '</script') !== false
+                    || strpos($fragment, '</pre') !== false) {
                     $exclude -= 1;
                     // This is needed because of a double increment at the first element.
                     if ($exclude == 1) {
                         $exclude -= 1;
                     }
-                } else if (strpos($fragment, '<span') !== false || strpos($fragment, '<script') !== false) {
+                } else if (strpos($fragment, '<span') !== false || strpos($fragment, '<script') !== false
+                    || strpos($fragment, '<pre') !== false) {
                     // If we find a nested tag we increase the exclusion level.
                     $exclude = $exclude + 1;
                 }
index 10ee76e..9935db9 100644 (file)
@@ -95,6 +95,21 @@ class filter_emoticon_testcase extends advanced_testcase {
                 'format' => FORMAT_HTML,
                 'expected' => '<span class="nolink"><span>(n)</span>(n)</span>' . $this->get_converted_content_for_emoticon('(n)'),
             ],
+            'Basic pre should not be processed' => [
+                'input' => '<pre>(n)</pre>',
+                'format' => FORMAT_HTML,
+                'expected' => '<pre>(n)</pre>',
+            ],
+            'Nested pre should not be processed' => [
+                'input' => '<pre><pre>(n)</pre>(n)</pre>',
+                'format' => FORMAT_HTML,
+                'expected' => '<pre><pre>(n)</pre>(n)</pre>',
+            ],
+            'Nested pre should not be processed but following emoticon' => [
+                'input' => '<pre><pre>(n)</pre>(n)</pre>(n)',
+                'format' => FORMAT_HTML,
+                'expected' => '<pre><pre>(n)</pre>(n)</pre>' . $this->get_converted_content_for_emoticon('(n)'),
+            ],
         ];
     }
 
index 1b82a20..53741c4 100644 (file)
@@ -767,7 +767,7 @@ class grade_report_grader extends grade_report {
                 $fieldcell = new html_table_cell();
                 $fieldcell->attributes['class'] = 'userfield user' . $field;
                 $fieldcell->header = false;
-                $fieldcell->text = $user->{$field};
+                $fieldcell->text = s($user->{$field});
                 $userrow->cells[] = $fieldcell;
             }
 
index 224e0bf..b3be57d 100644 (file)
@@ -322,7 +322,7 @@ class player {
      * @return string|false pathnamehash for the file in the internal URL.
      */
     private function get_pluginfile_hash(string $url) {
-        global $USER;
+        global $USER, $CFG;
 
         // Decode the URL before start processing it.
         $url = new \moodle_url(urldecode($url));
@@ -359,17 +359,54 @@ class player {
             throw new \moodle_exception('h5pprivatefile', 'core_h5p');
         }
 
-        // For CONTEXT_MODULE, check if the user is enrolled in the course and has permissions view this .h5p file.
-        if ($this->context->contextlevel == CONTEXT_MODULE) {
+        // For CONTEXT_COURSECAT No login necessary - unless login forced everywhere.
+        if ($this->context->contextlevel == CONTEXT_COURSECAT) {
+            if ($CFG->forcelogin) {
+                require_login(null, true, null, false, true);
+            }
+        }
+
+        // For CONTEXT_BLOCK.
+        if ($this->context->contextlevel == CONTEXT_BLOCK) {
+            if ($this->context->get_course_context(false)) {
+                // If block is in course context, then check if user has capability to access course.
+                require_course_login($course, true, null, false, true);
+            } else if ($CFG->forcelogin) {
+                // No login necessary - unless login forced everywhere.
+                require_login(null, true, null, false, true);
+            } else {
+                // Get parent context and see if user have proper permission.
+                $parentcontext = $this->context->get_parent_context();
+                if ($parentcontext->contextlevel === CONTEXT_COURSECAT) {
+                    // Check if category is visible and user can view this category.
+                    if (!core_course_category::get($parentcontext->instanceid, IGNORE_MISSING)) {
+                        send_file_not_found();
+                    }
+                } else if ($parentcontext->contextlevel === CONTEXT_USER && $parentcontext->instanceid != $USER->id) {
+                    // The block is in the context of a user, it is only visible to the user who it belongs to.
+                    send_file_not_found();
+                }
+                if ($filearea !== 'content') {
+                    send_file_not_found();
+                }
+            }
+        }
+
+        // For CONTEXT_MODULE and CONTEXT_COURSE check if the user is enrolled in the course.
+        // And for CONTEXT_MODULE has permissions view this .h5p file.
+        if ($this->context->contextlevel == CONTEXT_MODULE ||
+                $this->context->contextlevel == CONTEXT_COURSE) {
             // Require login to the course first (without login to the module).
             require_course_login($course, true, null, false, true);
 
             // Now check if module is available OR it is restricted but the intro is shown on the course page.
-            $cminfo = \cm_info::create($cm);
-            if (!$cminfo->uservisible) {
-                if (!$cm->showdescription || !$cminfo->is_visible_on_course_page()) {
-                    // Module intro is not visible on the course page and module is not available, show access error.
-                    require_course_login($course, true, $cminfo, false, true);
+            if ($this->context->contextlevel == CONTEXT_MODULE) {
+                $cminfo = \cm_info::create($cm);
+                if (!$cminfo->uservisible) {
+                    if (!$cm->showdescription || !$cminfo->is_visible_on_course_page()) {
+                        // Module intro is not visible on the course page and module is not available, show access error.
+                        require_course_login($course, true, $cminfo, false, true);
+                    }
                 }
             }
         }
@@ -377,12 +414,15 @@ class player {
         // Some components, such as mod_page or mod_resource, add the revision to the URL to prevent caching problems.
         // So the URL contains this revision number as itemid but a 0 is always stored in the files table.
         // In order to get the proper hash, a callback should be done (looking for those exceptions).
-        $pathdata = component_callback($component, 'get_path_from_pluginfile', [$filearea, $parts], null);
+        $pathdata = null;
+        if ($this->context->contextlevel == CONTEXT_MODULE || $this->context->contextlevel == CONTEXT_BLOCK) {
+            $pathdata = component_callback($component, 'get_path_from_pluginfile', [$filearea, $parts], null);
+        }
         if (null === $pathdata) {
             // Look for the components and fileareas which have empty itemid defined in xxx_pluginfile.
             $hasnullitemid = false;
             $hasnullitemid = $hasnullitemid || ($component === 'user' && ($filearea === 'private' || $filearea === 'profile'));
-            $hasnullitemid = $hasnullitemid || ($component === 'mod' && $filearea === 'intro');
+            $hasnullitemid = $hasnullitemid || (substr($component, 0, 4) === 'mod_' && $filearea === 'intro');
             $hasnullitemid = $hasnullitemid || ($component === 'course' &&
                     ($filearea === 'summary' || $filearea === 'overviewfiles'));
             $hasnullitemid = $hasnullitemid || ($component === 'coursecat' && $filearea === 'description');
index 0ec003f..25aacf0 100644 (file)
Binary files a/lib/amd/build/toast.min.js and b/lib/amd/build/toast.min.js differ
index ef6ab4a..c540fcc 100644 (file)
Binary files a/lib/amd/build/toast.min.js.map and b/lib/amd/build/toast.min.js.map differ
index 8fe5688..7128cc5 100644 (file)
@@ -78,10 +78,10 @@ export const add = async(message, configuration) => {
 };
 
 const getTargetNode = async() => {
-    const region = document.querySelector('.toast-wrapper');
+    const regions = document.querySelectorAll('.toast-wrapper');
 
-    if (region) {
-        return region;
+    if (regions.length) {
+        return regions[regions.length - 1];
     }
 
     await addToastRegion(document.body, 'fixed-bottom');
index 336bf87..0ea720d 100644 (file)
@@ -766,6 +766,9 @@ class auth_plugin_base {
      * @return string[] An array of strings with keys subject and message
      */
     public function get_password_change_info(stdClass $user) : array {
+
+        global $USER;
+
         $site = get_site();
         $systemcontext = context_system::instance();
 
@@ -776,6 +779,10 @@ class auth_plugin_base {
         $data->sitename  = format_string($site->fullname);
         $data->admin     = generate_email_signoff();
 
+        // This is a workaround as change_password_url() is designed to allow
+        // use of the $USER global. See MDL-66984.
+        $olduser = $USER;
+        $USER = $user;
         if ($this->can_change_password() and $this->change_password_url()) {
             // We have some external url for password changing.
             $data->link = $this->change_password_url();
@@ -783,6 +790,7 @@ class auth_plugin_base {
             // No way to change password, sorry.
             $data->link = '';
         }
+        $USER = $olduser;
 
         if (!empty($data->link) and has_capability('moodle/user:changeownpassword', $systemcontext, $user->id)) {
             $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
index 34b8a6e..258170d 100644 (file)
@@ -114,6 +114,7 @@ abstract class core_filetypes {
             'gzip' => array('type' => 'application/g-zip', 'icon' => 'archive',
                     'groups' => array('archive'), 'string' => 'archive'),
             'h' => array('type' => 'text/plain', 'icon' => 'sourcecode'),
+            'h5p' => array('type' => 'application/zip', 'icon' => 'h5p', 'string' => 'archive'),
             'hpp' => array('type' => 'text/plain', 'icon' => 'sourcecode'),
             'hqx' => array('type' => 'application/mac-binhex40', 'icon' => 'archive',
                     'groups' => array('archive'), 'string' => 'archive'),
index b8489f5..d10f31d 100644 (file)
@@ -3258,6 +3258,8 @@ function require_user_key_login($script, $instance = null, $keyvalue = null) {
         print_error('invaliduserid');
     }
 
+    core_user::require_active_user($user, true, true);
+
     // Emulate normal session.
     enrol_check_plugins($user);
     \core\session\manager::set_user($user);
index df50b1a..037ec67 100644 (file)
@@ -2807,8 +2807,8 @@ EOD;
             $output .= $this->header();
         }
 
-        $message = '<p class="errormessage">' . $message . '</p>'.
-                '<p class="errorcode"><a href="' . $moreinfourl . '">' .
+        $message = '<p class="errormessage">' . s($message) . '</p>'.
+                '<p class="errorcode"><a href="' . s($moreinfourl) . '">' .
                 get_string('moreinformation') . '</a></p>';
         if (empty($CFG->rolesactive)) {
             $message .= '<p class="errormessage">' . get_string('installproblem', 'error') . '</p>';
index bf4efcb..11e1dcc 100644 (file)
@@ -61,7 +61,6 @@ abstract class CSSList implements Renderable, Commentable {
                                $oListItem->setComments($comments);
                                $oList->append($oListItem);
                        }
-                       $oParserState->consumeWhiteSpace();
                }
                if(!$bIsRoot && !$bLenientParsing) {
                        throw new SourceException("Unexpected end of document", $oParserState->currentLine());
index 3fa031b..4480948 100644 (file)
@@ -56,7 +56,6 @@ class Rule implements Renderable, Commentable {
                while ($oParserState->comes(';')) {
                        $oParserState->consume(';');
                }
-               $oParserState->consumeWhiteSpace();
 
                return $oRule;
        }
index f4a2cc2..07f3b4d 100644 (file)
@@ -8,3 +8,5 @@ Import procedure:
 - Copy all the files from the folder 'lib/Sabberworm/CSS/' in this directory.
 
 - Apply the patch in Sabberworm/PHP-CSS-Parser#115
+
+- Apply the patch in sabberworm/PHP-CSS-Parser/issues/173 (if this has not already been resolved upstream).
diff --git a/lib/tests/rtlcss_test.php b/lib/tests/rtlcss_test.php
new file mode 100644 (file)
index 0000000..bc6d327
--- /dev/null
@@ -0,0 +1,1266 @@
+<?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 the core_rtlcss class.
+ *
+ * The core_rtlcss class extends \MoodleHQ\RTLCSS\RTLCSS library which depends on sabberworm/php-css-parser library.
+ * This test verifies that css parsing works as expected should any of the above change.
+ *
+ * @package    core
+ * @category   phpunit
+ * @copyright  2019 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+defined('MOODLE_INTERNAL') || die();
+
+use Sabberworm\CSS\Parser;
+use Sabberworm\CSS\OutputFormat;
+
+/**
+ * Class rtlcss_test.
+ */
+class rtlcss_test extends basic_testcase {
+    /**
+     * Data provider.
+     * @return array
+     */
+    public function background_image_provider() {
+        return [
+            /* Not supported by MoodleHQ/RTLCSS yet.
+            [[
+                'should' => 'Should process string map in url (processUrls:true)',
+                'expected' => 'div { background-image: url(images/rtl.png), url(images/right.png);}',
+                'input'    => 'div { background-image: url(images/ltr.png), url(images/left.png);}',
+                'reversable' => true,
+                'options' => [ 'processUrls' => true ],
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should not negate color value for linear gradient',
+                'expected' => 'div { background-image: linear-gradient(rgba(255, 255, 255, 0.3) 0%, #ff8 100%);}',
+                'input'    => 'div { background-image: linear-gradient(rgba(255, 255, 255, 0.3) 0%, #ff8 100%);}',
+                'reversable' => true,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should not negate color value for linear gradient with calc',
+                'expected' => 'div { background-image: linear-gradient(rgba(255, 255, calc((125 * 2) + 5), 0.3) 0%, #ff8 100%);}',
+                'input'    => 'div { background-image: linear-gradient(rgba(255, 255, calc((125 * 2) + 5), 0.3) 0%, #ff8 100%);}',
+                'reversable' => true,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should negate angle value for linear gradient',
+                'expected' => 'div { background-image: linear-gradient(13.25deg, rgba(255, 255, 255, .15) 25%, transparent 25%);}',
+                'input'    => 'div { background-image: linear-gradient(-13.25deg, rgba(255, 255, 255, .15) 25%, transparent 25%);}',
+                'reversable' => true,
+                'skip' => true
+            ]]
+            */
+        ];
+    }
+
+    /**
+     * Data provider.
+     * @return array
+     */
+    public function background_position_provider() {
+        return [
+            [[
+                'should' => 'Should complement percentage horizontal position ',
+                'expected' => 'div {background-position:100% 75%;}',
+                'input' => 'div {background-position:0 75%;}',
+                'reversable' => false
+            ]],
+            /* Not supported by MoodleHQ/RTLCSS yet.
+            [[
+                'should' => 'Should complement percentage horizontal position with calc',
+                'expected' => 'div {background-position:calc(100% - (30% + 50px)) 75%;}',
+                'input' => 'div {background-position:calc(30% + 50px) 75%;}',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should complement percentage horizontal position ',
+                'expected' => 'div {background-position:81.25% 75%, 10.75% top;}',
+                'input' => 'div {background-position:18.75% 75%, 89.25% top;}',
+                'reversable' => true,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should complement percentage horizontal position with calc',
+                'expected' => 'div {background-position:calc(100% - (30% + 50px)) calc(30% + 50px), 10.75% top;}',
+                'input' => 'div {background-position:calc(30% + 50px) calc(30% + 50px), 89.25% top;}',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should swap left with right',
+                'expected' => 'div {background-position:right 75%, left top;}',
+                'input' => 'div {background-position:left 75%, right top;}',
+                'reversable' => true,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should swap left with right wit calc',
+                'expected' => 'div {background-position:right -ms-calc(30% + 50px), left top;}',
+                'input' => 'div {background-position:left -ms-calc(30% + 50px), right top;}',
+                'reversable' => true,
+                'skip' => true
+            ]],
+            */
+            [[
+                'should' => 'Should complement percentage: position-x (treat 0 as 0%)',
+                'expected' => 'div {background-position-x:100%, 0%;}',
+                'input' => 'div {background-position-x:0, 100%;}',
+                'reversable' => false
+            ]],
+            [[
+                'should' => 'Should complement percentage: position-x',
+                'expected' => 'div {background-position-x:81.75%, 11%;}',
+                'input' => 'div {background-position-x:18.25%, 89%;}',
+                'reversable' => true
+            ]],
+            /* Not supported by MoodleHQ/RTLCSS yet.
+            [[
+                'should' => 'Should complement percentage with calc: position-x',
+                'expected' => 'div {background-position-x:calc(100% - (30% + 50px)), -webkit-calc(100% - (30% + 50px));}',
+                'input' => 'div {background-position-x:calc(30% + 50px), -webkit-calc(30% + 50px);}',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            */
+            [[
+                'should' => 'Should swap left with right: position-x',
+                'expected' => 'div {background-position-x:right, left;}',
+                'input' => 'div {background-position-x:left, right;}',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should keep as is: position-x',
+                'expected' => 'div {background-position-x:100px, 0px;}',
+                'input' => 'div {background-position-x:100px, 0px;}',
+                'reversable' => true
+            ]],
+
+            [[
+                'should' => 'Should flip when using 3 positions',
+                'expected' => 'div {background-position:center right 1px;}',
+                'input' => 'div {background-position:center left 1px;}',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should flip when using 4 positions',
+                'expected' => 'div {background-position:center 2px right 1px;}',
+                'input' => 'div {background-position:center 2px left 1px;}',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should flip when using 4 positions mixed',
+                'expected' => 'div {background-position:right 2px bottom 1px;}',
+                'input' => 'div {background-position:left 2px bottom 1px;}',
+                'reversable' => true
+            ]]
+        ];
+    }
+
+    /**
+     * Data provider.
+     * @return array
+     */
+    public function background_provider() {
+        return [
+            [[
+                'should' => 'Should treat 0 as 0%',
+                'expected' => '.banner { background: 100% top url("topbanner.png") #00d repeat-y fixed; }',
+                'input' => '.banner { background: 0 top url("topbanner.png") #00d repeat-y fixed; }',
+                'reversable' => false
+            ]],
+            [[
+                'should' => 'Should complement percentage horizontal position',
+                'expected' => '.banner { background: 81% top url("topbanner.png") #00d repeat-y fixed; }',
+                'input' => '.banner { background: 19% top url("topbanner.png") #00d repeat-y fixed; }',
+                'reversable' => true
+            ]],
+            /* Not supported by MoodleHQ/RTLCSS yet.
+            [[
+                'should' => 'Should complement calc horizontal position',
+                'expected' => '.banner { background: calc(100% - (19% + 2px)) top url(topbanner.png) #00d repeat-y fixed; }',
+                'input' => '.banner { background: calc(19% + 2px) top url(topbanner.png) #00d repeat-y fixed; }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            */
+            [[
+                'should' => 'Should mirror keyword horizontal position',
+                'expected' => '.banner { background: right top url("topbanner.png") #00d repeat-y fixed; }',
+                'input' => '.banner { background: left top url("topbanner.png") #00d repeat-y fixed; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should not process string map in url (default)',
+                'expected' => '.banner { background: 10px top url("ltr-top-right-banner.png") #00d repeat-y fixed; }',
+                'input' => '.banner { background: 10px top url("ltr-top-right-banner.png") #00d repeat-y fixed; }',
+                'reversable' => true
+            ]],
+            /* Not supported by MoodleHQ/RTLCSS yet.
+            [[
+                'should' => 'Should process string map in url (processUrls:true)',
+                'expected' => '.banner { background: 10px top url(rtl-top-left-banner.png) #00d repeat-y fixed; }',
+                'input' => '.banner { background: 10px top url(ltr-top-right-banner.png) #00d repeat-y fixed; }',
+                'reversable' => true,
+                'options' => [ 'processUrls' => true ],
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should process string map in url (processUrls:{decl:true})',
+                'expected' => '.banner { background: 10px top url(rtl-top-left-banner.png) #00d repeat-y fixed; }',
+                'input' => '.banner { background: 10px top url(ltr-top-right-banner.png) #00d repeat-y fixed; }',
+                'reversable' => true,
+                'options' => [ 'processUrls' => [ 'decl' => true ] ],
+                'skip' => true
+            ]],
+            */
+            [[
+                'should' => 'Should not process string map in url (processUrls:{atrule:true})',
+                'expected' => '.banner { background: 10px top url("ltr-top-right-banner.png") #00d repeat-y fixed; }',
+                'input' => '.banner { background: 10px top url("ltr-top-right-banner.png") #00d repeat-y fixed; }',
+                'reversable' => true,
+                'options' => [ 'processUrls' => [ 'atrule' => true ] ]
+            ]],
+            [[
+                'should' => 'Should not swap bright:bleft, ultra:urtla',
+                'expected' => '.banner { background: 10px top url("ultra/bright.png") #00d repeat-y fixed; }',
+                'input' => '.banner { background: 10px top url("ultra/bright.png") #00d repeat-y fixed; }',
+                'reversable' => true
+            ]],
+            /* Not supported by MoodleHQ/RTLCSS yet.
+            [[
+                'should' => 'Should swap bright:bleft, ultra:urtla (processUrls: true, greedy)',
+                'expected' => '.banner { background: 10px top url("urtla/bleft.png") #00d repeat-y fixed; }',
+                'input' => '.banner { background: 10px top url("ultra/bright.png") #00d repeat-y fixed; }',
+                'reversable' => true,
+                'options' => [ 'processUrls' => true, 'greedy' => true ],
+                'skip' => true
+            ]],
+            */
+            [[
+                'should' => 'Should not flip hex colors ',
+                'expected' => '.banner { background: #ff0; }',
+                'input' => '.banner { background: #ff0; }',
+                'reversable' => true
+            ]]
+        ];
+    }
+
+    /**
+     * Data provider.
+     * @return array
+     */
+    public function directives_provider() {
+        return [
+            [[
+                'should' => 'Should ignore flipping - rule level (default)',
+                'expected' => 'div {left:10px;text-align:right;}',
+                'input' => '/*rtl:ignore*/div { left:10px; text-align:right;}',
+                'reversable' => false
+            ]],
+            [[
+                'should' => 'Should ignore flipping - rule level (!important comment)',
+                'expected' => 'div {left:10px;text-align:right;}',
+                'input' => '/*!rtl:ignore*/div { left:10px; text-align:right;}',
+                'reversable' => false,
+            ]],
+            // Not supported by MoodleHQ/RTLCSS yet.
+            //[[
+            //    'should' => 'Should ignore flipping - decl. level (default)',
+            //    'expected' => 'div {left:10px;text-align:left;}',
+            //    'input' => 'div {left:10px/*rtl:ignore*/;text-align:right;}',
+            //    'reversable' => false,
+            //    'skip' => true
+            //]],
+            [[
+                'should' => 'Should add raw css rules',
+                'expected' => 'div {left:10px;text-align:right;} a {display:block;}',
+                'input' => '/*rtl:raw: div { left:10px;text-align:right;}*/ a {display:block;}',
+                'reversable' => false
+            ]],
+            [[
+                'should' => 'Should add raw css declarations',
+                'expected' => 'div {font-family:"Droid Kufi Arabic";right:10px;text-align:left;}',
+                'input' => 'div { /*rtl:raw: font-family: "Droid Kufi Arabic";*/ left:10px;text-align:right;}',
+                'reversable' => false
+            ]],
+            [[
+                'should' => 'Should support block-style',
+                'expected' => 'div {left:10px;text-align:right;}',
+                'input' => ' div {/*rtl:begin:ignore*/left:10px;/*rtl:end:ignore*/ text-align:left;}',
+                'reversable' => false
+            ]],
+            [[
+                'should' => 'Should support none block-style',
+                'expected' => 'div {left:10px;text-align:left;}',
+                'input' => ' /*rtl:ignore*/div {left:10px; text-align:left;}',
+                'reversable' => false
+            ]],
+            [[
+                'should' => 'Should remove rules (block-style)',
+                'expected' => 'b {float:right;}',
+                'input' => ' /*rtl:begin:remove*/div {left:10px; text-align:left;} a { display:block;} /*rtl:end:remove*/ b{float:left;}',
+                'reversable' => false
+            ]],
+            [[
+                'should' => 'Should remove rules',
+                'expected' => 'a {display:block;} b {float:right;}',
+                'input' => ' /*rtl:remove*/div {left:10px; text-align:left;} a { display:block;} b{float:left;}',
+                'reversable' => false
+            ]],
+            [[
+                'should' => 'Should remove declarations',
+                'expected' => 'div {text-align:right;}',
+                'input' => 'div {/*rtl:remove*/left:10px; text-align:left;}',
+                'reversable' => false
+            ]],
+            [[
+                'should' => 'Should remove declarations (block-style)',
+                'expected' => 'div {display:inline;}',
+                'input' => 'div {/*rtl:begin:remove*/left:10px; text-align:left;/*rtl:end:remove*/ display:inline;}',
+                'reversable' => false
+            ]],
+            // Not supported by MoodleHQ/RTLCSS yet.
+            //[[
+            //    'should' => 'Final/trailing comment ignored bug (block style): note a tag rules are NOT flipped as they should be',
+            //    'expected' => 'div {left:10px;text-align:left;} a {right:10px;}',
+            //    'input' => 'div {/*rtl:begin:ignore*/left:10px; text-align:left;/*rtl:end:ignore*/} a {left:10px;}',
+            //    'reversable' => false,
+            //    'skip' => true
+            //]]
+        ];
+    }
+
+    /**
+     * Data provider.
+     * @return array
+     */
+    public function properties_provider() {
+        return [
+            [[
+                'should' => 'Should mirror property name: border-top-right-radius',
+                'expected' => 'div { border-top-left-radius:15px; }',
+                'input' => 'div { border-top-right-radius:15px; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should mirror property name: border-bottom-right-radius',
+                'expected' => 'div { border-bottom-left-radius:15px; }',
+                'input' => 'div { border-bottom-right-radius:15px; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should mirror property name: border-left',
+                'expected' => 'div { border-right:1px solid black; }',
+                'input' => 'div { border-left:1px solid black; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should mirror property name: border-left-color',
+                'expected' => 'div { border-right-color:black; }',
+                'input' => 'div { border-left-color:black; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should mirror property name: border-left-style',
+                'expected' => 'div { border-right-style:solid; }',
+                'input' => 'div { border-left-style:solid; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should mirror property name: border-left-width',
+                'expected' => 'div { border-right-width:1em; }',
+                'input' => 'div { border-left-width:1em; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should mirror property name: left',
+                'expected' => 'div { right:auto; }',
+                'input' => 'div { left:auto; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should mirror property name: margin-left',
+                'expected' => 'div { margin-right:2em; }',
+                'input' => 'div { margin-left:2em; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should mirror property name: padding-left',
+                'expected' => 'div { padding-right:2em; }',
+                'input' => 'div { padding-left:2em; }',
+                'reversable' => true
+            ]]
+        ];
+    }
+
+    /**
+     * Data provider.
+     * @return array
+     */
+    public function special_provider() {
+        return [
+            /* Not supported by MoodleHQ/RTLCSS yet.
+            [[
+                'should' => 'Should not negate tokens',
+                'expected' => 'div { box-shadow: rgba(0, 128, 128, .98) inset -5em 1em 0;}',
+                'input' => 'div { box-shadow: rgba(0, 128, 128, .98) inset 5em 1em 0;}',
+                'reversable' => true,
+                'skip' => true,
+            ]]
+            */
+        ];
+    }
+
+    /**
+     * Data provider.
+     * @return array
+     */
+    public function transform_origin_provider() {
+        return [
+            [[
+                'should' => 'Should mirror (x-offset: 0 means 0%)',
+                'expected' => 'div { transform-origin:100%; }',
+                'input' => 'div { transform-origin:0; }',
+                'reversable' => false
+            ]],
+            [[
+                'should' => 'Should mirror (x-offset)',
+                'expected' => 'div { transform-origin:90.25%; }',
+                'input' => 'div { transform-origin:9.75%; }',
+                'reversable' => true
+            ]],
+            /* Not supported by MoodleHQ/RTLCSS yet.
+            [[
+                'should' => 'Should mirror calc (x-offset)',
+                'expected' => 'div { transform-origin: -moz-calc(100% - (((25%/2) * 10px))); }',
+                'input' => 'div { transform-origin: -moz-calc(((25%/2) * 10px)); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            */
+            [[
+                'should' => 'Should not mirror (x-offset: not percent, not calc)',
+                'expected' => 'div { transform-origin:10.75px; }',
+                'input' => 'div { transform-origin:10.75px; }',
+                'reversable' => false
+            ]],
+            [[
+                'should' => 'Should mirror (offset-keyword)',
+                'expected' => 'div { transform-origin:right; }',
+                'input' => 'div { transform-origin:left; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should mirror (x-offset y-offset: 0 means 0%)',
+                'expected' => 'div { transform-origin:100% 0; }',
+                'input' => 'div { transform-origin:0 0; }',
+                'reversable' => false
+            ]],
+            /* Not supported by MoodleHQ/RTLCSS yet.
+            [[
+                'should' => 'Should mirror with y being calc (x-offset y-offset: 0 means 0%)',
+                'expected' => 'div { transform-origin:100% -webkit-calc(15% * (3/2)); }',
+                'input' => 'div { transform-origin:0 -webkit-calc(15% * (3/2)); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            */
+            [[
+                'should' => 'Should mirror percent (x-offset y-offset)',
+                'expected' => 'div { transform-origin:30.25% 10%; }',
+                'input' => 'div { transform-origin:69.75% 10%; }',
+                'reversable' => true
+            ]],
+            /* Not supported by MoodleHQ/RTLCSS yet.
+            [[
+                'should' => 'Should mirror with x being calc (x-offset y-offset)',
+                'expected' => 'div { transform-origin: -webkit-calc(100% - (15% * (3/2))) 30.25%; }',
+                'input' => 'div { transform-origin: -webkit-calc(15% * (3/2)) 30.25%; }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror with y being calc (x-offset y-offset)',
+                'expected' => 'div { transform-origin:30.25% calc(15% * (3/2)); }',
+                'input' => 'div { transform-origin:69.75% calc(15% * (3/2)); }',
+                'reversable' => true,
+                'skip' => true
+            ]],
+            */
+            [[
+                'should' => 'Should mirror (y-offset x-offset-keyword)',
+                'expected' => 'div { transform-origin:70% right; }',
+                'input' => 'div { transform-origin:70% left; }',
+                'reversable' => true
+            ]],
+            /* Not supported by MoodleHQ/RTLCSS yet.
+            [[
+                'should' => 'Should mirror with calc (y-offset x-offset-keyword)',
+                'expected' => 'div { transform-origin:-ms-calc(140%/2) right; }',
+                'input' => 'div { transform-origin:-ms-calc(140%/2) left; }',
+                'reversable' => true,
+                'skip' => true
+            ]],
+            */
+            [[
+                'should' => 'Should mirror (x-offset-keyword y-offset)',
+                'expected' => 'div { transform-origin:right 70%; }',
+                'input' => 'div { transform-origin:left 70%; }',
+                'reversable' => true
+            ]],
+            /* Not supported by MoodleHQ/RTLCSS yet.
+            [[
+                'should' => 'Should mirror with calc (x-offset-keyword y-offset)',
+                'expected' => 'div { transform-origin:right -moz-calc(((140%/2))); }',
+                'input' => 'div { transform-origin:left -moz-calc(((140%/2))); }',
+                'reversable' => true,
+                'skip' => true
+            ]],
+            */
+            [[
+                'should' => 'Should mirror (y-offset-keyword x-offset)',
+                'expected' => 'div { transform-origin:top 30.25%; }',
+                'input' => 'div { transform-origin:top 69.75%; }',
+                'reversable' => true
+            ]],
+            /* Not supported by MoodleHQ/RTLCSS yet.
+            [[
+                'should' => 'Should not mirror with x being calc (y-offset-keyword x-offset)',
+                'expected' => 'div { transform-origin:top calc(100% - (((140%/2)))); }',
+                'input' => 'div { transform-origin:top calc(((140%/2))); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            */
+            [[
+                'should' => 'Should mirror (x-offset-keyword y-offset-keyword)',
+                'expected' => 'div { transform-origin:right top; }',
+                'input' => 'div { transform-origin:left top; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should mirror (y-offset-keyword x-offset-keyword)',
+                'expected' => 'div { transform-origin:top right; }',
+                'input' => 'div { transform-origin:top left; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should mirror (x-offset y-offset z-offset)',
+                'expected' => 'div { transform-origin:80.25% 30% 10%; }',
+                'input' => 'div { transform-origin:19.75% 30% 10%; }',
+                'reversable' => true
+            ]],
+            /* Not supported by MoodleHQ/RTLCSS yet.
+            [[
+                'should' => 'Should mirror with x being calc (x-offset y-offset z-offset)',
+                'expected' => 'div { transform-origin: calc(100% - (25% * 3 + 20px)) 30% 10%; }',
+                'input' => 'div { transform-origin: calc(25% * 3 + 20px) 30% 10%; }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            */
+            [[
+                'should' => 'Should mirror (y-offset x-offset-keyword z-offset)',
+                'expected' => 'div { transform-origin:20% right 10%; }',
+                'input' => 'div { transform-origin:20% left 10%; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should mirror (x-offset-keyword y-offset z-offset)',
+                'expected' => 'div { transform-origin:left 20% 10%; }',
+                'input' => 'div { transform-origin:right 20% 10%; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should mirror (x-offset-keyword y-offset-keyword z-offset)',
+                'expected' => 'div { transform-origin:left bottom 10%; }',
+                'input' => 'div { transform-origin:right bottom 10%; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should mirror (y-offset-keyword x-offset-keyword z-offset)',
+                'expected' => 'div { transform-origin:bottom left 10%; }',
+                'input' => 'div { transform-origin:bottom right 10%; }',
+                'reversable' => true
+            ]]
+        ];
+    }
+
+    /**
+     * Data provider.
+     * @return array
+     */
+    public function transforms_provider() {
+        return [
+            /* Not supported by MoodleHQ/RTLCSS yet.
+            [[
+                'should' => 'Should mirror transform : matrix',
+                'expected' => 'div { transform: matrix(2, 0.1, 20.75, 2, 2, 2); }',
+                'input' => 'div { transform: matrix(2, -0.1, -20.75, 2, -2, 2); }',
+                'reversable' => true,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform (with no digits before dot): matrix',
+                'expected' => 'div { transform: matrix(2, 0.1, 0.75, 2, 2, 2); }',
+                'input' => 'div { transform: matrix(2, -0.1, -.75, 2, -2, 2); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform with calc: matrix',
+                'expected' => 'div { transform: matrix( -moz-calc(((25%/2) * 10px)), calc(-1*(((25%/2) * 10px))), 20.75, 2, 2, 2 ); }',
+                'input' => 'div { transform: matrix( -moz-calc(((25%/2) * 10px)), calc(((25%/2) * 10px)), -20.75, 2, -2, 2 ); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform : matrix3d',
+                'expected' => 'div { transform:matrix3d(0.227114470162179, 0.127248412323519, 0, 0.000811630714323203, 0.113139853456515, 1.53997196559414, 0, 0.000596368270149729, 0, 0, 1, 0, -165, 67, 0, 1); }',
+                'input' => 'div { transform:matrix3d(0.227114470162179, -0.127248412323519, 0, -0.000811630714323203, -0.113139853456515, 1.53997196559414, 0, 0.000596368270149729, 0, 0, 1, 0, 165, 67, 0, 1); }',
+                'reversable' => true,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform (with no digits before dot): matrix3d',
+                'expected' => 'div { transform:matrix3d(0.227114470162179, 0.127248412323519, 0, 0.000811630714323203, 0.113139853456515, 1.53997196559414, 0, 0.000596368270149729, 0, 0, 1, 0, -165, 67, 0, 1); }',
+                'input' => 'div { transform:matrix3d(0.227114470162179, -.127248412323519, 0, -0.000811630714323203, -0.113139853456515, 1.53997196559414, 0, 0.000596368270149729, 0, 0, 1, 0, 165, 67, 0, 1); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform with calc : matrix3d',
+                'expected' => 'div { transform:matrix3d(0.227114470162179, 0.127248412323519, 0, 0.000811630714323203, 0.113139853456515, 1.53997196559414, 0, 0.000596368270149729, 0, 0, 1, 0, calc(-1*(((25%/2) * 10px))), 67, 0, 1); }',
+                'input' => 'div { transform:matrix3d(0.227114470162179, -0.127248412323519, 0, -0.000811630714323203, -0.113139853456515, 1.53997196559414, 0, 0.000596368270149729, 0, 0, 1, 0, calc(((25%/2) * 10px)), 67, 0, 1); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform : translate',
+                'expected' => 'div { transform: translate(-10.75px); }',
+                'input' => 'div { transform: translate(10.75px); }',
+                'reversable' => true,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform (with no digits before dot): translate',
+                'expected' => 'div { transform: translate(-0.75px); }',
+                'input' => 'div { transform: translate(.75px); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform with calc: translate',
+                'expected' => 'div { transform: translate(-moz-calc(-1*(((25%/2) * 10px)))); }',
+                'input' => 'div { transform: translate(-moz-calc(((25%/2) * 10px))); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform : translateX',
+                'expected' => 'div { transform: translateX(-50.25px); }',
+                'input' => 'div { transform: translateX(50.25px); }',
+                'reversable' => true,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform (with no digits before dot): translateX',
+                'expected' => 'div { transform: translateX(-0.25px); }',
+                'input' => 'div { transform: translateX(.25px); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform with calc : translateX',
+                'expected' => 'div { transform: translateX(-ms-calc(-1*(((25%/2) * 10px))))); }',
+                'input' => 'div { transform: translateX(-ms-calc(((25%/2) * 10px)))); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform : translate3d',
+                'expected' => 'div { transform: translate3d(-12.75px, 50%, 3em); }',
+                'input' => 'div { transform: translate3d(12.75px, 50%, 3em); }',
+                'reversable' => true,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform (with no digits before dot): translate3d',
+                'expected' => 'div { transform: translate3d(-0.75px, 50%, 3em); }',
+                'input' => 'div { transform: translate3d(.75px, 50%, 3em); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform with calc: translate3d',
+                'expected' => 'div { transform: translate3d(-webkit-calc(-1*(((25%/2) * 10px))))), 50%, calc(((25%/2) * 10px))))); }',
+                'input' => 'div { transform: translate3d(-webkit-calc(((25%/2) * 10px)))), 50%, calc(((25%/2) * 10px))))); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform : rotate',
+                'expected' => 'div { transform: rotate(-20.75deg); }',
+                'input' => 'div { transform: rotate(20.75deg); }',
+                'reversable' => true,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform (with no digits before dot): rotate',
+                'expected' => 'div { transform: rotate(-0.75deg); }',
+                'input' => 'div { transform: rotate(.75deg); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform with calc: rotate',
+                'expected' => 'div { transform: rotate(calc(-1*(((25%/2) * 10deg)))); }',
+                'input' => 'div { transform: rotate(calc(((25%/2) * 10deg))); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform : rotate3d',
+                'expected' => 'div { transform: rotate3d(10, -20.15, 10, -45.14deg); }',
+                'input' => 'div { transform: rotate3d(10, 20.15, 10, 45.14deg); }',
+                'reversable' => true,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform (with no digits before dot): rotate3d',
+                'expected' => 'div { transform: rotate3d(10, -20, 10, -0.14deg); }',
+                'input' => 'div { transform: rotate3d(10, 20, 10, .14deg); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform with calc: rotate3d',
+                'expected' => 'div { transform: rotate3d(10, -20.15, 10, calc(-1*(((25%/2) * 10deg)))); }',
+                'input' => 'div { transform: rotate3d(10, 20.15, 10, calc(((25%/2) * 10deg))); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should not mirror transform : rotateX',
+                'expected' => 'div { transform: rotateX(45deg); }',
+                'input' => 'div { transform: rotateX(45deg); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should not mirror transform with calc: rotateX',
+                'expected' => 'div { transform: rotateX(calc(((25%/2) * 10deg))); }',
+                'input' => 'div { transform: rotateX(calc(((25%/2) * 10deg))); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should not mirror transform : rotateY',
+                'expected' => 'div { transform: rotateY(45deg); }',
+                'input' => 'div { transform: rotateY(45deg); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should not mirror transform with calc: rotateY',
+                'expected' => 'div { transform: rotateY(calc(((25%/2) * 10deg))); }',
+                'input' => 'div { transform: rotateY(calc(((25%/2) * 10deg))); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform : rotateZ',
+                'expected' => 'div { transform: rotateZ(-45.75deg); }',
+                'input' => 'div { transform: rotateZ(45.75deg); }',
+                'reversable' => true,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform (with no digits before dot): rotateZ',
+                'expected' => 'div { transform: rotateZ(-0.75deg); }',
+                'input' => 'div { transform: rotateZ(.75deg); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform with calc: rotateZ',
+                'expected' => 'div { transform: rotateZ(-ms-calc(-1*(((25%/2) * 10deg)))); }',
+                'input' => 'div { transform: rotateZ(-ms-calc(((25%/2) * 10deg))); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform : skew',
+                'expected' => 'div { transform: skew(-20.25rad,-30deg); }',
+                'input' => 'div { transform: skew(20.25rad,30deg); }',
+                'reversable' => true,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform (with no digits before dot): skew',
+                'expected' => 'div { transform: skew(-0.25rad,-30deg); }',
+                'input' => 'div { transform: skew(.25rad,30deg); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform with calc: skew',
+                'expected' => 'div { transform: skew(calc(-1*(((25%/2) * 10rad))),calc(-1*(((25%/2) * 10deg)))); }',
+                'input' => 'div { transform: skew(calc(((25%/2) * 10rad)),calc(((25%/2) * 10deg))); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform : skewX',
+                'expected' => 'div { transform: skewX(-20.75rad); }',
+                'input' => 'div { transform: skewX(20.75rad); }',
+                'reversable' => true,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform (with no digits before dot): skewX',
+                'expected' => 'div { transform: skewX(-0.75rad); }',
+                'input' => 'div { transform: skewX(.75rad); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform with calc: skewX',
+                'expected' => 'div { transform: skewX(-moz-calc(-1*(((25%/2) * 10rad)))); }',
+                'input' => 'div { transform: skewX(-moz-calc(((25%/2) * 10rad))); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform : skewY',
+                'expected' => 'div { transform: skewY(-10.75grad); }',
+                'input' => 'div { transform: skewY(10.75grad); }',
+                'reversable' => true,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform (with no digits before dot): skewY',
+                'expected' => 'div { transform: skewY(-0.75grad); }',
+                'input' => 'div { transform: skewY(.75grad); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror transform with calc: skewY',
+                'expected' => 'div { transform: skewY(calc(-1*(((25%/2) * 10grad)))); }',
+                'input' => 'div { transform: skewY(calc(((25%/2) * 10grad))); }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror multiple transforms : translateX translateY Rotate',
+                'expected' => 'div { transform: translateX(-50.25px) translateY(50.25px) rotate(-20.75deg); }',
+                'input' => 'div { transform: translateX(50.25px) translateY(50.25px) rotate(20.75deg); }',
+                'reversable' => true,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror multiple transforms with calc : translateX translateY Rotate',
+                'expected' => 'div { transform: translateX(-ms-calc(-1*(((25%/2) * 10px)))) translateY(-moz-calc(((25%/2) * 10rad))) rotate(calc(-1*(((25%/2) * 10grad)))); }',
+                'input' => 'div { transform: translateX(-ms-calc(((25%/2) * 10px))) translateY(-moz-calc(((25%/2) * 10rad))) rotate(calc(((25%/2) * 10grad))); }',
+                'reversable' => false,
+                'skip' => true
+            ]]
+            */
+        ];
+    }
+
+    /**
+     * Data provider.
+     * @return array
+     */
+    public function values_nsyntax_provider() {
+        return [
+            [[
+                'should' => 'Should mirror property value: border-radius (4 values)',
+                'expected' => 'div { border-radius: 40.25px 10.5px 10.75px 40.3px; }',
+                'input' => 'div { border-radius: 10.5px 40.25px 40.3px 10.75px; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should mirror property value: border-radius (3 values)',
+                'expected' => 'div { border-radius: 40.75px 10.75px 40.75px 40.3px; }',
+                'input' => 'div { border-radius: 10.75px 40.75px 40.3px; }',
+                'reversable' => false
+            ]],
+            [[
+                'should' => 'Should mirror property value: border-radius (2 values)',
+                'expected' => 'div { border-radius: 40.25px 10.75px; }',
+                'input' => 'div { border-radius: 10.75px 40.25px; }',
+                'reversable' => true
+            ]],
+            /* Not supported by MoodleHQ/RTLCSS yet.
+            [[
+                'should' => 'Should mirror property value: border-radius (4 values - double)',
+                'expected' => 'div { border-radius: 40.25px 10.75px .5px 40.75px / .4em 1em 1em 4.5em; }',
+                'input' => 'div { border-radius: 10.75px 40.25px 40.75px .5px / 1em .4em 4.5em 1em; }',
+                'reversable' => true,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror property value: border-radius (3 values - double)',
+                'expected' => 'div { border-radius: .40px 10.5px .40px 40px / 4em 1em 4em 3em; }',
+                'input' => 'div { border-radius: 10.5px .40px 40px / 1em 4em 3em; }',
+                'reversable' => false,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror property value: border-radius (2 values- double)',
+                'expected' => 'div { border-radius: 40px 10px / 2.5em .75em; }',
+                'input' => 'div { border-radius: 10px 40px / .75em 2.5em; }',
+                'reversable' => true,
+                'skip' => true
+            ]],
+            */
+            [[
+                'should' => 'Should mirror property value: border-width',
+                'expected' => 'div { border-width: 1px 4px .3em 2.5em; }',
+                'input' => 'div { border-width: 1px 2.5em .3em 4px; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should mirror property value: border-width (none length)',
+                'expected' => 'div { border-width: thin medium thick none; }',
+                'input' => 'div { border-width: thin none thick medium; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should mirror property value: border-style (4 values)',
+                'expected' => 'div { border-style: none dashed dotted solid; }',
+                'input' => 'div { border-style: none solid dotted dashed; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should mirror property value: border-color (4 values)',
+                'expected' => 'div { border-color: rgba(255, 255, 255, 1) rgb(0, 0, 0) rgb(0, 0, 0) hsla(0, 100%, 50%, 1); }',
+                'input' => 'div { border-color: rgba(255, 255, 255, 1) hsla(0, 100%, 50%, 1) rgb(0, 0, 0) rgb(0, 0, 0); }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should not mirror property value: border-color (3 values)',
+                'expected' => 'div { border-color: rgb(0, 0, 0) rgb(0, 0, 0) hsla(0, 100%, 50%, 1); }',
+                'input' => 'div { border-color: #000 rgb(0, 0, 0) hsla(0, 100%, 50%, 1); }',
+                'reversable' => false
+            ]],
+            [[
+                'should' => 'Should not mirror property value: border-color (2 values)',
+                'expected' => 'div { border-color: rgb(0, 0, 0) hsla(0, 100%, 50%, 1); }',
+                'input' => 'div { border-color: rgb(0, 0, 0) hsla(0, 100%, 50%, 1); }',
+                'reversable' => false
+            ]],
+            [[
+                'should' => 'Should mirror property value: margin',
+                'expected' => 'div { margin: .1em auto 3.5rem 2px; }',
+                'input' => 'div { margin: .1em 2px 3.5rem auto; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should mirror property value: padding',
+                'expected' => 'div { padding: 1px 4px .3rem 2.5em; }',
+                'input' => 'div { padding: 1px 2.5em .3rem 4px; }',
+                'reversable' => true
+            ]],
+            /* Not supported by MoodleHQ/RTLCSS yet.
+            [[
+                'should' => 'Should mirror property value: box-shadow',
+                'expected' => 'div { box-shadow: -60px -16px rgba(0, 128, 128, 0.98), -10.25px 5px 5px #ff0, inset -0.5em 1em 0 white; }',
+                'input' => 'div { box-shadow: 60px -16px rgba(0, 128, 128, 0.98), 10.25px 5px 5px #ff0, inset 0.5em 1em 0 white; }',
+                'reversable' => true,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror property value: text-shadow',
+                'expected' => 'div { text-shadow: -60px -16px rgba(0, 128, 128, 0.98), -10.25px 5px 5px #ff0, inset -0.5em 1em 0 white; }',
+                'input' => 'div { text-shadow: 60px -16px rgba(0, 128, 128, 0.98), 10.25px 5px 5px #ff0, inset 0.5em 1em 0 white; }',
+                'reversable' => true,
+                'skip' => true
+            ]],
+            [[
+                'should' => 'Should mirror property value (no digit before the dot): box-shadow, text-shadow',
+                'expected' => 'div { box-shadow: inset -0.5em 1em 0 white; text-shadow: inset -0.5em 1em 0 white; }',
+                'input' => 'div { box-shadow: inset .5em 1em 0 white; text-shadow: inset .5em 1em 0 white; }',
+                'reversable' => false,
+                'skip' => true
+            ]]
+            */
+        ];
+    }
+
+    /**
+     * Data provider.
+     * @return array
+     */
+    public function values_provider() {
+        return [
+            [[
+                'should' => 'Should mirror property value: clear',
+                'expected' => 'div { clear:right; }',
+                'input' => 'div { clear:left; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should mirror property value: direction',
+                'expected' => 'div { direction:ltr; }',
+                'input' => 'div { direction:rtl; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should mirror property value: float',
+                'expected' => 'div { float:right; }',
+                'input' => 'div { float:left; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should mirror property value: text-align',
+                'expected' => 'div { text-align:right; }',
+                'input' => 'div { text-align:left; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should mirror property value: cursor nw',
+                'expected' => 'div { cursor:nw-resize; }',
+                'input' => 'div { cursor:ne-resize; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should mirror property value: cursor sw',
+                'expected' => 'div { cursor:sw-resize; }',
+                'input' => 'div { cursor:se-resize; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should mirror property value: cursor nesw',
+                'expected' => 'div { cursor:nesw-resize; }',
+                'input' => 'div { cursor:nwse-resize; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should keep property value as is: cursor ew',
+                'expected' => 'div { cursor:ew-resize; }',
+                'input' => 'div { cursor:ew-resize; }',
+                'reversable' => false
+            ]],
+            /* Not supported by MoodleHQ/RTLCSS yet.
+            [[
+                'should' => 'Should process string map in url: cursor (processUrls: true)',
+                'expected' => '.foo { cursor: url(right.cur), url(rtl.cur), se-resize, auto }',
+                'input' => '.foo { cursor: url(left.cur), url(ltr.cur), sw-resize, auto }',
+                'reversable' => true,
+                'options' => [ 'processUrls' => true ],
+                'skip' => true
+            ]],
+            */
+            [[
+                'should' => 'Should mirror property value: transition',
+                'expected' => '.foo { transition:right .3s ease .1s,left .3s ease .1s,margin-right .3s ease,margin-left .3s ease,padding-right .3s ease,padding-left .3s ease; }',
+                'input' => '.foo { transition:left .3s ease .1s,right .3s ease .1s,margin-left .3s ease,margin-right .3s ease,padding-left .3s ease,padding-right .3s ease; }',
+                'reversable' => true
+            ]],
+            [[
+                'should' => 'Should mirror property value: transition-property',
+                'expected' => '.foo { transition-property:right; }',
+                'input' => '.foo { transition-property:left; }',
+                'reversable' => true
+            ]]
+        ];
+    }
+
+    /**
+     * Assert that the provided data flips.
+     *
+     * @param string $expected The expected output.
+     * @param string $input The input.
+     * @param string $description The description of the assertion.
+     * @param OutputFormat $output The output format to use.
+     */
+    protected function assert_flips($expected, $input, $description, $output = null) {
+        $parser = new Parser($input);
+        $tree = $parser->parse();
+        $rtlcss = new core_rtlcss($tree);
+        $flipped = $rtlcss->flip();
+        $this->assertEquals($expected, $flipped->render($output), $description);
+    }
+
+    /**
+     * Assert data.
+     *
+     * @param array $data With the keys: 'input', 'expected', 'reversable', 'should', and 'skip'.
+     * @param OutputFormat $output The output format to use.
+     */
+    protected function assert_sample($data, $output = null) {
+        if (!empty($data['skip'])) {
+            $this->markTestSkipped('Not yet supported!');
+        }
+        $this->assert_flips($data['expected'], $data['input'], $data['should'], $output);
+        if (!empty($data['reversable'])) {
+            $this->assert_flips($data['input'], $data['expected'], $data['should'] . ' (reversed)', $output);
+        }
+    }
+
+    /**
+     * Test background images.
+     * @param array $data the provider data.
+     * @dataProvider background_image_provider
+     */
+    /* Not supported by MoodleHQ/RTLCSS yet.
+    public function test_background_image($data) {
+        $output = new OutputFormat();
+        $this->assert_sample($data, $output);
+    }
+    */
+
+    /**
+     * Test background position.
+     * @param array $data the provider data.
+     * @dataProvider background_position_provider
+     */
+    public function test_background_position($data) {
+        $output = new OutputFormat();
+        $output->set('SpaceAfterRuleName', '');
+        $output->set('SpaceAfterListArgumentSeparator', array('default' => '', ',' => ' '));
+        $this->assert_sample($data, $output);
+    }
+
+    /**
+     * Test background.
+     * @param array $data the provider data.
+     * @dataProvider background_provider
+     */
+    public function test_background($data) {
+        $output = new OutputFormat();
+        $output->set('SpaceAfterRuleName', ' ');
+        $output->set('SpaceBeforeRules', ' ');
+        $output->set('SpaceAfterRules', ' ');
+        $output->set('SpaceAfterListArgumentSeparator', array('default' => '', ',' => ' '));
+        $this->assert_sample($data, $output);
+    }
+
+    /**
+     * Test directives.
+     * @param array $data the provider data.
+     * @dataProvider directives_provider
+     */
+    public function test_directives($data) {
+        $output = new OutputFormat();
+        $output->set('SpaceAfterRuleName', '');
+        $output->set('SpaceBeforeRules', '');
+        $output->set('SpaceAfterRules', '');
+        $output->set('SpaceBetweenRules', '');
+        $output->set('SpaceBetweenBlocks', ' ');
+        $output->set('SpaceAfterListArgumentSeparator', array('default' => '', ',' => ' '));
+        $this->assert_sample($data, $output);
+    }
+
+    /**
+     * Test properties.
+     * @param array $data the provider data.
+     * @dataProvider properties_provider
+     */
+    public function test_properties($data) {
+        $output = new OutputFormat();
+        $output->set('SpaceAfterRuleName', '');
+        $output->set('SpaceBeforeRules', ' ');
+        $output->set('SpaceAfterRules', ' ');
+        $output->set('SpaceAfterListArgumentSeparator', array('default' => '', ',' => ' '));
+        $this->assert_sample($data, $output);
+    }
+
+    /**
+     * Test special.
+     * @param array $data the provider data.
+     * @dataProvider special_provider
+     */
+    /* Not supported by MoodleHQ/RTLCSS yet.
+    public function test_special($data) {
+        $output = new OutputFormat();
+        $output->set('SpaceBeforeRules', ' ');
+        $output->set('SpaceAfterListArgumentSeparator', array('default' => '', ',' => ' '));
+        $this->assert_sample($data, $output);
+    }
+    */
+
+    /**
+     * Test transform original.
+     * @param array $data the provider data.
+     * @dataProvider transform_origin_provider
+     */
+    public function test_transform_origin($data) {
+        $output = new OutputFormat();
+        $output->set('SpaceAfterRuleName', '');
+        $output->set('SpaceBeforeRules', ' ');
+        $output->set('SpaceAfterRules', ' ');
+        $output->set('SpaceAfterListArgumentSeparator', array('default' => '', ',' => ' '));
+        $this->assert_sample($data, $output);
+    }
+
+
+    /**
+     * Test transform.
+     * @param array $data the provider data.
+     * @dataProvider transforms_provider
+     */
+    /* Not supported by MoodleHQ/RTLCSS yet.
+    public function test_transforms($data) {
+        $output = new OutputFormat();
+        $output->set('SpaceBeforeRules', ' ');
+        $output->set('SpaceAfterRules', ' ');
+        $output->set('SpaceAfterListArgumentSeparator', array('default' => '', ',' => ' '));
+        $this->assert_sample($data, $output);
+    }
+    */
+
+    /**
+     * Test values n-syntax.
+     * @param array $data the provider data.
+     * @dataProvider values_nsyntax_provider
+     */
+    public function test_values_nsyntax($data) {
+        $output = new OutputFormat();
+        $output->set('SpaceBeforeRules', ' ');
+        $output->set('SpaceAfterRules', ' ');
+        $output->set('RGBHashNotation', false);
+        $output->set('SpaceAfterListArgumentSeparator', array('default' => '', ',' => ' '));
+        $this->assert_sample($data, $output);
+    }
+
+    /**
+     * Test values.
+     * @param array $data the provider data.
+     * @dataProvider values_provider
+     */
+    public function test_values($data) {
+        $output = new OutputFormat();
+        $output->set('SpaceAfterRuleName', '');
+        $output->set('SpaceBeforeRules', ' ');
+        $output->set('SpaceAfterRules', ' ');
+        $output->set('SpaceAfterListArgumentSeparator', array('default' => '', ',' => ' '));
+        $this->assert_sample($data, $output);
+    }
+}
index d3895a5..4e81345 100644 (file)
@@ -525,6 +525,18 @@ EXPECTED;
                 'email' => "moodle@example.com>\r\nRCPT TO:<victim@example.com",
                 'result' => false
             ],
+            [
+                'email' => 'greater>than@example.com',
+                'result' => false
+            ],
+            [
+                'email' => 'less<than@example.com',
+                'result' => false
+            ],
+            [
+                'email' => '"this<is>validbutwerejectit"@example.com',
+                'result' => false
+            ],
 
             // Extra email addresses from Wikipedia page on Email Addresses.
             // Valid.
index 095caaf..f786ba9 100644 (file)
@@ -1110,7 +1110,7 @@ function validate_email($address) {
     global $CFG;
     require_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php');
 
-    return moodle_phpmailer::validateAddress($address);
+    return moodle_phpmailer::validateAddress($address) && !preg_match('/[<>]/', $address);
 }
 
 /**
index bbf0e70..7f66560 100644 (file)
@@ -193,7 +193,7 @@ if ($frm and isset($frm->username)) {                             // Login WITH
                     echo $OUTPUT->notification(get_string('emailconfirmsentsuccess'), \core\output\notification::NOTIFY_SUCCESS);
                 }
             }
-            echo $OUTPUT->box(get_string("emailconfirmsent", "", $user->email), "generalbox boxaligncenter");
+            echo $OUTPUT->box(get_string("emailconfirmsent", "", s($user->email)), "generalbox boxaligncenter");
             $resendconfirmurl = new moodle_url('/login/index.php',
                 [
                     'username' => $frm->username,
index 14669a0..e7df086 100644 (file)
Binary files a/mod/forum/amd/build/local/layout/fullscreen.min.js and b/mod/forum/amd/build/local/layout/fullscreen.min.js differ
index 83ad24c..3b53242 100644 (file)
Binary files a/mod/forum/amd/build/local/layout/fullscreen.min.js.map and b/mod/forum/amd/build/local/layout/fullscreen.min.js.map differ
index 2aaf122..512ecae 100644 (file)
@@ -21,6 +21,7 @@
  */
 
 import {addIconToContainer} from 'core/loadingicon';
+import {addToastRegion} from 'core/toast';
 
 /**
  * @param {string} templateName
@@ -36,6 +37,7 @@ const getComposedLayout = ({
     container.classList.add('layout');
     container.classList.add('fullscreen');
     container.setAttribute('aria-role', 'application');
+    addToastRegion(container);
 
     // Lock scrolling on the document body.
     lockBodyScroll();
index 7f7bef8..1e28d9e 100644 (file)
@@ -32,8 +32,11 @@ $pageid = required_param('pageid', PARAM_INT);
 $id     = required_param('id', PARAM_INT);         // Course Module ID
 $qtype  = optional_param('qtype', 0, PARAM_INT);
 $edit   = optional_param('edit', false, PARAM_BOOL);
-$returnto = optional_param('returnto', null, PARAM_URL);
-if (empty($returnto)) {
+$returnto = optional_param('returnto', null, PARAM_LOCALURL);
+
+if (!empty($returnto)) {
+    $returnto = new moodle_url($returnto);
+} else {
     $returnto = new moodle_url('/mod/lesson/edit.php', array('id' => $id));
     $returnto->set_anchor('lesson-' . $pageid);
 }
index 7ce6b74..30a8d4b 100644 (file)
@@ -584,7 +584,7 @@ function lesson_add_header_buttons($cm, $context, $extraeditbuttons=false, $less
                 'id'       => $cm->id,
                 'pageid'   => $lessonpageid,
                 'edit'     => 1,
-                'returnto' => $PAGE->url->out(false)
+                'returnto' => $PAGE->url->out_as_local_url(false)
             ));
             $PAGE->set_button($OUTPUT->single_button($url, get_string('editpagecontent', 'lesson')));
         }
@@ -1363,7 +1363,7 @@ abstract class lesson_add_page_form_base extends moodleform {
 
         if (!empty($this->_customdata['returnto'])) {
             $mform->addElement('hidden', 'returnto', $this->_customdata['returnto']);
-            $mform->setType('returnto', PARAM_URL);
+            $mform->setType('returnto', PARAM_LOCALURL);
         }
 
         $mform->addElement('hidden', 'id');
diff --git a/pix/f/h5p-180.png b/pix/f/h5p-180.png
new file mode 100644 (file)
index 0000000..8e2e824
Binary files /dev/null and b/pix/f/h5p-180.png differ
diff --git a/pix/f/h5p-24.png b/pix/f/h5p-24.png
new file mode 100644 (file)
index 0000000..b3554ab
Binary files /dev/null and b/pix/f/h5p-24.png differ
diff --git a/pix/f/h5p-256.png b/pix/f/h5p-256.png
new file mode 100644 (file)
index 0000000..5beca83
Binary files /dev/null and b/pix/f/h5p-256.png differ
diff --git a/pix/f/h5p-48.png b/pix/f/h5p-48.png
new file mode 100644 (file)
index 0000000..227b8dd
Binary files /dev/null and b/pix/f/h5p-48.png differ
diff --git a/pix/f/h5p-64.png b/pix/f/h5p-64.png
new file mode 100644 (file)
index 0000000..cdd1c9b
Binary files /dev/null and b/pix/f/h5p-64.png differ
diff --git a/pix/f/h5p-72.png b/pix/f/h5p-72.png
new file mode 100644 (file)
index 0000000..a6e4b56
Binary files /dev/null and b/pix/f/h5p-72.png differ
diff --git a/pix/f/h5p-80.png b/pix/f/h5p-80.png
new file mode 100644 (file)
index 0000000..dba1aeb
Binary files /dev/null and b/pix/f/h5p-80.png differ
diff --git a/pix/f/h5p-96.png b/pix/f/h5p-96.png
new file mode 100644 (file)
index 0000000..9b7c2f2
Binary files /dev/null and b/pix/f/h5p-96.png differ
diff --git a/pix/f/h5p.png b/pix/f/h5p.png
new file mode 100644 (file)
index 0000000..1747be3
Binary files /dev/null and b/pix/f/h5p.png differ
index 8358e4a..9c7d3d1 100644 (file)
@@ -698,7 +698,7 @@ function report_security_check_riskadmin($detailed=false) {
     if ($detailed) {
         foreach ($admins as $uid=>$user) {
             $url = "$CFG->wwwroot/user/view.php?id=$user->id";
-            $admins[$uid] = '<li><a href="'.$url.'">'.fullname($user).' ('.$user->email.')</a></li>';
+            $admins[$uid] = '<li><a href="'.$url.'">' . fullname($user, true) . ' (' . s($user->email) . ')</a></li>';
         }
         $admins = '<ul>'.implode('', $admins).'</ul>';
     }
@@ -824,7 +824,7 @@ function report_security_check_riskbackup($detailed=false) {
         foreach ($rs as $user) {
             $context = context::instance_by_id($user->contextid);
             $url = "$CFG->wwwroot/$CFG->admin/roles/assign.php?contextid=$user->contextid&amp;roleid=$user->roleid";
-            $a = (object)array('fullname'=>fullname($user), 'url'=>$url, 'email'=>$user->email,
+            $a = (object)array('fullname'=>fullname($user), 'url'=>$url, 'email'=>s($user->email),
                                'contextname'=>$context->get_context_name());
             $users[] = '<li>'.get_string('check_riskbackup_unassign', 'report_security', $a).'</li>';
         }
index 156d412..22449ad 100644 (file)
@@ -37,6 +37,7 @@ if (0 == strpos($relativepath, '/token/')) {
     $relativepath = ltrim($relativepath, '/');
     $pathparts = explode('/', $relativepath, 2);
     $token = $pathparts[0];
+    $token = clean_param($token, PARAM_ALPHANUM);
     $relativepath = "/{$pathparts[1]}";
 }
 
index b8bc9bb..7cf6c2b 100644 (file)
@@ -581,7 +581,7 @@ abstract class user_selector_base {
         if ($this->extrafields) {
             $displayfields = array();
             foreach ($this->extrafields as $field) {
-                $displayfields[] = $user->{$field};
+                $displayfields[] = s($user->{$field});
             }
             $out .= ' (' . implode(', ', $displayfields) . ')';
         }
index 6a751de..f97c03c 100644 (file)
@@ -109,7 +109,7 @@ class core_webservice_renderer extends plugin_renderer_base {
             $modifiedauthoriseduserurl = new moodle_url('/' . $CFG->admin . '/webservice/service_user_settings.php',
                             array('userid' => $user->id, 'serviceid' => $serviceid));
             $html .= html_writer::tag('a', $user->firstname . " "
-                            . $user->lastname . ", " . $user->email,
+                            . $user->lastname . ", " . s($user->email),
                             array('href' => $modifiedauthoriseduserurl));
             //add missing capabilities
             if (!empty($user->missingcapabilities)) {