Merge branch 'install_master' of https://git.in.moodle.com/amosbot/moodle-install
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Thu, 20 Jun 2019 15:42:50 +0000 (17:42 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Thu, 20 Jun 2019 15:42:50 +0000 (17:42 +0200)
122 files changed:
admin/tool/log/db/subplugins.json [new file with mode: 0644]
admin/tool/log/db/subplugins.php
admin/tool/policy/amd/build/jquery-eu-cookie-law-popup.min.js
admin/tool/policy/amd/src/jquery-eu-cookie-law-popup.js
admin/tool/policy/readme_moodle.txt
admin/tool/policy/thirdpartylibs.xml
backup/util/plan/backup_structure_step.class.php
backup/util/plan/restore_structure_step.class.php
backup/util/plan/tests/step_test.php
blocks/recentlyaccesseditems/classes/helper.php
blocks/recentlyaccesseditems/tests/helper_test.php [new file with mode: 0644]
course/externallib.php
course/format/renderer.php
course/format/topics/renderer.php
course/format/upgrade.txt
course/tests/behat/app_course_completion.feature [deleted file]
course/tests/behat/app_courselist.feature [deleted file]
course/tests/externallib_test.php
course/upgrade.txt
lib/accesslib.php
lib/adminlib.php
lib/classes/component.php
lib/classes/plugin_manager.php
lib/components.json [new file with mode: 0644]
lib/db/services.php
lib/editor/atto/db/subplugins.json [new file with mode: 0644]
lib/editor/atto/db/subplugins.php
lib/editor/tinymce/db/subplugins.json [new file with mode: 0644]
lib/editor/tinymce/db/subplugins.php
lib/formslib.php
lib/html2text/Html2Text.php
lib/moodlelib.php
lib/outputlib.php
lib/php-css-parser/CSSList/AtRuleBlockList.php
lib/php-css-parser/CSSList/CSSList.php
lib/php-css-parser/CSSList/Document.php
lib/php-css-parser/OutputFormat.php
lib/php-css-parser/Parser.php
lib/php-css-parser/Parsing/ParserState.php [new file with mode: 0644]
lib/php-css-parser/Property/AtRule.php
lib/php-css-parser/Rule/Rule.php
lib/php-css-parser/RuleSet/DeclarationBlock.php
lib/php-css-parser/RuleSet/RuleSet.php
lib/php-css-parser/Value/CSSFunction.php
lib/php-css-parser/Value/CSSString.php
lib/php-css-parser/Value/CalcFunction.php [new file with mode: 0644]
lib/php-css-parser/Value/CalcRuleValueList.php [new file with mode: 0644]
lib/php-css-parser/Value/Color.php
lib/php-css-parser/Value/LineName.php [new file with mode: 0644]
lib/php-css-parser/Value/Size.php
lib/php-css-parser/Value/URL.php
lib/php-css-parser/Value/Value.php
lib/php-css-parser/moodle_readme.txt
lib/tests/moodlelib_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-debug.js
lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-min.js
lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker.js
lib/yui/src/formchangechecker/js/formchangechecker.js
mod/assign/db/subplugins.json [new file with mode: 0644]
mod/assign/db/subplugins.php
mod/assign/lang/en/assign.php
mod/assign/lang/en/deprecated.txt
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/tests/generator.php
mod/assign/tests/lib_test.php
mod/assignment/db/subplugins.json [new file with mode: 0644]
mod/assignment/db/subplugins.php
mod/book/db/subplugins.json [new file with mode: 0644]
mod/book/db/subplugins.php
mod/chat/lib.php
mod/choice/lang/en/choice.php
mod/choice/lang/en/deprecated.txt
mod/choice/lib.php
mod/data/db/subplugins.json [new file with mode: 0644]
mod/data/db/subplugins.php
mod/forum/classes/subscriptions.php
mod/forum/externallib.php
mod/forum/lang/en/deprecated.txt
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/post.php
mod/forum/templates/discussion_list.mustache
mod/forum/tests/behat/app_basic_usage.feature [deleted file]
mod/forum/tests/externallib_test.php
mod/forum/tests/lib_test.php
mod/forum/tests/mail_test.php
mod/lesson/lang/en/deprecated.txt [new file with mode: 0644]
mod/lesson/lang/en/lesson.php
mod/lesson/lib.php
mod/lti/db/subplugins.json [new file with mode: 0644]
mod/lti/db/subplugins.php
mod/quiz/attemptlib.php
mod/quiz/db/subplugins.json [new file with mode: 0644]
mod/quiz/db/subplugins.php
mod/quiz/lang/en/deprecated.txt
mod/quiz/lang/en/quiz.php
mod/quiz/lib.php
mod/quiz/tests/attempt_test.php
mod/quiz/tests/events_test.php
mod/scorm/db/subplugins.json [new file with mode: 0644]
mod/scorm/db/subplugins.php
mod/scorm/lang/en/deprecated.txt [new file with mode: 0644]
mod/scorm/lang/en/scorm.php
mod/scorm/lib.php
mod/upgrade.txt
mod/workshop/db/subplugins.json [new file with mode: 0644]
mod/workshop/db/subplugins.php
question/engine/datalib.php
question/engine/tests/datalib_test.php
question/type/ddmarker/amd/build/shapes.min.js
question/type/ddmarker/amd/src/shapes.js
report/stats/user.php
search/engine/solr/classes/engine.php
search/engine/solr/tests/engine_test.php
theme/boost/scss/moodle/blocks.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css
user/editadvanced_form.php
version.php

diff --git a/admin/tool/log/db/subplugins.json b/admin/tool/log/db/subplugins.json
new file mode 100644 (file)
index 0000000..3c8f43f
--- /dev/null
@@ -0,0 +1,5 @@
+{
+    "plugintypes": {
+        "logstore": "admin\/tool\/log\/store"
+    }
+}
index 9d1fe61..538c286 100644 (file)
@@ -22,4 +22,5 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$subplugins = array('logstore' => 'admin/tool/log/store');
+debugging('Use of subplugins.php has been deprecated. Please provide a subplugins.json instead.', DEBUG_DEVELOPER);
+$subplugins = (array) json_decode(file_get_contents(__DIR__ . "/subplugins.json"))->plugintypes;
index 8ea0109..1ed6d8b 100644 (file)
Binary files a/admin/tool/policy/amd/build/jquery-eu-cookie-law-popup.min.js and b/admin/tool/policy/amd/build/jquery-eu-cookie-law-popup.min.js differ
index 22688c5..379c3c4 100644 (file)
@@ -1,22 +1,19 @@
 /**\r
- *     \r
+ *\r
  * JQUERY EU COOKIE LAW POPUPS\r
- * version 1.0.1\r
- * \r
+ * version 1.1.1\r
+ *\r
  * Code on Github:\r
  * https://github.com/wimagguc/jquery-eu-cookie-law-popup\r
- * \r
+ *\r
  * To see a live demo, go to:\r
- * http://www.wimagguc.com/2015/03/jquery-eu-cookie-law-popup/\r
- * \r
+ * http://www.wimagguc.com/2018/05/gdpr-compliance-with-the-jquery-eu-cookie-law-plugin/\r
+ *\r
  * by Richard Dancsi\r
  * http://www.wimagguc.com/\r
- * \r
+ *\r
  */\r
-\r
-define(\r
-['jquery'],\r
-function($) {\r
+define(['jquery'], function($) {\r
 \r
 // for ie9 doesn't support debug console >>>\r
 if (!window.console) window.console = {};\r
@@ -30,7 +27,7 @@ $.fn.euCookieLawPopup = (function() {
        ///////////////////////////////////////////////////////////////////////////////////////////////\r
        // PARAMETERS (MODIFY THIS PART) //////////////////////////////////////////////////////////////\r
        _self.params = {\r
-               cookiePolicyUrl : 'http://www.wimagguc.com/?cookie-policy',\r
+               cookiePolicyUrl : '/?cookie-policy',\r
                popupPosition : 'top',\r
                colorStyle : 'default',\r
                compactStyle : false,\r
@@ -137,10 +134,10 @@ $.fn.euCookieLawPopup = (function() {
                        return _self.params.htmlMarkup;\r
                }\r
 \r
-               var html = \r
-                       '<div class="eupopup-container' + \r
-                           ' eupopup-container-' + _self.params.popupPosition + \r
-                           (_self.params.compactStyle ? ' eupopup-style-compact' : '') + \r
+               var html =\r
+                       '<div class="eupopup-container' +\r
+                           ' eupopup-container-' + _self.params.popupPosition +\r
+                           (_self.params.compactStyle ? ' eupopup-style-compact' : '') +\r
                                ' eupopup-color-' + _self.params.colorStyle + '">' +\r
                                '<div class="eupopup-head">' + _self.params.popupTitle + '</div>' +\r
                                '<div class="eupopup-body">' + _self.params.popupText + '</div>' +\r
@@ -181,7 +178,7 @@ $.fn.euCookieLawPopup = (function() {
 \r
                return userAcceptedCookies;\r
        };\r
-       \r
+\r
        var hideContainer = function() {\r
                // $('.eupopup-container').slideUp(200);\r
                $('.eupopup-container').animate({\r
@@ -206,6 +203,7 @@ $.fn.euCookieLawPopup = (function() {
 \r
                        // No need to display this if user already accepted the policy\r
                        if (userAlreadyAcceptedCookies()) {\r
+        $(document).trigger("user_cookie_already_accepted", {'consent': true});\r
                                return;\r
                        }\r
 \r
@@ -239,7 +237,7 @@ $.fn.euCookieLawPopup = (function() {
                        // Ready to start!\r
                        $('.eupopup-container').show();\r
 \r
-                       // In case it's alright to just display the message once \r
+                       // In case it's alright to just display the message once\r
                        if (_self.params.autoAcceptCookiePolicy) {\r
                                setUserAcceptsCookies(true);\r
                        }\r
@@ -250,5 +248,4 @@ $.fn.euCookieLawPopup = (function() {
 \r
        return publicfunc;\r
 });\r
-\r
 });\r
index 74794c5..a4241d9 100644 (file)
@@ -1,4 +1,4 @@
-jQuery EU Cookie Law popups 1.0.1
+jQuery EU Cookie Law popups 1.1.2
 -------------
 https://github.com/wimagguc/jquery-eu-cookie-law-popup
 
@@ -53,4 +53,4 @@ $(document).bind("user_cookie_consent_changed", function(event, object) {
    Add "tool_policy styles" to the end of the styles file.
 
 4. Execute grunt to compile js
-   grunt js
+   grunt amd
index 151961b..9d7a636 100644 (file)
@@ -4,7 +4,7 @@
     <location>amd/src/jquery-eu-cookie-law-popup.js</location>
     <name>jQuery EU Cookie Law popups</name>
     <license>MIT</license>
-    <version>1.0.1</version>
+    <version>1.1.2</version>
     <licenseversion></licenseversion>
   </library>
 </libraries>
index e18ada4..e94cfe8 100644 (file)
@@ -173,7 +173,7 @@ abstract class backup_structure_step extends backup_step {
      * looking for /mod/modulenanme subplugins. This new method is a generalization of the
      * existing one for activities, supporting all subplugins injecting information everywhere.
      *
-     * @param string $subplugintype type of subplugin as defined in plugin's db/subplugins.php.
+     * @param string $subplugintype type of subplugin as defined in plugin's db/subplugins.json.
      * @param backup_nested_element $element element in the backup tree (anywhere) that
      *                                       we are going to add subplugin information to.
      * @param bool $multiple to define if multiple subplugins can produce information
@@ -206,11 +206,10 @@ abstract class backup_structure_step extends backup_step {
         }
 
         // Check the requested subplugintype is a valid one.
-        $subpluginsfile = core_component::get_component_directory($plugintype . '_' . $pluginname) . '/db/subplugins.php';
-        if (!file_exists($subpluginsfile)) {
-             throw new backup_step_exception('plugin_missing_subplugins_php_file', array($plugintype, $pluginname));
+        $subplugins = core_component::get_subplugins("{$plugintype}_{$pluginname}");
+        if (null === $subplugins) {
+            throw new backup_step_exception('plugin_missing_subplugins_configuration', [$plugintype, $pluginname]);
         }
-        include($subpluginsfile);
         if (!array_key_exists($subplugintype, $subplugins)) {
              throw new backup_step_exception('incorrect_subplugin_type', $subplugintype);
         }
index b59ac82..bffc7b7 100644 (file)
@@ -305,7 +305,7 @@ abstract class restore_structure_step extends restore_step {
      * looking for /mod/modulenanme subplugins. This new method is a generalization of the
      * existing one for activities, supporting all subplugins injecting information everywhere.
      *
-     * @param string $subplugintype type of subplugin as defined in plugin's db/subplugins.php.
+     * @param string $subplugintype type of subplugin as defined in plugin's db/subplugins.json.
      * @param restore_path_element $element element in the structure restore tree that
      *                              we are going to add subplugin information to.
      * @param string $plugintype type of the plugin.
@@ -336,11 +336,10 @@ abstract class restore_structure_step extends restore_step {
         }
 
         // Check the requested subplugintype is a valid one.
-        $subpluginsfile = core_component::get_component_directory($plugintype . '_' . $pluginname) . '/db/subplugins.php';
-        if (!file_exists($subpluginsfile)) {
-            throw new restore_step_exception('plugin_missing_subplugins_php_file', array($plugintype, $pluginname));
+        $subplugins = core_component::get_subplugins("{$plugintype}_{$pluginname}");
+        if (null === $subplugins) {
+            throw new restore_step_exception('plugin_missing_subplugins_configuration', array($plugintype, $pluginname));
         }
-        include($subpluginsfile);
         if (!array_key_exists($subplugintype, $subplugins)) {
              throw new restore_step_exception('incorrect_subplugin_type', $subplugintype);
         }
index 0cc854b..8b9f6e1 100644 (file)
@@ -241,7 +241,7 @@ class backup_step_testcase extends advanced_testcase {
             $this->assertTrue(false, 'base_step_exception expected');
         } catch (exception $e) {
             $this->assertTrue($e instanceof backup_step_exception);
-            $this->assertEquals('plugin_missing_subplugins_php_file', $e->errorcode);
+            $this->assertEquals('plugin_missing_subplugins_configuration', $e->errorcode);
         }
         // Wrong BC (defaulting to mod and modulename) use not having subplugins.
         try {
@@ -250,7 +250,7 @@ class backup_step_testcase extends advanced_testcase {
             $this->assertTrue(false, 'base_step_exception expected');
         } catch (exception $e) {
             $this->assertTrue($e instanceof backup_step_exception);
-            $this->assertEquals('plugin_missing_subplugins_php_file', $e->errorcode);
+            $this->assertEquals('plugin_missing_subplugins_configuration', $e->errorcode);
         }
         // Wrong subplugin type.
         try {
@@ -362,7 +362,7 @@ class backup_step_testcase extends advanced_testcase {
             $this->assertTrue(false, 'base_step_exception expected');
         } catch (exception $e) {
             $this->assertTrue($e instanceof restore_step_exception);
-            $this->assertEquals('plugin_missing_subplugins_php_file', $e->errorcode);
+            $this->assertEquals('plugin_missing_subplugins_configuration', $e->errorcode);
         }
         // Wrong BC (defaulting to mod and modulename) use not having subplugins.
         try {
@@ -371,7 +371,7 @@ class backup_step_testcase extends advanced_testcase {
             $this->assertTrue(false, 'base_step_exception expected');
         } catch (exception $e) {
             $this->assertTrue($e instanceof restore_step_exception);
-            $this->assertEquals('plugin_missing_subplugins_php_file', $e->errorcode);
+            $this->assertEquals('plugin_missing_subplugins_configuration', $e->errorcode);
         }
         // Wrong subplugin type.
         try {
index b7d2b72..d1ac05b 100644 (file)
@@ -53,11 +53,13 @@ class helper {
             return $recentitems;
         }
 
-        // Determine sort sql clause.
-        $sort = 'timeaccess DESC';
-
         $paramsql = array('userid' => $userid);
-        $records = $DB->get_records('block_recentlyaccesseditems', $paramsql, $sort);
+        $sql = "SELECT rai.*
+                  FROM {block_recentlyaccesseditems} rai
+                  JOIN {course} c ON c.id = rai.courseid
+                 WHERE userid = :userid
+                 ORDER BY rai.timeaccess DESC";
+        $records = $DB->get_records_sql($sql, $paramsql);
         $order = 0;
 
         // Get array of items by course. Use $order index to keep sql sorted results.
diff --git a/blocks/recentlyaccesseditems/tests/helper_test.php b/blocks/recentlyaccesseditems/tests/helper_test.php
new file mode 100644 (file)
index 0000000..aed8863
--- /dev/null
@@ -0,0 +1,71 @@
+<?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/>.
+
+/**
+ * Block recentlyaccesseditems helper tests.
+ *
+ * @package    block_recentlyaccesseditems
+ * @copyright  2019 University of Nottingham
+ * @author     Neill Magill <neill.magill@nottingham.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use block_recentlyaccesseditems\helper;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Block Recently accessed helper class tests.
+ *
+ * @package    block_recentlyaccesseditems
+ * @copyright  2019 University of Nottingham
+ * @author     Neill Magill <neill.magill@nottingham.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class block_recentlyaccesseditems_helper_testcase extends advanced_testcase {
+    /**
+     * Tests that the get recent items method can handle getting records when courses have been deleted.
+     */
+    public function test_get_recent_items() {
+        $this->resetAfterTest();
+        $course = self::getDataGenerator()->create_course();
+        $coursetodelete = self::getDataGenerator()->create_course();
+        $user = self::getDataGenerator()->create_and_enrol($course, 'student');
+        self::getDataGenerator()->enrol_user($user->id, $coursetodelete->id, 'student');
+
+        // Add an activity to each course.
+        $forum = self::getDataGenerator()->create_module('forum', ['course' => $course]);
+        $glossary = self::getDataGenerator()->create_module('glossary', ['course' => $coursetodelete]);
+        self::setUser($user);
+
+        // Get the user to visit the activities.
+        $event1params = ['context' => context_module::instance($forum->cmid), 'objectid' => $forum->id];
+        $event1 = \mod_forum\event\course_module_viewed::create($event1params);
+        $event1->trigger();
+        $event2params = ['context' => context_module::instance($glossary->cmid), 'objectid' => $glossary->id];
+        $event2 = \mod_glossary\event\course_module_viewed::create($event2params);
+        $event2->trigger();
+        $recent1 = helper::get_recent_items();
+        self::assertCount(2, $recent1);
+        $recentlimited = helper::get_recent_items(1);
+        self::assertCount(1, $recentlimited);
+        delete_course($coursetodelete, false);
+
+        // There should be no errors if a course has been deleted.
+        $recent2 = helper::get_recent_items();
+        self::assertCount(1, $recent2);
+    }
+}
index bd44260..c914656 100644 (file)
@@ -2906,121 +2906,6 @@ class core_course_external extends external_api {
         return self::get_course_module_returns();
     }
 
-    /**
-     * Returns description of method parameters
-     *
-     * @deprecated since 3.3
-     * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
-     * @return external_function_parameters
-     * @since Moodle 3.2
-     */
-    public static function get_activities_overview_parameters() {
-        return new external_function_parameters(
-            array(
-                'courseids' => new external_multiple_structure(new external_value(PARAM_INT, 'Course id.')),
-            )
-        );
-    }
-
-    /**
-     * Return activities overview for the given courses.
-     *
-     * @deprecated since 3.3
-     * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
-     * @param array $courseids a list of course ids
-     * @return array of warnings and the activities overview
-     * @since Moodle 3.2
-     * @throws moodle_exception
-     */
-    public static function get_activities_overview($courseids) {
-        global $USER;
-
-        // Parameter validation.
-        $params = self::validate_parameters(self::get_activities_overview_parameters(), array('courseids' => $courseids));
-        $courseoverviews = array();
-
-        list($courses, $warnings) = external_util::validate_courses($params['courseids']);
-
-        if (!empty($courses)) {
-            // Add lastaccess to each course (required by print_overview function).
-            // We need the complete user data, the ws server does not load a complete one.
-            $user = get_complete_user_data('id', $USER->id);
-            foreach ($courses as $course) {
-                if (isset($user->lastcourseaccess[$course->id])) {
-                    $course->lastaccess = $user->lastcourseaccess[$course->id];
-                } else {
-                    $course->lastaccess = 0;
-                }
-            }
-
-            $overviews = array();
-            if ($modules = get_plugin_list_with_function('mod', 'print_overview')) {
-                foreach ($modules as $fname) {
-                    $fname($courses, $overviews);
-                }
-            }
-
-            // Format output.
-            foreach ($overviews as $courseid => $modules) {
-                $courseoverviews[$courseid]['id'] = $courseid;
-                $courseoverviews[$courseid]['overviews'] = array();
-
-                foreach ($modules as $modname => $overviewtext) {
-                    $courseoverviews[$courseid]['overviews'][] = array(
-                        'module' => $modname,
-                        'overviewtext' => $overviewtext // This text doesn't need formatting.
-                    );
-                }
-            }
-        }
-
-        $result = array(
-            'courses' => $courseoverviews,
-            'warnings' => $warnings
-        );
-        return $result;
-    }
-
-    /**
-     * Returns description of method result value
-     *
-     * @deprecated since 3.3
-     * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
-     * @return external_description
-     * @since Moodle 3.2
-     */
-    public static function get_activities_overview_returns() {
-        return new external_single_structure(
-            array(
-                'courses' => new external_multiple_structure(
-                    new external_single_structure(
-                        array(
-                            'id' => new external_value(PARAM_INT, 'Course id'),
-                            'overviews' => new external_multiple_structure(
-                                new external_single_structure(
-                                    array(
-                                        'module' => new external_value(PARAM_PLUGIN, 'Module name'),
-                                        'overviewtext' => new external_value(PARAM_RAW, 'Overview text'),
-                                    )
-                                )
-                            )
-                        )
-                    ), 'List of courses'
-                ),
-                'warnings' => new external_warnings()
-            )
-        );
-    }
-
-    /**
-     * Marking the method as deprecated.
-     *
-     * @return bool
-     */
-    public static function get_activities_overview_is_deprecated() {
-        return true;
-    }
-
     /**
      * Returns description of method parameters
      *
index 2f94041..ffdd6db 100644 (file)
@@ -254,39 +254,11 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
     }
 
     /**
-     * Generate the edit controls of a section
-     *
-     * @param stdClass $course The course entry from DB
-     * @param stdClass $section The course_section entry from DB
-     * @param bool $onsectionpage true if being printed on a section page
-     * @return array of links with edit controls
-     * @deprecated since Moodle 3.0 MDL-48947 - please do not use this function any more.
-     * @see format_section_renderer_base::section_edit_control_items()
+     * @deprecated since Moodle 3.0 MDL-48947 - Use format_section_renderer_base::section_edit_control_items() instead
      */
-    protected function section_edit_controls($course, $section, $onsectionpage = false) {
-        global $PAGE;
-
-        if (!$PAGE->user_is_editing()) {
-            return array();
-        }
-
-        $controls = array();
-        $items = $this->section_edit_control_items($course, $section, $onsectionpage);
-
-        foreach ($items as $key => $item) {
-                $url = empty($item['url']) ? '' : $item['url'];
-                $icon = empty($item['icon']) ? '' : $item['icon'];
-                $name = empty($item['name']) ? '' : $item['name'];
-                $attr = empty($item['attr']) ? '' : $item['attr'];
-                $class = empty($item['pixattr']['class']) ? '' : $item['pixattr']['class'];
-                $alt = empty($item['pixattr']['alt']) ? '' : $item['pixattr']['alt'];
-                $controls[$key] = html_writer::link(
-                    new moodle_url($url), $this->output->pix_icon($icon, $alt),
-                    $attr);
-        }
-
-        debugging('section_edit_controls() is deprecated, please use section_edit_control_items() instead.', DEBUG_DEVELOPER);
-        return $controls;
+    protected function section_edit_controls() {
+        throw new coding_exception('section_edit_controls() can not be used anymore. Please use ' .
+            'section_edit_control_items() instead.');
     }
 
     /**
index 1efad30..67551c5 100644 (file)
@@ -44,7 +44,7 @@ class format_topics_renderer extends format_section_renderer_base {
     public function __construct(moodle_page $page, $target) {
         parent::__construct($page, $target);
 
-        // Since format_topics_renderer::section_edit_controls() only displays the 'Set current section' control when editing mode is on
+        // Since format_topics_renderer::section_edit_control_items() only displays the 'Highlight' control when editing mode is on
         // we need to be sure that the link 'Turn editing mode on' is available for a user who does not have any other managing capability.
         $page->set_other_editing_capability('moodle/course:setcurrentsection');
     }
index 68d7194..d549e46 100644 (file)
@@ -2,6 +2,11 @@ This files describes API changes for course formats
 
 Overview of this plugin type at http://docs.moodle.org/dev/Course_formats
 
+=== 3.8 ===
+
+* The following functions have been finally deprecated and can not be used anymore:
+  * section_edit_controls()
+
 === 3.6 ===
 * New method validate_format_options() cleans the values of the course/section format options before inserting them
   in the database. Course format options can now be set in tool_uploadcourse and validation of user-submitted data is important.
diff --git a/course/tests/behat/app_course_completion.feature b/course/tests/behat/app_course_completion.feature
deleted file mode 100644 (file)
index f0c30ba..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-@core @core_course @app @javascript
-Feature: Check course completion feature.
-  In order to track the progress of the course on mobile device
-  As a student
-  I need to be able to update the activity completion status.
-
-  Background:
-    Given the following "users" exist:
-      | username | firstname | lastname | email                |
-      | student1 | Student   | 1        | student1@example.com |
-    And the following "courses" exist:
-      | fullname | shortname | category | enablecompletion |
-      | Course 1 | C1        | 0        | 1                |
-    And the following "course enrolments" exist:
-      | user     | course | role    |
-      | student1 | C1     | student |
-
-  Scenario: Complete the activity manually by clicking at the completion checkbox.
-    Given the following "activities" exist:
-      | activity | name         | course | idnumber | completion | completionview |
-      | forum    | First forum  | C1     | forum1   | 1          | 0              |
-      | forum    | Second forum | C1     | forum2   | 1          | 0              |
-    When I enter the app
-    And I log in as "student1"
-    And I press "Course 1" near "Recently accessed courses" in the app
-    # Set activities as completed.
-    And I should see "0%"
-    And I press "Not completed: First forum. Select to mark as complete." in the app
-    And I should see "50%"
-    And I press "Not completed: Second forum. Select to mark as complete." in the app
-    And I should see "100%"
-    # Set activities as not completed.
-    And I press "Completed: First forum. Select to mark as not complete." in the app
-    And I should see "50%"
-    And I press "Completed: Second forum. Select to mark as not complete." in the app
-    And I should see "0%"
diff --git a/course/tests/behat/app_courselist.feature b/course/tests/behat/app_courselist.feature
deleted file mode 100644 (file)
index ea68c05..0000000
+++ /dev/null
@@ -1,120 +0,0 @@
-@core @core_course @app @javascript
-Feature: Test course list shown on app start tab
-  In order to select a course
-  As a student
-  I need to see the correct list of courses
-
-  Background:
-    Given the following "courses" exist:
-      | fullname | shortname |
-      | Course 1 | C1        |
-      | Course 2 | C2        |
-    And the following "users" exist:
-      | username |
-      | student1 |
-      | student2 |
-    And the following "course enrolments" exist:
-      | user     | course | role    |
-      | student1 | C1     | student |
-      | student2 | C1     | student |
-      | student2 | C2     | student |
-
-  Scenario: Student is registered on one course
-    When I enter the app
-    And I log in as "student1"
-    Then I should see "Course 1"
-    And I should not see "Course 2"
-
-  Scenario: Student is registered on two courses (shortnames not displayed)
-    When I enter the app
-    And I log in as "student2"
-    Then I should see "Course 1"
-    And I should see "Course 2"
-    And I should not see "C1"
-    And I should not see "C2"
-
-  Scenario: Student is registered on two courses (shortnames displayed)
-    Given the following config values are set as admin:
-      | courselistshortnames | 1 |
-    When I enter the app
-    And I log in as "student2"
-    Then I should see "Course 1"
-    And I should see "Course 2"
-    And I should see "C1"
-    And I should see "C2"
-
-  Scenario: Student uses course list to enter course, then leaves it again
-    When I enter the app
-    And I log in as "student2"
-    And I press "Course 2" near "Course overview" in the app
-    Then the header should be "Course 2" in the app
-    And I press the back button in the app
-    Then the header should be "Acceptance test site" in the app
-
-  Scenario: Student uses filter feature to reduce course list
-    Given the following config values are set as admin:
-      | courselistshortnames | 1 |
-    And the following "courses" exist:
-      | fullname | shortname |
-      | Frog 3   | C3        |
-      | Frog 4   | C4        |
-      | Course 5 | C5        |
-      | Toad 6   | C6        |
-    And the following "course enrolments" exist:
-      | user     | course | role    |
-      | student2 | C3     | student |
-      | student2 | C4     | student |
-      | student2 | C5     | student |
-      | student2 | C6     | student |
-    # Create bogus courses so that the main ones aren't shown in the 'recently accessed' part.
-    # Because these come later in alphabetical order, they may not be displayed in the lower part
-    # which is OK.
-    And the following "courses" exist:
-      | fullname | shortname |
-      | Zogus 1  | Z1        |
-      | Zogus 2  | Z2        |
-      | Zogus 3  | Z3        |
-      | Zogus 4  | Z4        |
-      | Zogus 5  | Z5        |
-      | Zogus 6  | Z6        |
-      | Zogus 7  | Z7        |
-      | Zogus 8  | Z8        |
-      | Zogus 9  | Z9        |
-      | Zogus 10 | Z10       |
-    And the following "course enrolments" exist:
-      | user     | course | role    |
-      | student2 | Z1     | student |
-      | student2 | Z2     | student |
-      | student2 | Z3     | student |
-      | student2 | Z4     | student |
-      | student2 | Z5     | student |
-      | student2 | Z6     | student |
-      | student2 | Z7     | student |
-      | student2 | Z8     | student |
-      | student2 | Z9     | student |
-      | student2 | Z10    | student |
-    When I enter the app
-    And I log in as "student2"
-    Then I should see "C1"
-    And I should see "C2"
-    And I should see "C3"
-    And I should see "C4"
-    And I should see "C5"
-    And I should see "C6"
-    And I press "more" near "Course overview" in the app
-    And I press "Filter my courses" in the app
-    And I set the field "Filter my courses" to "fr" in the app
-    Then I should not see "C1"
-    And I should not see "C2"
-    And I should see "C3"
-    And I should see "C4"
-    And I should not see "C5"
-    And I should not see "C6"
-    And I press "more" near "Course overview" in the app
-    And I press "Filter my courses" in the app
-    Then I should see "C1"
-    And I should see "C2"
-    And I should see "C3"
-    And I should see "C4"
-    And I should see "C5"
-    And I should see "C6"
index ac06afa..ac4358a 100644 (file)
@@ -2194,50 +2194,6 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
 
     }
 
-    /**
-     * Test get_activities_overview
-     */
-    public function test_get_activities_overview() {
-        global $USER;
-
-        $this->resetAfterTest();
-        $course1 = self::getDataGenerator()->create_course();
-        $course2 = self::getDataGenerator()->create_course();
-
-        // Create a viewer user.
-        $viewer = self::getDataGenerator()->create_user((object) array('trackforums' => 1));
-        $this->getDataGenerator()->enrol_user($viewer->id, $course1->id);
-        $this->getDataGenerator()->enrol_user($viewer->id, $course2->id);
-
-        // Create two forums - one in each course.
-        $record = new stdClass();
-        $record->course = $course1->id;
-        $forum1 = self::getDataGenerator()->create_module('forum', (object) array('course' => $course1->id));
-        $forum2 = self::getDataGenerator()->create_module('forum', (object) array('course' => $course2->id));
-
-        $this->setAdminUser();
-        // A standard post in the forum.
-        $record = new stdClass();
-        $record->course = $course1->id;
-        $record->userid = $USER->id;
-        $record->forum = $forum1->id;
-        $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
-
-        $this->setUser($viewer->id);
-        $courses = array($course1->id , $course2->id);
-
-        $result = core_course_external::get_activities_overview($courses);
-        $this->assertDebuggingCalledCount(8);
-        $result = external_api::clean_returnvalue(core_course_external::get_activities_overview_returns(), $result);
-
-        // There should be one entry for course1, and no others.
-        $this->assertCount(1, $result['courses']);
-        $this->assertEquals($course1->id, $result['courses'][0]['id']);
-        // Check expected overview data for the module.
-        $this->assertEquals('forum', $result['courses'][0]['overviews'][0]['module']);
-        $this->assertContains('1 total unread', $result['courses'][0]['overviews'][0]['overviewtext']);
-    }
-
     /**
      * Test get_user_navigation_options
      */
index 5ebb81b..10407f9 100644 (file)
@@ -1,6 +1,10 @@
 This files describes API changes in /course/*,
 information provided here is intended especially for developers.
 
+=== 3.8 ===
+* The following functions have been finally deprecated and can not be used any more:
+  - core_course_external::get_activities_overview
+
 === 3.7 ===
 
  * The course pattern function in course_summary_exporter::get_course_pattern has been moved to $OUTPUT->get_generated_image_for_id.
@@ -10,7 +14,6 @@ information provided here is intended especially for developers.
  * External function core_course_external::get_course_contents now returns a new contentsinfo field with summary files information.
  * External function core_course_external::get_course_contents now returns an additional field "tags" returning the content tags.
 
-
 === 3.6 ===
 
  * External function core_course_external::get_course_public_information now returns the roles and the primary role of course
index e80ba19..856c556 100644 (file)
@@ -7010,20 +7010,27 @@ class context_module extends context {
         $module = $DB->get_record('modules', array('id'=>$cm->module));
 
         $subcaps = array();
-        $subpluginsfile = "$CFG->dirroot/mod/$module->name/db/subplugins.php";
-        if (file_exists($subpluginsfile)) {
+
+        $modulepath = "{$CFG->dirroot}/mod/{$module->name}";
+        if (file_exists("{$modulepath}/db/subplugins.json")) {
+            $subplugins = (array) json_decode(file_get_contents("{$modulepath}/db/subplugins.json"))->plugintypes;
+        } else if (file_exists("{$modulepath}/db/subplugins.php")) {
+            debugging('Use of subplugins.php has been deprecated. ' .
+                    'Please update your plugin to provide a subplugins.json file instead.',
+                    DEBUG_DEVELOPER);
             $subplugins = array();  // should be redefined in the file
-            include($subpluginsfile);
-            if (!empty($subplugins)) {
-                foreach (array_keys($subplugins) as $subplugintype) {
-                    foreach (array_keys(core_component::get_plugin_list($subplugintype)) as $subpluginname) {
-                        $subcaps = array_merge($subcaps, array_keys(load_capability_def($subplugintype.'_'.$subpluginname)));
-                    }
+            include("{$modulepath}/db/subplugins.php");
+        }
+
+        if (!empty($subplugins)) {
+            foreach (array_keys($subplugins) as $subplugintype) {
+                foreach (array_keys(core_component::get_plugin_list($subplugintype)) as $subpluginname) {
+                    $subcaps = array_merge($subcaps, array_keys(load_capability_def($subplugintype.'_'.$subpluginname)));
                 }
             }
         }
 
-        $modfile = "$CFG->dirroot/mod/$module->name/lib.php";
+        $modfile = "{$modulepath}/lib.php";
         $extracaps = array();
         if (file_exists($modfile)) {
             include_once($modfile);
index 78b1a35..fdb999b 100644 (file)
@@ -132,17 +132,26 @@ function uninstall_plugin($type, $name) {
     $subplugintypes = core_component::get_plugin_types_with_subplugins();
     if (isset($subplugintypes[$type])) {
         $base = core_component::get_plugin_directory($type, $name);
-        if (file_exists("$base/db/subplugins.php")) {
-            $subplugins = array();
-            include("$base/db/subplugins.php");
-            foreach ($subplugins as $subplugintype=>$dir) {
+
+        $subpluginsfile = "{$base}/db/subplugins.json";
+        if (file_exists($subpluginsfile)) {
+            $subplugins = (array) json_decode(file_get_contents($subpluginsfile))->plugintypes;
+        } else if (file_exists("{$base}/db/subplugins.php")) {
+            debugging('Use of subplugins.php has been deprecated. ' .
+                    'Please update your plugin to provide a subplugins.json file instead.',
+                    DEBUG_DEVELOPER);
+            $subplugins = [];
+            include("{$base}/db/subplugins.php");
+        }
+
+        if (!empty($subplugins)) {
+            foreach (array_keys($subplugins) as $subplugintype) {
                 $instances = core_component::get_plugin_list($subplugintype);
                 foreach ($instances as $subpluginname => $notusedpluginpath) {
                     uninstall_plugin($subplugintype, $subpluginname);
                 }
             }
         }
-
     }
 
     $component = $type . '_' . $name;  // eg. 'qtype_multichoice' or 'workshopgrading_accumulative' or 'mod_forum'
index 6c5d842..2f1d336 100644 (file)
@@ -47,6 +47,8 @@ class core_component {
     /** @var array list plugin types that support subplugins, do not add more here unless absolutely necessary */
     protected static $supportsubplugins = array('mod', 'editor', 'tool', 'local');
 
+    /** @var object JSON source of the component data */
+    protected static $componentsource = null;
     /** @var array cache of plugin types */
     protected static $plugintypes = null;
     /** @var array cache of plugin locations */
@@ -416,79 +418,20 @@ $cache = '.var_export($cache, true).';
         global $CFG;
 
         // NOTE: Any additions here must be verified to not collide with existing add-on modules and subplugins!!!
+        $info = [];
+        foreach (self::fetch_component_source('subsystems') as $subsystem => $path) {
+            // Replace admin/ directory with the config setting.
+            if ($CFG->admin !== 'admin') {
+                if ($path === 'admin') {
+                    $path = $CFG->admin;
+                }
+                if (strpos($path, 'admin/') === 0) {
+                    $path = $CFG->admin . substr($path, 0, 5);
+                }
+            }
 
-        $info = array(
-            'access'      => null,
-            'admin'       => $CFG->dirroot.'/'.$CFG->admin,
-            'analytics'   => $CFG->dirroot . '/analytics',
-            'antivirus'   => $CFG->dirroot . '/lib/antivirus',
-            'auth'        => $CFG->dirroot.'/auth',
-            'availability' => $CFG->dirroot . '/availability',
-            'backup'      => $CFG->dirroot.'/backup/util/ui',
-            'badges'      => $CFG->dirroot.'/badges',
-            'block'       => $CFG->dirroot.'/blocks',
-            'blog'        => $CFG->dirroot.'/blog',
-            'bulkusers'   => null,
-            'cache'       => $CFG->dirroot.'/cache',
-            'calendar'    => $CFG->dirroot.'/calendar',
-            'cohort'      => $CFG->dirroot.'/cohort',
-            'comment'     => $CFG->dirroot.'/comment',
-            'competency'  => $CFG->dirroot.'/competency',
-            'completion'  => $CFG->dirroot.'/completion',
-            'countries'   => null,
-            'course'      => $CFG->dirroot.'/course',
-            'currencies'  => null,
-            'customfield' => $CFG->dirroot.'/customfield',
-            'dbtransfer'  => null,
-            'debug'       => null,
-            'editor'      => $CFG->dirroot.'/lib/editor',
-            'edufields'   => null,
-            'enrol'       => $CFG->dirroot.'/enrol',
-            'error'       => null,
-            'favourites'  => $CFG->dirroot . '/favourites',
-            'filepicker'  => null,
-            'fileconverter' => $CFG->dirroot.'/files/converter',
-            'files'       => $CFG->dirroot.'/files',
-            'filters'     => $CFG->dirroot.'/filter',
-            //'fonts'       => null, // Bogus.
-            'form'        => $CFG->dirroot.'/lib/form',
-            'grades'      => $CFG->dirroot.'/grade',
-            'grading'     => $CFG->dirroot.'/grade/grading',
-            'group'       => $CFG->dirroot.'/group',
-            'help'        => null,
-            'hub'         => null,
-            'imscc'       => null,
-            'install'     => null,
-            'iso6392'     => null,
-            'langconfig'  => null,
-            'license'     => null,
-            'mathslib'    => null,
-            'media'       => $CFG->dirroot.'/media',
-            'message'     => $CFG->dirroot.'/message',
-            'mimetypes'   => null,
-            'mnet'        => $CFG->dirroot.'/mnet',
-            //'moodle.org'  => null, // Not used any more.
-            'my'          => $CFG->dirroot.'/my',
-            'notes'       => $CFG->dirroot.'/notes',
-            'pagetype'    => null,
-            'pix'         => null,
-            'plagiarism'  => $CFG->dirroot.'/plagiarism',
-            'plugin'      => null,
-            'portfolio'   => $CFG->dirroot.'/portfolio',
-            'privacy'     => $CFG->dirroot . '/privacy',
-            'question'    => $CFG->dirroot.'/question',
-            'rating'      => $CFG->dirroot.'/rating',
-            'repository'  => $CFG->dirroot.'/repository',
-            'rss'         => $CFG->dirroot.'/rss',
-            'role'        => $CFG->dirroot.'/'.$CFG->admin.'/roles',
-            'search'      => $CFG->dirroot.'/search',
-            'table'       => null,
-            'tag'         => $CFG->dirroot.'/tag',
-            'timezones'   => null,
-            'user'        => $CFG->dirroot.'/user',
-            'userkey'     => $CFG->dirroot.'/lib/userkey',
-            'webservice'  => $CFG->dirroot.'/webservice',
-        );
+            $info[$subsystem] = empty($path) ? null : "{$CFG->dirroot}/{$path}";
+        }
 
         return $info;
     }
@@ -500,43 +443,15 @@ $cache = '.var_export($cache, true).';
     protected static function fetch_plugintypes() {
         global $CFG;
 
-        $types = array(
-            'antivirus'     => $CFG->dirroot . '/lib/antivirus',
-            'availability'  => $CFG->dirroot . '/availability/condition',
-            'qtype'         => $CFG->dirroot.'/question/type',
-            'mod'           => $CFG->dirroot.'/mod',
-            'auth'          => $CFG->dirroot.'/auth',
-            'calendartype'  => $CFG->dirroot.'/calendar/type',
-            'customfield'   => $CFG->dirroot.'/customfield/field',
-            'enrol'         => $CFG->dirroot.'/enrol',
-            'message'       => $CFG->dirroot.'/message/output',
-            'block'         => $CFG->dirroot.'/blocks',
-            'media'         => $CFG->dirroot.'/media/player',
-            'filter'        => $CFG->dirroot.'/filter',
-            'editor'        => $CFG->dirroot.'/lib/editor',
-            'format'        => $CFG->dirroot.'/course/format',
-            'dataformat'    => $CFG->dirroot.'/dataformat',
-            'profilefield'  => $CFG->dirroot.'/user/profile/field',
-            'report'        => $CFG->dirroot.'/report',
-            'coursereport'  => $CFG->dirroot.'/course/report', // Must be after system reports.
-            'gradeexport'   => $CFG->dirroot.'/grade/export',
-            'gradeimport'   => $CFG->dirroot.'/grade/import',
-            'gradereport'   => $CFG->dirroot.'/grade/report',
-            'gradingform'   => $CFG->dirroot.'/grade/grading/form',
-            'mlbackend'     => $CFG->dirroot.'/lib/mlbackend',
-            'mnetservice'   => $CFG->dirroot.'/mnet/service',
-            'webservice'    => $CFG->dirroot.'/webservice',
-            'repository'    => $CFG->dirroot.'/repository',
-            'portfolio'     => $CFG->dirroot.'/portfolio',
-            'search'        => $CFG->dirroot.'/search/engine',
-            'qbehaviour'    => $CFG->dirroot.'/question/behaviour',
-            'qformat'       => $CFG->dirroot.'/question/format',
-            'plagiarism'    => $CFG->dirroot.'/plagiarism',
-            'tool'          => $CFG->dirroot.'/'.$CFG->admin.'/tool',
-            'cachestore'    => $CFG->dirroot.'/cache/stores',
-            'cachelock'     => $CFG->dirroot.'/cache/locks',
-            'fileconverter' => $CFG->dirroot.'/files/converter',
-        );
+        $types = [];
+        foreach (self::fetch_component_source('plugintypes') as $plugintype => $path) {
+            // Replace admin/ with the config setting.
+            if ($CFG->admin !== 'admin' && strpos($path, 'admin/') === 0) {
+                $path = $CFG->admin . substr($path, 0, 5);
+            }
+            $types[$plugintype] = "{$CFG->dirroot}/{$path}";
+        }
+
         $parents = array();
         $subplugins = array();
 
@@ -596,6 +511,19 @@ $cache = '.var_export($cache, true).';
         return array($types, $parents, $subplugins);
     }
 
+    /**
+     * Returns the component source content as loaded from /lib/components.json.
+     *
+     * @return array
+     */
+    protected static function fetch_component_source(string $key) {
+        if (null === self::$componentsource) {
+            self::$componentsource = (array) json_decode(file_get_contents(__DIR__ . '/../components.json'));
+        }
+
+        return (array) self::$componentsource[$key];
+    }
+
     /**
      * Returns list of subtypes.
      * @param string $ownerdir
@@ -605,28 +533,35 @@ $cache = '.var_export($cache, true).';
         global $CFG;
 
         $types = array();
-        if (file_exists("$ownerdir/db/subplugins.php")) {
-            $subplugins = array();
+        $subplugins = array();
+        if (file_exists("$ownerdir/db/subplugins.json")) {
+            $subplugins = (array) json_decode(file_get_contents("$ownerdir/db/subplugins.json"))->plugintypes;
+        } else if (file_exists("$ownerdir/db/subplugins.php")) {
+            debugging('Use of subplugins.php has been deprecated. ' .
+                    'Please update your plugin to provide a subplugins.json file instead.',
+                    DEBUG_DEVELOPER);
             include("$ownerdir/db/subplugins.php");
-            foreach ($subplugins as $subtype => $dir) {
-                if (!preg_match('/^[a-z][a-z0-9]*$/', $subtype)) {
-                    error_log("Invalid subtype '$subtype'' detected in '$ownerdir', invalid characters present.");
-                    continue;
-                }
-                if (isset(self::$subsystems[$subtype])) {
-                    error_log("Invalid subtype '$subtype'' detected in '$ownerdir', duplicates core subsystem.");
-                    continue;
-                }
-                if ($CFG->admin !== 'admin' and strpos($dir, 'admin/') === 0) {
-                    $dir = preg_replace('|^admin/|', "$CFG->admin/", $dir);
-                }
-                if (!is_dir("$CFG->dirroot/$dir")) {
-                    error_log("Invalid subtype directory '$dir' detected in '$ownerdir'.");
-                    continue;
-                }
-                $types[$subtype] = "$CFG->dirroot/$dir";
+        }
+
+        foreach ($subplugins as $subtype => $dir) {
+            if (!preg_match('/^[a-z][a-z0-9]*$/', $subtype)) {
+                error_log("Invalid subtype '$subtype'' detected in '$ownerdir', invalid characters present.");
+                continue;
+            }
+            if (isset(self::$subsystems[$subtype])) {
+                error_log("Invalid subtype '$subtype'' detected in '$ownerdir', duplicates core subsystem.");
+                continue;
             }
+            if ($CFG->admin !== 'admin' and strpos($dir, 'admin/') === 0) {
+                $dir = preg_replace('|^admin/|', "$CFG->admin/", $dir);
+            }
+            if (!is_dir("$CFG->dirroot/$dir")) {
+                error_log("Invalid subtype directory '$dir' detected in '$ownerdir'.");
+                continue;
+            }
+            $types[$subtype] = "$CFG->dirroot/$dir";
         }
+
         return $types;
     }
 
index 4eaa742..ce5442f 100644 (file)
@@ -511,10 +511,10 @@ class core_plugin_manager {
 
     /**
      * Returns list of plugins that define their subplugins and the information
-     * about them from the db/subplugins.php file.
+     * about them from the db/subplugins.json file.
      *
      * @return array with keys like 'mod_quiz', and values the data from the
-     *      corresponding db/subplugins.php file.
+     *      corresponding db/subplugins.json file.
      */
     public function get_subplugins() {
 
diff --git a/lib/components.json b/lib/components.json
new file mode 100644 (file)
index 0000000..8ca41cf
--- /dev/null
@@ -0,0 +1,111 @@
+{
+    "plugintypes": {
+        "antivirus": "lib\/antivirus",
+        "availability": "availability\/condition",
+        "qtype": "question\/type",
+        "mod": "mod",
+        "auth": "auth",
+        "calendartype": "calendar\/type",
+        "customfield": "customfield\/field",
+        "enrol": "enrol",
+        "message": "message\/output",
+        "block": "blocks",
+        "media": "media\/player",
+        "filter": "filter",
+        "editor": "lib\/editor",
+        "format": "course\/format",
+        "dataformat": "dataformat",
+        "profilefield": "user\/profile\/field",
+        "report": "report",
+        "coursereport": "course\/report",
+        "gradeexport": "grade\/export",
+        "gradeimport": "grade\/import",
+        "gradereport": "grade\/report",
+        "gradingform": "grade\/grading\/form",
+        "mlbackend": "lib\/mlbackend",
+        "mnetservice": "mnet\/service",
+        "webservice": "webservice",
+        "repository": "repository",
+        "portfolio": "portfolio",
+        "search": "search\/engine",
+        "qbehaviour": "question\/behaviour",
+        "qformat": "question\/format",
+        "plagiarism": "plagiarism",
+        "tool": "admin\/tool",
+        "cachestore": "cache\/stores",
+        "cachelock": "cache\/locks",
+        "fileconverter": "files\/converter",
+        "theme": "theme",
+        "local": "local"
+    },
+    "subsystems": {
+        "access": null,
+        "admin": "admin",
+        "analytics": "analytics",
+        "antivirus": "lib\/antivirus",
+        "auth": "auth",
+        "availability": "availability",
+        "backup": "backup\/util\/ui",
+        "badges": "badges",
+        "block": "blocks",
+        "blog": "blog",
+        "bulkusers": null,
+        "cache": "cache",
+        "calendar": "calendar",
+        "cohort": "cohort",
+        "comment": "comment",
+        "competency": "competency",
+        "completion": "completion",
+        "countries": null,
+        "course": "course",
+        "currencies": null,
+        "customfield": "customfield",
+        "dbtransfer": null,
+        "debug": null,
+        "editor": "lib\/editor",
+        "edufields": null,
+        "enrol": "enrol",
+        "error": null,
+        "favourites": "favourites",
+        "filepicker": null,
+        "fileconverter": "files\/converter",
+        "files": "files",
+        "filters": "filter",
+        "form": "lib\/form",
+        "grades": "grade",
+        "grading": "grade\/grading",
+        "group": "group",
+        "help": null,
+        "hub": null,
+        "imscc": null,
+        "install": null,
+        "iso6392": null,
+        "langconfig": null,
+        "license": null,
+        "mathslib": null,
+        "media": "media",
+        "message": "message",
+        "mimetypes": null,
+        "mnet": "mnet",
+        "my": "my",
+        "notes": "notes",
+        "pagetype": null,
+        "pix": null,
+        "plagiarism": "plagiarism",
+        "plugin": null,
+        "portfolio": "portfolio",
+        "privacy": "privacy",
+        "question": "question",
+        "rating": "rating",
+        "repository": "repository",
+        "rss": "rss",
+        "role": "admin\/roles",
+        "search": "search",
+        "table": null,
+        "tag": "tag",
+        "timezones": null,
+        "user": "user",
+        "userkey": "lib\/userkey",
+        "webservice": "webservice"
+    }
+}
index ce63f3d..1e17cdd 100644 (file)
@@ -529,15 +529,6 @@ $functions = array(
         'type' => 'write',
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
-    'core_course_get_activities_overview' => array(
-        'classname' => 'core_course_external',
-        'methodname' => 'get_activities_overview',
-        'classpath' => 'course/externallib.php',
-        'description' => '** DEPRECATED ** Please do not call this function any more.
-                          Return activities overview for the given courses.',
-        'type' => 'read',
-        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
-    ),
     'core_course_get_user_navigation_options' => array(
         'classname' => 'core_course_external',
         'methodname' => 'get_user_navigation_options',
diff --git a/lib/editor/atto/db/subplugins.json b/lib/editor/atto/db/subplugins.json
new file mode 100644 (file)
index 0000000..d9b9d8c
--- /dev/null
@@ -0,0 +1,5 @@
+{
+    "plugintypes": {
+        "atto": "lib\/editor\/atto\/plugins"
+    }
+}
index de0b440..7500a5f 100644 (file)
@@ -24,4 +24,5 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$subplugins = array('atto' => 'lib/editor/atto/plugins');
+debugging('Use of subplugins.php has been deprecated. Please provide a subplugins.json instead.', DEBUG_DEVELOPER);
+$subplugins = (array) json_decode(file_get_contents(__DIR__ . "/subplugins.json"))->plugintypes;
diff --git a/lib/editor/tinymce/db/subplugins.json b/lib/editor/tinymce/db/subplugins.json
new file mode 100644 (file)
index 0000000..f6c8601
--- /dev/null
@@ -0,0 +1,5 @@
+{
+    "plugintypes": {
+        "tinymce": "lib\/editor\/tinymce\/plugins"
+    }
+}
index 68ef191..a5baea9 100644 (file)
@@ -24,4 +24,5 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$subplugins = array('tinymce' => 'lib/editor/tinymce/plugins');
+debugging('Use of subplugins.php has been deprecated. Please provide a subplugins.json instead.', DEBUG_DEVELOPER);
+$subplugins = (array) json_decode(file_get_contents(__DIR__ . "/subplugins.json"))->plugintypes;
index bc0d581..ca39e81 100644 (file)
@@ -1448,6 +1448,16 @@ abstract class moodleform {
         $oldclass = $this->_form->getAttribute('class');
         $this->_form->updateAttributes(array('class' => $oldclass . ' full-width-labels'));
     }
+
+    /**
+     * Set the initial 'dirty' state of the form.
+     *
+     * @param bool $state
+     * @since Moodle 3.7.1
+     */
+    public function set_initial_dirty_state($state = false) {
+        $this->_form->set_initial_dirty_state($state);
+    }
 }
 
 /**
@@ -1503,6 +1513,13 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
     /** @var bool whether to automatically initialise M.formchangechecker for this form. */
     protected $_use_form_change_checker = true;
 
+    /**
+     * The initial state of the dirty state.
+     *
+     * @var bool
+     */
+    protected $_initial_form_dirty_state = false;
+
     /**
      * The form name is derived from the class name of the wrapper minus the trailing form
      * It is a name with words joined by underscores whereas the id attribute is words joined by underscores.
@@ -1721,6 +1738,26 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
         $this->_disableShortforms = $disable;
     }
 
+    /**
+     * Set the initial 'dirty' state of the form.
+     *
+     * @param bool $state
+     * @since Moodle 3.7.1
+     */
+    public function set_initial_dirty_state($state = false) {
+        $this->_initial_form_dirty_state = $state;
+    }
+
+    /**
+     * Is the form currently set to dirty?
+     *
+     * @return boolean Initial dirty state.
+     * @since Moodle 3.7.1
+     */
+    public function is_dirty() {
+        return $this->_initial_form_dirty_state;
+    }
+
     /**
      * Call this method if you don't want the formchangechecker JavaScript to be
      * automatically initialised for this form.
@@ -2964,7 +3001,8 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
             $PAGE->requires->yui_module('moodle-core-formchangechecker',
                     'M.core_formchangechecker.init',
                     array(array(
-                        'formid' => $formid
+                        'formid' => $formid,
+                        'initialdirtystate' => $form->is_dirty(),
                     ))
             );
             $PAGE->requires->string_for_js('changesmadereallygoaway', 'moodle');
index 5001b2f..f279879 100644 (file)
@@ -112,6 +112,7 @@ class Html2Text
         '/&#151;/i',                                     // m-dash in win-1252
         '/&(amp|#38);/i',                                // Ampersand: see converter()
         '/[ ]{2,}/',                                     // Runs of spaces, post-handling
+        '/&#39;/i',                                      // The apostrophe symbol
     );
 
     /**
@@ -125,6 +126,7 @@ class Html2Text
         '—',         // m-dash
         '|+|amp|+|', // Ampersand: see converter()
         ' ',         // Runs of spaces, post-handling
+        '\'',        // Apostrophe
     );
 
     /**
index 617729c..d6ffcde 100644 (file)
@@ -8618,7 +8618,7 @@ function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=fal
     $result = number_format($float, $decimalpoints, $separator, '');
     if ($stripzeros) {
         // Remove zeros and final dot if not needed.
-        $result = preg_replace('~(' . preg_quote($separator) . ')?0+$~', '', $result);
+        $result = preg_replace('~(' . preg_quote($separator, '~') . ')?0+$~', '', $result);
     }
     return $result;
 }
index b568f28..d9c47f0 100644 (file)
@@ -1549,7 +1549,7 @@ class theme_config {
      * @return string Return compiled css.
      */
     public function get_precompiled_css_content() {
-        $configs = [$this] + $this->parent_configs;
+        $configs = array_reverse($this->parent_configs) + [$this];
         $css = '';
 
         foreach ($configs as $config) {
index a1ff8f8..24e79f0 100644 (file)
@@ -35,9 +35,11 @@ class AtRuleBlockList extends CSSBlockList implements AtRule {
                if($sArgs) {
                        $sArgs = ' ' . $sArgs;
                }
-               $sResult = "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{";
+               $sResult  = $oOutputFormat->sBeforeAtRuleBlock;
+               $sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{";
                $sResult .= parent::render($oOutputFormat);
                $sResult .= '}';
+               $sResult .= $oOutputFormat->sAfterAtRuleBlock;
                return $sResult;
        }
 
index f9986eb..bf4efcb 100644 (file)
@@ -2,11 +2,22 @@
 
 namespace Sabberworm\CSS\CSSList;
 
+use Sabberworm\CSS\Comment\Commentable;
+use Sabberworm\CSS\Parsing\ParserState;
+use Sabberworm\CSS\Parsing\SourceException;
+use Sabberworm\CSS\Parsing\UnexpectedTokenException;
+use Sabberworm\CSS\Property\AtRule;
+use Sabberworm\CSS\Property\Charset;
+use Sabberworm\CSS\Property\CSSNamespace;
+use Sabberworm\CSS\Property\Import;
+use Sabberworm\CSS\Property\Selector;
 use Sabberworm\CSS\Renderable;
+use Sabberworm\CSS\RuleSet\AtRuleSet;
 use Sabberworm\CSS\RuleSet\DeclarationBlock;
 use Sabberworm\CSS\RuleSet\RuleSet;
-use Sabberworm\CSS\Property\Selector;
-use Sabberworm\CSS\Comment\Commentable;
+use Sabberworm\CSS\Value\CSSString;
+use Sabberworm\CSS\Value\URL;
+use Sabberworm\CSS\Value\Value;
 
 /**
  * A CSSList is the most generic container available. Its contents include RuleSet as well as other CSSList objects.
@@ -24,6 +35,147 @@ abstract class CSSList implements Renderable, Commentable {
                $this->iLineNo = $iLineNo;
        }
 
+       public static function parseList(ParserState $oParserState, CSSList $oList) {
+               $bIsRoot = $oList instanceof Document;
+               if(is_string($oParserState)) {
+                       $oParserState = new ParserState($oParserState);
+               }
+               $bLenientParsing = $oParserState->getSettings()->bLenientParsing;
+               while(!$oParserState->isEnd()) {
+                       $comments = $oParserState->consumeWhiteSpace();
+                       $oListItem = null;
+                       if($bLenientParsing) {
+                               try {
+                                       $oListItem = self::parseListItem($oParserState, $oList);
+                               } catch (UnexpectedTokenException $e) {
+                                       $oListItem = false;
+                               }
+                       } else {
+                               $oListItem = self::parseListItem($oParserState, $oList);
+                       }
+                       if($oListItem === null) {
+                               // List parsing finished
+                               return;
+                       }
+                       if($oListItem) {
+                               $oListItem->setComments($comments);
+                               $oList->append($oListItem);
+                       }
+                       $oParserState->consumeWhiteSpace();
+               }
+               if(!$bIsRoot && !$bLenientParsing) {
+                       throw new SourceException("Unexpected end of document", $oParserState->currentLine());
+               }
+       }
+
+       private static function parseListItem(ParserState $oParserState, CSSList $oList) {
+               $bIsRoot = $oList instanceof Document;
+               if ($oParserState->comes('@')) {
+                       $oAtRule = self::parseAtRule($oParserState);
+                       if($oAtRule instanceof Charset) {
+                               if(!$bIsRoot) {
+                                       throw new UnexpectedTokenException('@charset may only occur in root document', '', 'custom', $oParserState->currentLine());
+                               }
+                               if(count($oList->getContents()) > 0) {
+                                       throw new UnexpectedTokenException('@charset must be the first parseable token in a document', '', 'custom', $oParserState->currentLine());
+                               }
+                               $oParserState->setCharset($oAtRule->getCharset()->getString());
+                       }
+                       return $oAtRule;
+               } else if ($oParserState->comes('}')) {
+                       $oParserState->consume('}');
+                       if ($bIsRoot) {
+                               if ($oParserState->getSettings()->bLenientParsing) {
+                                       while ($oParserState->comes('}')) $oParserState->consume('}');
+                                       return DeclarationBlock::parse($oParserState);
+                               } else {
+                                       throw new SourceException("Unopened {", $oParserState->currentLine());
+                               }
+                       } else {
+                               return null;
+                       }
+               } else {
+                       return DeclarationBlock::parse($oParserState);
+               }
+       }
+
+       private static function parseAtRule(ParserState $oParserState) {
+               $oParserState->consume('@');
+               $sIdentifier = $oParserState->parseIdentifier();
+               $iIdentifierLineNum = $oParserState->currentLine();
+               $oParserState->consumeWhiteSpace();
+               if ($sIdentifier === 'import') {
+                       $oLocation = URL::parse($oParserState);
+                       $oParserState->consumeWhiteSpace();
+                       $sMediaQuery = null;
+                       if (!$oParserState->comes(';')) {
+                               $sMediaQuery = $oParserState->consumeUntil(';');
+                       }
+                       $oParserState->consume(';');
+                       return new Import($oLocation, $sMediaQuery, $iIdentifierLineNum);
+               } else if ($sIdentifier === 'charset') {
+                       $sCharset = CSSString::parse($oParserState);
+                       $oParserState->consumeWhiteSpace();
+                       $oParserState->consume(';');
+                       return new Charset($sCharset, $iIdentifierLineNum);
+               } else if (self::identifierIs($sIdentifier, 'keyframes')) {
+                       $oResult = new KeyFrame($iIdentifierLineNum);
+                       $oResult->setVendorKeyFrame($sIdentifier);
+                       $oResult->setAnimationName(trim($oParserState->consumeUntil('{', false, true)));
+                       CSSList::parseList($oParserState, $oResult);
+                       return $oResult;
+               } else if ($sIdentifier === 'namespace') {
+                       $sPrefix = null;
+                       $mUrl = Value::parsePrimitiveValue($oParserState);
+                       if (!$oParserState->comes(';')) {
+                               $sPrefix = $mUrl;
+                               $mUrl = Value::parsePrimitiveValue($oParserState);
+                       }
+                       $oParserState->consume(';');
+                       if ($sPrefix !== null && !is_string($sPrefix)) {
+                               throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum);
+                       }
+                       if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) {
+                               throw new UnexpectedTokenException('Wrong namespace url of invalid type', $mUrl, 'custom', $iIdentifierLineNum);
+                       }
+                       return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum);
+               } else {
+                       //Unknown other at rule (font-face or such)
+                       $sArgs = trim($oParserState->consumeUntil('{', false, true));
+                       if (substr_count($sArgs, "(") != substr_count($sArgs, ")")) {
+                               if($oParserState->getSettings()->bLenientParsing) {
+                                       return NULL;
+                               } else {
+                                       throw new SourceException("Unmatched brace count in media query", $oParserState->currentLine());
+                               }
+                       }
+                       $bUseRuleSet = true;
+                       foreach(explode('/', AtRule::BLOCK_RULES) as $sBlockRuleName) {
+                               if(self::identifierIs($sIdentifier, $sBlockRuleName)) {
+                                       $bUseRuleSet = false;
+                                       break;
+                               }
+                       }
+                       if($bUseRuleSet) {
+                               $oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum);
+                               RuleSet::parseRuleSet($oParserState, $oAtRule);
+                       } else {
+                               $oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum);
+                               CSSList::parseList($oParserState, $oAtRule);
+                       }
+                       return $oAtRule;
+               }
+       }
+
+               /**
+        * Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed. We need to check for these versions too.
+        */
+       private static function identifierIs($sIdentifier, $sMatch) {
+               return (strcasecmp($sIdentifier, $sMatch) === 0)
+                       ?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1;
+       }
+
+
        /**
         * @return int
         */
@@ -31,27 +183,53 @@ abstract class CSSList implements Renderable, Commentable {
                return $this->iLineNo;
        }
 
+       /**
+        * Prepend item to list of contents.
+        *
+        * @param object $oItem Item.
+        */
+       public function prepend($oItem) {
+               array_unshift($this->aContents, $oItem);
+       }
+
+       /**
+        * Append item to list of contents.
+        *
+        * @param object $oItem Item.
+        */
        public function append($oItem) {
                $this->aContents[] = $oItem;
        }
 
        /**
-        * Insert an item before its sibling.
+        * Splice the list of contents.
         *
-        * @param mixed $oItem The item.
-        * @param mixed $oSibling The sibling.
+        * @param int       $iOffset      Offset.
+        * @param int       $iLength      Length. Optional.
+        * @param RuleSet[] $mReplacement Replacement. Optional.
         */
-       public function insert($oItem, $oSibling) {
-               $iIndex = array_search($oSibling, $this->aContents);
-               if ($iIndex === false) {
-                       return $this->append($oItem);
-               }
-               array_splice($this->aContents, $iIndex, 0, array($oItem));
+       public function splice($iOffset, $iLength = null, $mReplacement = null) {
+               array_splice($this->aContents, $iOffset, $iLength, $mReplacement);
        }
 
+    /**
+     * Insert an item before its sibling.
+     *
+     * @param mixed $oItem The item.
+     * @param mixed $oSibling The sibling.
+     */
+    public function insert($oItem, $oSibling) {
+        $iIndex = array_search($oSibling, $this->aContents);
+        if ($iIndex === false) {
+            return $this->append($oItem);
+        }
+        array_splice($this->aContents, $iIndex, 0, array($oItem));
+    }
+
        /**
         * Removes an item from the CSS list.
         * @param RuleSet|Import|Charset|CSSList $oItemToRemove May be a RuleSet (most likely a DeclarationBlock), a Import, a Charset or another CSSList (most likely a MediaQuery)
+        * @return bool Whether the item was removed.
         */
        public function remove($oItemToRemove) {
                $iKey = array_search($oItemToRemove, $this->aContents, true);
@@ -62,6 +240,19 @@ abstract class CSSList implements Renderable, Commentable {
                return false;
        }
 
+       /**
+        * Replaces an item from the CSS list.
+        * @param RuleSet|Import|Charset|CSSList $oItemToRemove May be a RuleSet (most likely a DeclarationBlock), a Import, a Charset or another CSSList (most likely a MediaQuery)
+        */
+       public function replace($oOldItem, $oNewItem) {
+               $iKey = array_search($oOldItem, $this->aContents, true);
+               if ($iKey !== false) {
+                       array_splice($this->aContents, $iKey, 1, $oNewItem);
+                       return true;
+               }
+               return false;
+       }
+
        /**
         * Set the contents.
         * @param array $aContents Objects to set as content.
@@ -137,7 +328,7 @@ abstract class CSSList implements Renderable, Commentable {
 
                return $sResult;
        }
-       
+
        /**
        * Return true if the list can not be further outdented. Only important when rendering.
        */
index bd4a23e..873df75 100644 (file)
@@ -2,6 +2,8 @@
 
 namespace Sabberworm\CSS\CSSList;
 
+use Sabberworm\CSS\Parsing\ParserState;
+
 /**
  * The root CSSList of a parsed file. Contains all top-level css contents, mostly declaration blocks, but also any @-rules encountered.
  */
@@ -14,6 +16,12 @@ class Document extends CSSBlockList {
                parent::__construct($iLineNo);
        }
 
+       public static function parse(ParserState $oParserState) {
+               $oDocument = new Document($oParserState->currentLine());
+               CSSList::parseList($oParserState, $oDocument);
+               return $oDocument;
+       }
+
        /**
         * Gets all DeclarationBlock objects recursively.
         */
index 1b17984..f7ebb5a 100644 (file)
@@ -4,6 +4,11 @@ namespace Sabberworm\CSS;
 
 use Sabberworm\CSS\Parsing\OutputException;
 
+/**
+ * Class OutputFormat
+ *
+ * @method OutputFormat setSemicolonAfterLastRule( bool $bSemicolonAfterLastRule ) Set whether semicolons are added after last rule.
+ */
 class OutputFormat {
        /**
        * Value format
@@ -35,6 +40,10 @@ class OutputFormat {
        public $sSpaceAfterBlocks = '';
        public $sSpaceBetweenBlocks = "\n";
 
+       // Content injected in and around @-rule blocks.
+       public $sBeforeAtRuleBlock = '';
+       public $sAfterAtRuleBlock = '';
+
        // This is what’s printed before and after the comma if a declaration block contains multiple selectors.
        public $sSpaceBeforeSelectorSeparator = '';
        public $sSpaceAfterSelectorSeparator = ' ';
@@ -43,7 +52,12 @@ class OutputFormat {
        public $sSpaceAfterListArgumentSeparator = '';
        
        public $sSpaceBeforeOpeningBrace = ' ';
-       
+
+       // Content injected in and around declaration blocks.
+       public $sBeforeDeclarationBlock = '';
+       public $sAfterDeclarationBlockSelectors = '';
+       public $sAfterDeclarationBlock = '';
+
        /**
        * Indentation
        */
@@ -141,17 +155,36 @@ class OutputFormat {
        public function level() {
                return $this->iIndentationLevel;
        }
-       
+
+       /**
+        * Create format.
+        *
+        * @return OutputFormat Format.
+        */
        public static function create() {
                return new OutputFormat();
        }
-       
+
+       /**
+        * Create compact format.
+        *
+        * @return OutputFormat Format.
+        */
        public static function createCompact() {
-               return self::create()->set('Space*Rules', "")->set('Space*Blocks', "")->setSpaceAfterRuleName('')->setSpaceBeforeOpeningBrace('')->setSpaceAfterSelectorSeparator('');
+               $format = self::create();
+               $format->set('Space*Rules', "")->set('Space*Blocks', "")->setSpaceAfterRuleName('')->setSpaceBeforeOpeningBrace('')->setSpaceAfterSelectorSeparator('');
+               return $format;
        }
-       
+
+       /**
+        * Create pretty format.
+        *
+        * @return OutputFormat Format.
+        */
        public static function createPretty() {
-               return self::create()->set('Space*Rules', "\n")->set('Space*Blocks', "\n")->setSpaceBetweenBlocks("\n\n")->set('SpaceAfterListArgumentSeparator', array('default' => '', ',' => ' '));
+               $format = self::create();
+               $format->set('Space*Rules', "\n")->set('Space*Blocks', "\n")->setSpaceBetweenBlocks("\n\n")->set('SpaceAfterListArgumentSeparator', array('default' => '', ',' => ' '));
+               return $format;
        }
 }
 
@@ -286,4 +319,4 @@ class OutputFormatter {
        private function indent() {
                return str_repeat($this->oFormat->sIndentation, $this->oFormat->level());
        }
-}
\ No newline at end of file
+}
index 65ea2f0..2520cb3 100644 (file)
@@ -2,41 +2,14 @@
 
 namespace Sabberworm\CSS;
 
-use Sabberworm\CSS\CSSList\CSSList;
 use Sabberworm\CSS\CSSList\Document;
-use Sabberworm\CSS\CSSList\KeyFrame;
-use Sabberworm\CSS\Parsing\SourceException;
-use Sabberworm\CSS\Property\AtRule;
-use Sabberworm\CSS\Property\Import;
-use Sabberworm\CSS\Property\Charset;
-use Sabberworm\CSS\Property\CSSNamespace;
-use Sabberworm\CSS\RuleSet\AtRuleSet;
-use Sabberworm\CSS\CSSList\AtRuleBlockList;
-use Sabberworm\CSS\RuleSet\DeclarationBlock;
-use Sabberworm\CSS\Value\CSSFunction;
-use Sabberworm\CSS\Value\RuleValueList;
-use Sabberworm\CSS\Value\Size;
-use Sabberworm\CSS\Value\Color;
-use Sabberworm\CSS\Value\URL;
-use Sabberworm\CSS\Value\CSSString;
-use Sabberworm\CSS\Rule\Rule;
-use Sabberworm\CSS\Parsing\UnexpectedTokenException;
-use Sabberworm\CSS\Comment\Comment;
+use Sabberworm\CSS\Parsing\ParserState;
 
 /**
  * Parser class parses CSS from text into a data structure.
  */
 class Parser {
-
-       private $sText;
-       private $aText;
-       private $iCurrentPosition;
-       private $oParserSettings;
-       private $sCharset;
-       private $iLength;
-       private $blockRules;
-       private $aSizeUnits;
-       private $iLineNo;
+       private $oParserState;
 
        /**
         * Parser constructor.
@@ -47,682 +20,22 @@ class Parser {
         * @param int $iLineNo
         */
        public function __construct($sText, Settings $oParserSettings = null, $iLineNo = 1) {
-               $this->sText = $sText;
-               $this->iCurrentPosition = 0;
-               $this->iLineNo = $iLineNo;
                if ($oParserSettings === null) {
                        $oParserSettings = Settings::create();
                }
-               $this->oParserSettings = $oParserSettings;
-               $this->blockRules = explode('/', AtRule::BLOCK_RULES);
-
-               foreach (explode('/', Size::ABSOLUTE_SIZE_UNITS.'/'.Size::RELATIVE_SIZE_UNITS.'/'.Size::NON_SIZE_UNITS) as $val) {
-                       $iSize = strlen($val);
-                       if(!isset($this->aSizeUnits[$iSize])) {
-                               $this->aSizeUnits[$iSize] = array();
-                       }
-                       $this->aSizeUnits[$iSize][strtolower($val)] = $val;
-               }
-               ksort($this->aSizeUnits, SORT_NUMERIC);
+               $this->oParserState = new ParserState($sText, $oParserSettings, $iLineNo);
        }
 
        public function setCharset($sCharset) {
-               $this->sCharset = $sCharset;
-               $this->aText = $this->strsplit($this->sText);
-               $this->iLength = count($this->aText);
+               $this->oParserState->setCharset($sCharset);
        }
 
        public function getCharset() {
-               return $this->sCharset;
+               $this->oParserState->getCharset();
        }
 
        public function parse() {
-               $this->setCharset($this->oParserSettings->sDefaultCharset);
-               $oResult = new Document($this->iLineNo);
-               $this->parseDocument($oResult);
-               return $oResult;
-       }
-
-       private function parseDocument(Document $oDocument) {
-               $this->parseList($oDocument, true);
-       }
-
-       private function parseList(CSSList $oList, $bIsRoot = false) {
-               while (!$this->isEnd()) {
-                       $comments = $this->consumeWhiteSpace();
-                       $oListItem = null;
-                       if($this->oParserSettings->bLenientParsing) {
-                               try {
-                                       $oListItem = $this->parseListItem($oList, $bIsRoot);
-                               } catch (UnexpectedTokenException $e) {
-                                       $oListItem = false;
-                               }
-                       } else {
-                               $oListItem = $this->parseListItem($oList, $bIsRoot);
-                       }
-                       if($oListItem === null) {
-                               // List parsing finished
-                               return;
-                       }
-                       if($oListItem) {
-                               $oListItem->setComments($comments);
-                               $oList->append($oListItem);
-                       }
-               }
-               if (!$bIsRoot) {
-                       throw new SourceException("Unexpected end of document", $this->iLineNo);
-               }
-       }
-       
-       private function parseListItem(CSSList $oList, $bIsRoot = false) {
-               if ($this->comes('@')) {
-                       $oAtRule = $this->parseAtRule();
-                       if($oAtRule instanceof Charset) {
-                               if(!$bIsRoot) {
-                                       throw new UnexpectedTokenException('@charset may only occur in root document', '', 'custom', $this->iLineNo);
-                               }
-                               if(count($oList->getContents()) > 0) {
-                                       throw new UnexpectedTokenException('@charset must be the first parseable token in a document', '', 'custom', $this->iLineNo);
-                               }
-                               $this->setCharset($oAtRule->getCharset()->getString());
-                       }
-                       return $oAtRule;
-               } else if ($this->comes('}')) {
-                       $this->consume('}');
-                       if ($bIsRoot) {
-                               throw new SourceException("Unopened {", $this->iLineNo);
-                       } else {
-                               return null;
-                       }
-               } else {
-                       return $this->parseSelector();
-               }
-       }
-
-       private function parseAtRule() {
-               $this->consume('@');
-               $sIdentifier = $this->parseIdentifier(false);
-               $iIdentifierLineNum = $this->iLineNo;
-               $this->consumeWhiteSpace();
-               if ($sIdentifier === 'import') {
-                       $oLocation = $this->parseURLValue();
-                       $this->consumeWhiteSpace();
-                       $sMediaQuery = null;
-                       if (!$this->comes(';')) {
-                               $sMediaQuery = $this->consumeUntil(';');
-                       }
-                       $this->consume(';');
-                       return new Import($oLocation, $sMediaQuery, $iIdentifierLineNum);
-               } else if ($sIdentifier === 'charset') {
-                       $sCharset = $this->parseStringValue();
-                       $this->consumeWhiteSpace();
-                       $this->consume(';');
-                       return new Charset($sCharset, $iIdentifierLineNum);
-               } else if ($this->identifierIs($sIdentifier, 'keyframes')) {
-                       $oResult = new KeyFrame($iIdentifierLineNum);
-                       $oResult->setVendorKeyFrame($sIdentifier);
-                       $oResult->setAnimationName(trim($this->consumeUntil('{', false, true)));
-                       $this->parseList($oResult);
-                       return $oResult;
-               } else if ($sIdentifier === 'namespace') {
-                       $sPrefix = null;
-                       $mUrl = $this->parsePrimitiveValue();
-                       if (!$this->comes(';')) {
-                               $sPrefix = $mUrl;
-                               $mUrl = $this->parsePrimitiveValue();
-                       }
-                       $this->consume(';');
-                       if ($sPrefix !== null && !is_string($sPrefix)) {
-                               throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum);
-                       }
-                       if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) {
-                               throw new UnexpectedTokenException('Wrong namespace url of invalid type', $mUrl, 'custom', $iIdentifierLineNum);
-                       }
-                       return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum);
-               } else {
-                       //Unknown other at rule (font-face or such)
-                       $sArgs = trim($this->consumeUntil('{', false, true));
-                       $bUseRuleSet = true;
-                       foreach($this->blockRules as $sBlockRuleName) {
-                               if($this->identifierIs($sIdentifier, $sBlockRuleName)) {
-                                       $bUseRuleSet = false;
-                                       break;
-                               }
-                       }
-                       if($bUseRuleSet) {
-                               $oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum);
-                               $this->parseRuleSet($oAtRule);
-                       } else {
-                               $oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum);
-                               $this->parseList($oAtRule);
-                       }
-                       return $oAtRule;
-               }
-       }
-
-       private function parseIdentifier($bAllowFunctions = true, $bIgnoreCase = true) {
-               $sResult = $this->parseCharacter(true);
-               if ($sResult === null) {
-                       throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo);
-               }
-               $sCharacter = null;
-               while (($sCharacter = $this->parseCharacter(true)) !== null) {
-                       $sResult .= $sCharacter;
-               }
-               if ($bIgnoreCase) {
-                       $sResult = $this->strtolower($sResult);
-               }
-               if ($bAllowFunctions && $this->comes('(')) {
-                       $this->consume('(');
-                       $aArguments = $this->parseValue(array('=', ' ', ','));
-                       $sResult = new CSSFunction($sResult, $aArguments, ',', $this->iLineNo);
-                       $this->consume(')');
-               }
-               return $sResult;
-       }
-
-       private function parseStringValue() {
-               $sBegin = $this->peek();
-               $sQuote = null;
-               if ($sBegin === "'") {
-                       $sQuote = "'";
-               } else if ($sBegin === '"') {
-                       $sQuote = '"';
-               }
-               if ($sQuote !== null) {
-                       $this->consume($sQuote);
-               }
-               $sResult = "";
-               $sContent = null;
-               if ($sQuote === null) {
-                       //Unquoted strings end in whitespace or with braces, brackets, parentheses
-                       while (!preg_match('/[\\s{}()<>\\[\\]]/isu', $this->peek())) {
-                               $sResult .= $this->parseCharacter(false);
-                       }
-               } else {
-                       while (!$this->comes($sQuote)) {
-                               $sContent = $this->parseCharacter(false);
-                               if ($sContent === null) {
-                                       throw new SourceException("Non-well-formed quoted string {$this->peek(3)}", $this->iLineNo);
-                               }
-                               $sResult .= $sContent;
-                       }
-                       $this->consume($sQuote);
-               }
-               return new CSSString($sResult, $this->iLineNo);
-       }
-
-       private function parseCharacter($bIsForIdentifier) {
-               if ($this->peek() === '\\') {
-                       if ($bIsForIdentifier && $this->oParserSettings->bLenientParsing && ($this->comes('\0') || $this->comes('\9'))) {
-                               // Non-strings can contain \0 or \9 which is an IE hack supported in lenient parsing.
-                               return null;
-                       }
-                       $this->consume('\\');
-                       if ($this->comes('\n') || $this->comes('\r')) {
-                               return '';
-                       }
-                       if (preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) {
-                               return $this->consume(1);
-                       }
-                       $sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6);
-                       if ($this->strlen($sUnicode) < 6) {
-                               //Consume whitespace after incomplete unicode escape
-                               if (preg_match('/\\s/isSu', $this->peek())) {
-                                       if ($this->comes('\r\n')) {
-                                               $this->consume(2);
-                                       } else {
-                                               $this->consume(1);
-                                       }
-                               }
-                       }
-                       $iUnicode = intval($sUnicode, 16);
-                       $sUtf32 = "";
-                       for ($i = 0; $i < 4; ++$i) {
-                               $sUtf32 .= chr($iUnicode & 0xff);
-                               $iUnicode = $iUnicode >> 8;
-                       }
-                       return iconv('utf-32le', $this->sCharset, $sUtf32);
-               }
-               if ($bIsForIdentifier) {
-                       $peek = ord($this->peek());
-                       // Ranges: a-z A-Z 0-9 - _
-                       if (($peek >= 97 && $peek <= 122) ||
-                               ($peek >= 65 && $peek <= 90) ||
-                               ($peek >= 48 && $peek <= 57) ||
-                               ($peek === 45) ||
-                               ($peek === 95) ||
-                               ($peek > 0xa1)) {
-                               return $this->consume(1);
-                       }
-               } else {
-                       return $this->consume(1);
-               }
-               return null;
-       }
-
-       private function parseSelector() {
-               $aComments = array();
-               $oResult = new DeclarationBlock($this->iLineNo);
-               $oResult->setSelector($this->consumeUntil('{', false, true, $aComments));
-               $oResult->setComments($aComments);
-               $this->parseRuleSet($oResult);
-               return $oResult;
-       }
-
-       private function parseRuleSet($oRuleSet) {
-               while ($this->comes(';')) {
-                       $this->consume(';');
-               }
-               while (!$this->comes('}')) {
-                       $oRule = null;
-                       if($this->oParserSettings->bLenientParsing) {
-                               try {
-                                       $oRule = $this->parseRule();
-                               } catch (UnexpectedTokenException $e) {
-                                       try {
-                                               $sConsume = $this->consumeUntil(array("\n", ";", '}'), true);
-                                               // We need to “unfind” the matches to the end of the ruleSet as this will be matched later
-                                               if($this->streql(substr($sConsume, -1), '}')) {
-                                                       --$this->iCurrentPosition;
-                                               } else {
-                                                       while ($this->comes(';')) {
-                                                               $this->consume(';');
-                                                       }
-                                               }
-                                       } catch (UnexpectedTokenException $e) {
-                                               // We’ve reached the end of the document. Just close the RuleSet.
-                                               return;
-                                       }
-                               }
-                       } else {
-                               $oRule = $this->parseRule();
-                       }
-                       if($oRule) {
-                               $oRuleSet->addRule($oRule);
-                       }
-               }
-               $this->consume('}');
-       }
-
-       private function parseRule() {
-               $aComments = $this->consumeWhiteSpace();
-               $oRule = new Rule($this->parseIdentifier(), $this->iLineNo);
-               $oRule->setComments($aComments);
-               $oRule->addComments($this->consumeWhiteSpace());
-               $this->consume(':');
-               $oValue = $this->parseValue(self::listDelimiterForRule($oRule->getRule()));
-               $oRule->setValue($oValue);
-               if ($this->oParserSettings->bLenientParsing) {
-                       while ($this->comes('\\')) {
-                               $this->consume('\\');
-                               $oRule->addIeHack($this->consume());
-                               $this->consumeWhiteSpace();
-                       }
-               }
-               if ($this->comes('!')) {
-                       $this->consume('!');
-                       $this->consumeWhiteSpace();
-                       $this->consume('important');
-                       $oRule->setIsImportant(true);
-               }
-               while ($this->comes(';')) {
-                       $this->consume(';');
-               }
-               return $oRule;
-       }
-
-       private function parseValue($aListDelimiters) {
-               $aStack = array();
-               $this->consumeWhiteSpace();
-               //Build a list of delimiters and parsed values
-               while (!($this->comes('}') || $this->comes(';') || $this->comes('!') || $this->comes(')') || $this->comes('\\'))) {
-                       if (count($aStack) > 0) {
-                               $bFoundDelimiter = false;
-                               foreach ($aListDelimiters as $sDelimiter) {
-                                       if ($this->comes($sDelimiter)) {
-                                               array_push($aStack, $this->consume($sDelimiter));
-                                               $this->consumeWhiteSpace();
-                                               $bFoundDelimiter = true;
-                                               break;
-                                       }
-                               }
-                               if (!$bFoundDelimiter) {
-                                       //Whitespace was the list delimiter
-                                       array_push($aStack, ' ');
-                               }
-                       }
-                       array_push($aStack, $this->parsePrimitiveValue());
-                       $this->consumeWhiteSpace();
-               }
-               //Convert the list to list objects
-               foreach ($aListDelimiters as $sDelimiter) {
-                       if (count($aStack) === 1) {
-                               return $aStack[0];
-                       }
-                       $iStartPosition = null;
-                       while (($iStartPosition = array_search($sDelimiter, $aStack, true)) !== false) {
-                               $iLength = 2; //Number of elements to be joined
-                               for ($i = $iStartPosition + 2; $i < count($aStack); $i+=2, ++$iLength) {
-                                       if ($sDelimiter !== $aStack[$i]) {
-                                               break;
-                                       }
-                               }
-                               $oList = new RuleValueList($sDelimiter, $this->iLineNo);
-                               for ($i = $iStartPosition - 1; $i - $iStartPosition + 1 < $iLength * 2; $i+=2) {
-                                       $oList->addListComponent($aStack[$i]);
-                               }
-                               array_splice($aStack, $iStartPosition - 1, $iLength * 2 - 1, array($oList));
-                       }
-               }
-               return $aStack[0];
-       }
-
-       private static function listDelimiterForRule($sRule) {
-               if (preg_match('/^font($|-)/', $sRule)) {
-                       return array(',', '/', ' ');
-               }
-               return array(',', ' ', '/');
-       }
-
-       private function parsePrimitiveValue() {
-               $oValue = null;
-               $this->consumeWhiteSpace();
-               if (is_numeric($this->peek()) || ($this->comes('-.') && is_numeric($this->peek(1, 2))) || (($this->comes('-') || $this->comes('.')) && is_numeric($this->peek(1, 1)))) {
-                       $oValue = $this->parseNumericValue();
-               } else if ($this->comes('#') || $this->comes('rgb', true) || $this->comes('hsl', true)) {
-                       $oValue = $this->parseColorValue();
-               } else if ($this->comes('url', true)) {
-                       $oValue = $this->parseURLValue();
-               } else if ($this->comes("'") || $this->comes('"')) {
-                       $oValue = $this->parseStringValue();
-               } else if ($this->comes("progid:") && $this->oParserSettings->bLenientParsing) {
-                       $oValue = $this->parseMicrosoftFilter();
-               } else {
-                       $oValue = $this->parseIdentifier(true, false);
-               }
-               $this->consumeWhiteSpace();
-               return $oValue;
-       }
-
-       private function parseNumericValue($bForColor = false) {
-               $sSize = '';
-               if ($this->comes('-')) {
-                       $sSize .= $this->consume('-');
-               }
-               while (is_numeric($this->peek()) || $this->comes('.')) {
-                       if ($this->comes('.')) {
-                               $sSize .= $this->consume('.');
-                       } else {
-                               $sSize .= $this->consume(1);
-                       }
-               }
-
-               $sUnit = null;
-               foreach ($this->aSizeUnits as $iLength => &$aValues) {
-                       $sKey = strtolower($this->peek($iLength));
-                       if(array_key_exists($sKey, $aValues)) {
-                               if (($sUnit = $aValues[$sKey]) !== null) {
-                                       $this->consume($iLength);
-                                       break;
-                               }
-                       }
-               }
-               return new Size(floatval($sSize), $sUnit, $bForColor, $this->iLineNo);
-       }
-
-       private function parseColorValue() {
-               $aColor = array();
-               if ($this->comes('#')) {
-                       $this->consume('#');
-                       $sValue = $this->parseIdentifier(false);
-                       if ($this->strlen($sValue) === 3) {
-                               $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2];
-                       }
-                       $aColor = array('r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $this->iLineNo), 'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $this->iLineNo), 'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $this->iLineNo));
-               } else {
-                       $sColorMode = $this->parseIdentifier(false);
-                       $this->consumeWhiteSpace();
-                       $this->consume('(');
-                       $iLength = $this->strlen($sColorMode);
-                       for ($i = 0; $i < $iLength; ++$i) {
-                               $this->consumeWhiteSpace();
-                               $aColor[$sColorMode[$i]] = $this->parseNumericValue(true);
-                               $this->consumeWhiteSpace();
-                               if ($i < ($iLength - 1)) {
-                                       $this->consume(',');
-                               }
-                       }
-                       $this->consume(')');
-               }
-               return new Color($aColor, $this->iLineNo);
-       }
-
-       private function parseMicrosoftFilter() {
-               $sFunction = $this->consumeUntil('(', false, true);
-               $aArguments = $this->parseValue(array(',', '='));
-               return new CSSFunction($sFunction, $aArguments, ',', $this->iLineNo);
-       }
-
-       private function parseURLValue() {
-               $bUseUrl = $this->comes('url', true);
-               if ($bUseUrl) {
-                       $this->consume('url');
-                       $this->consumeWhiteSpace();
-                       $this->consume('(');
-               }
-               $this->consumeWhiteSpace();
-               $oResult = new URL($this->parseStringValue(), $this->iLineNo);
-               if ($bUseUrl) {
-                       $this->consumeWhiteSpace();
-                       $this->consume(')');
-               }
-               return $oResult;
-       }
-
-       /**
-        * Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed. We need to check for these versions too.
-        */
-       private function identifierIs($sIdentifier, $sMatch) {
-               return (strcasecmp($sIdentifier, $sMatch) === 0)
-                       ?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1;
-       }
-
-       private function comes($sString, $bCaseInsensitive = false) {
-               $sPeek = $this->peek(strlen($sString));
-               return ($sPeek == '')
-                       ? false
-                       : $this->streql($sPeek, $sString, $bCaseInsensitive);
-       }
-
-       private function peek($iLength = 1, $iOffset = 0) {
-               $iOffset += $this->iCurrentPosition;
-               if ($iOffset >= $this->iLength) {
-                       return '';
-               }
-               return $this->substr($iOffset, $iLength);
-       }
-
-       private function consume($mValue = 1) {
-               if (is_string($mValue)) {
-                       $iLineCount = substr_count($mValue, "\n");
-                       $iLength = $this->strlen($mValue);
-                       if (!$this->streql($this->substr($this->iCurrentPosition, $iLength), $mValue)) {
-                               throw new UnexpectedTokenException($mValue, $this->peek(max($iLength, 5)), $this->iLineNo);
-                       }
-                       $this->iLineNo += $iLineCount;
-                       $this->iCurrentPosition += $this->strlen($mValue);
-                       return $mValue;
-               } else {
-                       if ($this->iCurrentPosition + $mValue > $this->iLength) {
-                               throw new UnexpectedTokenException($mValue, $this->peek(5), 'count', $this->iLineNo);
-                       }
-                       $sResult = $this->substr($this->iCurrentPosition, $mValue);
-                       $iLineCount = substr_count($sResult, "\n");
-                       $this->iLineNo += $iLineCount;
-                       $this->iCurrentPosition += $mValue;
-                       return $sResult;
-               }
-       }
-
-       private function consumeExpression($mExpression, $iMaxLength = null) {
-               $aMatches = null;
-               $sInput = $iMaxLength !== null ? $this->peek($iMaxLength) : $this->inputLeft();
-               if (preg_match($mExpression, $sInput, $aMatches, PREG_OFFSET_CAPTURE) === 1) {
-                       return $this->consume($aMatches[0][0]);
-               }
-               throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo);
-       }
-
-       private function consumeWhiteSpace() {
-               $comments = array();
-               do {
-                       while (preg_match('/\\s/isSu', $this->peek()) === 1) {
-                               $this->consume(1);
-                       }
-                       if($this->oParserSettings->bLenientParsing) {
-                               try {
-                                       $oComment = $this->consumeComment();
-                               } catch(UnexpectedTokenException $e) {
-                                       // When we can’t find the end of a comment, we assume the document is finished.
-                                       $this->iCurrentPosition = $this->iLength;
-                                       return;
-                               }
-                       } else {
-                               $oComment = $this->consumeComment();
-                       }
-                       if ($oComment !== false) {
-                               $comments[] = $oComment;
-                       }
-               } while($oComment !== false);
-               return $comments;
-       }
-
-       /**
-        * @return false|Comment
-        */
-       private function consumeComment() {
-               $mComment = false;
-               if ($this->comes('/*')) {
-                       $iLineNo = $this->iLineNo;
-                       $this->consume(1);
-                       $mComment = '';
-                       while (($char = $this->consume(1)) !== '') {
-                               $mComment .= $char;
-                               if ($this->comes('*/')) {
-                                       $this->consume(2);
-                                       break;
-                               }
-                       }
-               }
-
-               if ($mComment !== false) {
-                       // We skip the * which was included in the comment.
-                       return new Comment(substr($mComment, 1), $iLineNo);
-               }
-
-               return $mComment;
-       }
-
-       private function isEnd() {
-               return $this->iCurrentPosition >= $this->iLength;
-       }
-
-       private function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = array()) {
-               $aEnd = is_array($aEnd) ? $aEnd : array($aEnd);
-               $out = '';
-               $start = $this->iCurrentPosition;
-
-               while (($char = $this->consume(1)) !== '') {
-                       if (in_array($char, $aEnd)) {
-                               if ($bIncludeEnd) {
-                                       $out .= $char;
-                               } elseif (!$consumeEnd) {
-                                       $this->iCurrentPosition -= $this->strlen($char);
-                               }
-                               return $out;
-                       }
-                       $out .= $char;
-                       if ($comment = $this->consumeComment()) {
-                               $comments[] = $comment;
-                       }
-               }
-
-               $this->iCurrentPosition = $start;
-               throw new UnexpectedTokenException('One of ("'.implode('","', $aEnd).'")', $this->peek(5), 'search', $this->iLineNo);
-       }
-
-       private function inputLeft() {
-               return $this->substr($this->iCurrentPosition, -1);
-       }
-
-       private function substr($iStart, $iLength) {
-               if ($iLength < 0) {
-                       $iLength = $this->iLength - $iStart + $iLength;
-               }
-               if ($iStart + $iLength > $this->iLength) {
-                       $iLength = $this->iLength - $iStart;
-               }
-               $sResult = '';
-               while ($iLength > 0) {
-                       $sResult .= $this->aText[$iStart];
-                       $iStart++;
-                       $iLength--;
-               }
-               return $sResult;
-       }
-
-       private function strlen($sString) {
-               if ($this->oParserSettings->bMultibyteSupport) {
-                       return mb_strlen($sString, $this->sCharset);
-               } else {
-                       return strlen($sString);
-               }
-       }
-
-       private function streql($sString1, $sString2, $bCaseInsensitive = true) {
-               if($bCaseInsensitive) {
-                       return $this->strtolower($sString1) === $this->strtolower($sString2);
-               } else {
-                       return $sString1 === $sString2;
-               }
-       }
-
-       private function strtolower($sString) {
-               if ($this->oParserSettings->bMultibyteSupport) {
-                       return mb_strtolower($sString, $this->sCharset);
-               } else {
-                       return strtolower($sString);
-               }
-       }
-
-       private function strsplit($sString) {
-               if ($this->oParserSettings->bMultibyteSupport) {
-                       if ($this->streql($this->sCharset, 'utf-8')) {
-                               return preg_split('//u', $sString, null, PREG_SPLIT_NO_EMPTY);
-                       } else {
-                               $iLength = mb_strlen($sString, $this->sCharset);
-                               $aResult = array();
-                               for ($i = 0; $i < $iLength; ++$i) {
-                                       $aResult[] = mb_substr($sString, $i, 1, $this->sCharset);
-                               }
-                               return $aResult;
-                       }
-               } else {
-                       if($sString === '') {
-                               return array();
-                       } else {
-                               return str_split($sString);
-                       }
-               }
-       }
-
-       private function strpos($sString, $sNeedle, $iOffset) {
-               if ($this->oParserSettings->bMultibyteSupport) {
-                       return mb_strpos($sString, $sNeedle, $iOffset, $this->sCharset);
-               } else {
-                       return strpos($sString, $sNeedle, $iOffset);
-               }
+               return Document::parse($this->oParserState);
        }
 
 }
diff --git a/lib/php-css-parser/Parsing/ParserState.php b/lib/php-css-parser/Parsing/ParserState.php
new file mode 100644 (file)
index 0000000..4305c9a
--- /dev/null
@@ -0,0 +1,310 @@
+<?php
+namespace Sabberworm\CSS\Parsing;
+
+use Sabberworm\CSS\Comment\Comment;
+use Sabberworm\CSS\Parsing\UnexpectedTokenException;
+use Sabberworm\CSS\Settings;
+
+class ParserState {
+       private $oParserSettings;
+
+       private $sText;
+
+       private $aText;
+       private $iCurrentPosition;
+       private $sCharset;
+       private $iLength;
+       private $iLineNo;
+
+       public function __construct($sText, Settings $oParserSettings, $iLineNo = 1) {
+               $this->oParserSettings = $oParserSettings;
+               $this->sText = $sText;
+               $this->iCurrentPosition = 0;
+               $this->iLineNo = $iLineNo;
+               $this->setCharset($this->oParserSettings->sDefaultCharset);
+       }
+
+       public function setCharset($sCharset) {
+               $this->sCharset = $sCharset;
+               $this->aText = $this->strsplit($this->sText);
+               $this->iLength = count($this->aText);
+       }
+
+       public function getCharset() {
+               $this->oParserHelper->getCharset();
+               return $this->sCharset;
+       }
+
+       public function currentLine() {
+               return $this->iLineNo;
+       }
+
+       public function getSettings() {
+               return $this->oParserSettings;
+       }
+
+       public function parseIdentifier($bIgnoreCase = true) {
+               $sResult = $this->parseCharacter(true);
+               if ($sResult === null) {
+                       throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo);
+               }
+               $sCharacter = null;
+               while (($sCharacter = $this->parseCharacter(true)) !== null) {
+                       $sResult .= $sCharacter;
+               }
+               if ($bIgnoreCase) {
+                       $sResult = $this->strtolower($sResult);
+               }
+               return $sResult;
+       }
+
+       public function parseCharacter($bIsForIdentifier) {
+               if ($this->peek() === '\\') {
+                       if ($bIsForIdentifier && $this->oParserSettings->bLenientParsing && ($this->comes('\0') || $this->comes('\9'))) {
+                               // Non-strings can contain \0 or \9 which is an IE hack supported in lenient parsing.
+                               return null;
+                       }
+                       $this->consume('\\');
+                       if ($this->comes('\n') || $this->comes('\r')) {
+                               return '';
+                       }
+                       if (preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) {
+                               return $this->consume(1);
+                       }
+                       $sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6);
+                       if ($this->strlen($sUnicode) < 6) {
+                               //Consume whitespace after incomplete unicode escape
+                               if (preg_match('/\\s/isSu', $this->peek())) {
+                                       if ($this->comes('\r\n')) {
+                                               $this->consume(2);
+                                       } else {
+                                               $this->consume(1);
+                                       }
+                               }
+                       }
+                       $iUnicode = intval($sUnicode, 16);
+                       $sUtf32 = "";
+                       for ($i = 0; $i < 4; ++$i) {
+                               $sUtf32 .= chr($iUnicode & 0xff);
+                               $iUnicode = $iUnicode >> 8;
+                       }
+                       return iconv('utf-32le', $this->sCharset, $sUtf32);
+               }
+               if ($bIsForIdentifier) {
+                       $peek = ord($this->peek());
+                       // Ranges: a-z A-Z 0-9 - _
+                       if (($peek >= 97 && $peek <= 122) ||
+                               ($peek >= 65 && $peek <= 90) ||
+                               ($peek >= 48 && $peek <= 57) ||
+                               ($peek === 45) ||
+                               ($peek === 95) ||
+                               ($peek > 0xa1)) {
+                               return $this->consume(1);
+                       }
+               } else {
+                       return $this->consume(1);
+               }
+               return null;
+       }
+
+       public function consumeWhiteSpace() {
+               $comments = array();
+               do {
+                       while (preg_match('/\\s/isSu', $this->peek()) === 1) {
+                               $this->consume(1);
+                       }
+                       if($this->oParserSettings->bLenientParsing) {
+                               try {
+                                       $oComment = $this->consumeComment();
+                               } catch(UnexpectedTokenException $e) {
+                                       // When we can’t find the end of a comment, we assume the document is finished.
+                                       $this->iCurrentPosition = $this->iLength;
+                                       return;
+                               }
+                       } else {
+                               $oComment = $this->consumeComment();
+                       }
+                       if ($oComment !== false) {
+                               $comments[] = $oComment;
+                       }
+               } while($oComment !== false);
+               return $comments;
+       }
+
+       public function comes($sString, $bCaseInsensitive = false) {
+               $sPeek = $this->peek(strlen($sString));
+               return ($sPeek == '')
+                       ? false
+                       : $this->streql($sPeek, $sString, $bCaseInsensitive);
+       }
+
+       public function peek($iLength = 1, $iOffset = 0) {
+               $iOffset += $this->iCurrentPosition;
+               if ($iOffset >= $this->iLength) {
+                       return '';
+               }
+               return $this->substr($iOffset, $iLength);
+       }
+
+       public function consume($mValue = 1) {
+               if (is_string($mValue)) {
+                       $iLineCount = substr_count($mValue, "\n");
+                       $iLength = $this->strlen($mValue);
+                       if (!$this->streql($this->substr($this->iCurrentPosition, $iLength), $mValue)) {
+                               throw new UnexpectedTokenException($mValue, $this->peek(max($iLength, 5)), $this->iLineNo);
+                       }
+                       $this->iLineNo += $iLineCount;
+                       $this->iCurrentPosition += $this->strlen($mValue);
+                       return $mValue;
+               } else {
+                       if ($this->iCurrentPosition + $mValue > $this->iLength) {
+                               throw new UnexpectedTokenException($mValue, $this->peek(5), 'count', $this->iLineNo);
+                       }
+                       $sResult = $this->substr($this->iCurrentPosition, $mValue);
+                       $iLineCount = substr_count($sResult, "\n");
+                       $this->iLineNo += $iLineCount;
+                       $this->iCurrentPosition += $mValue;
+                       return $sResult;
+               }
+       }
+
+       public function consumeExpression($mExpression, $iMaxLength = null) {
+               $aMatches = null;
+               $sInput = $iMaxLength !== null ? $this->peek($iMaxLength) : $this->inputLeft();
+               if (preg_match($mExpression, $sInput, $aMatches, PREG_OFFSET_CAPTURE) === 1) {
+                       return $this->consume($aMatches[0][0]);
+               }
+               throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo);
+       }
+
+       /**
+        * @return false|Comment
+        */
+       public function consumeComment() {
+               $mComment = false;
+               if ($this->comes('/*')) {
+                       $iLineNo = $this->iLineNo;
+                       $this->consume(1);
+                       $mComment = '';
+                       while (($char = $this->consume(1)) !== '') {
+                               $mComment .= $char;
+                               if ($this->comes('*/')) {
+                                       $this->consume(2);
+                                       break;
+                               }
+                       }
+               }
+
+               if ($mComment !== false) {
+                       // We skip the * which was included in the comment.
+                       return new Comment(substr($mComment, 1), $iLineNo);
+               }
+
+               return $mComment;
+       }
+
+       public function isEnd() {
+               return $this->iCurrentPosition >= $this->iLength;
+       }
+
+       public function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = array()) {
+               $aEnd = is_array($aEnd) ? $aEnd : array($aEnd);
+               $out = '';
+               $start = $this->iCurrentPosition;
+
+               while (($char = $this->consume(1)) !== '') {
+                       if (in_array($char, $aEnd)) {
+                               if ($bIncludeEnd) {
+                                       $out .= $char;
+                               } elseif (!$consumeEnd) {
+                                       $this->iCurrentPosition -= $this->strlen($char);
+                               }
+                               return $out;
+                       }
+                       $out .= $char;
+                       if ($comment = $this->consumeComment()) {
+                               $comments[] = $comment;
+                       }
+               }
+
+               $this->iCurrentPosition = $start;
+               throw new UnexpectedTokenException('One of ("'.implode('","', $aEnd).'")', $this->peek(5), 'search', $this->iLineNo);
+       }
+
+       private function inputLeft() {
+               return $this->substr($this->iCurrentPosition, -1);
+       }
+
+       public function streql($sString1, $sString2, $bCaseInsensitive = true) {
+               if($bCaseInsensitive) {
+                       return $this->strtolower($sString1) === $this->strtolower($sString2);
+               } else {
+                       return $sString1 === $sString2;
+               }
+       }
+
+       public function backtrack($iAmount) {
+               $this->iCurrentPosition -= $iAmount;
+       }
+
+       public function strlen($sString) {
+               if ($this->oParserSettings->bMultibyteSupport) {
+                       return mb_strlen($sString, $this->sCharset);
+               } else {
+                       return strlen($sString);
+               }       
+       }       
+
+       private function substr($iStart, $iLength) {
+               if ($iLength < 0) {
+                       $iLength = $this->iLength - $iStart + $iLength;
+               }       
+               if ($iStart + $iLength > $this->iLength) {
+                       $iLength = $this->iLength - $iStart;
+               }       
+               $sResult = '';
+               while ($iLength > 0) {
+                       $sResult .= $this->aText[$iStart];
+                       $iStart++;
+                       $iLength--;
+               }       
+               return $sResult;
+       }
+
+       private function strtolower($sString) {
+               if ($this->oParserSettings->bMultibyteSupport) {
+                       return mb_strtolower($sString, $this->sCharset);
+               } else {
+                       return strtolower($sString);
+               }
+       }
+
+       private function strsplit($sString) {
+               if ($this->oParserSettings->bMultibyteSupport) {
+                       if ($this->streql($this->sCharset, 'utf-8')) {
+                               return preg_split('//u', $sString, null, PREG_SPLIT_NO_EMPTY);
+                       } else {
+                               $iLength = mb_strlen($sString, $this->sCharset);
+                               $aResult = array();
+                               for ($i = 0; $i < $iLength; ++$i) {
+                                       $aResult[] = mb_substr($sString, $i, 1, $this->sCharset);
+                               }
+                               return $aResult;
+                       }
+               } else {
+                       if($sString === '') {
+                               return array();
+                       } else {
+                               return str_split($sString);
+                       }
+               }
+       }
+
+       private function strpos($sString, $sNeedle, $iOffset) {
+               if ($this->oParserSettings->bMultibyteSupport) {
+                       return mb_strpos($sString, $sNeedle, $iOffset, $this->sCharset);
+               } else {
+                       return strpos($sString, $sNeedle, $iOffset);
+               }
+       }
+}
\ No newline at end of file
index de3eea1..b20c8c6 100644 (file)
@@ -6,9 +6,10 @@ use Sabberworm\CSS\Renderable;
 use Sabberworm\CSS\Comment\Commentable;
 
 interface AtRule extends Renderable, Commentable {
-       const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values';
        // Since there are more set rules than block rules, we’re whitelisting the block rules and have anything else be treated as a set rule.
-       const SET_RULES = 'font-face/counter-style/page/swash/styleset/annotation'; //…and more font-specific ones (to be used inside font-feature-values)
+       const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values';
+       // …and more font-specific ones (to be used inside font-feature-values)
+       const SET_RULES = 'font-face/counter-style/page/swash/styleset/annotation';
        
        public function atRuleName();
        public function atRuleArgs();
index 3e48537..3fa031b 100644 (file)
@@ -2,10 +2,11 @@
 
 namespace Sabberworm\CSS\Rule;
 
+use Sabberworm\CSS\Comment\Commentable;
+use Sabberworm\CSS\Parsing\ParserState;
 use Sabberworm\CSS\Renderable;
 use Sabberworm\CSS\Value\RuleValueList;
 use Sabberworm\CSS\Value\Value;
-use Sabberworm\CSS\Comment\Commentable;
 
 /**
  * RuleSets contains Rule objects which always have a key and a value.
@@ -29,6 +30,44 @@ class Rule implements Renderable, Commentable {
                $this->aComments = array();
        }
 
+       public static function parse(ParserState $oParserState) {
+               $aComments = $oParserState->consumeWhiteSpace();
+               $oRule = new Rule($oParserState->parseIdentifier(), $oParserState->currentLine());
+               $oRule->setComments($aComments);
+               $oRule->addComments($oParserState->consumeWhiteSpace());
+               $oParserState->consume(':');
+               $oValue = Value::parseValue($oParserState, self::listDelimiterForRule($oRule->getRule()));
+               $oRule->setValue($oValue);
+               if ($oParserState->getSettings()->bLenientParsing) {
+                       while ($oParserState->comes('\\')) {
+                               $oParserState->consume('\\');
+                               $oRule->addIeHack($oParserState->consume());
+                               $oParserState->consumeWhiteSpace();
+                       }
+               }
+               $oParserState->consumeWhiteSpace();
+               if ($oParserState->comes('!')) {
+                       $oParserState->consume('!');
+                       $oParserState->consumeWhiteSpace();
+                       $oParserState->consume('important');
+                       $oRule->setIsImportant(true);
+               }
+               $oParserState->consumeWhiteSpace();
+               while ($oParserState->comes(';')) {
+                       $oParserState->consume(';');
+               }
+               $oParserState->consumeWhiteSpace();
+
+               return $oRule;
+       }
+
+       private static function listDelimiterForRule($sRule) {
+               if (preg_match('/^font($|-)/', $sRule)) {
+                       return array(',', '/', ' ');
+               }
+               return array(',', ' ', '/');
+       }
+
        /**
         * @return int
         */
index e18f5d8..6614b1d 100644 (file)
@@ -2,6 +2,8 @@
 
 namespace Sabberworm\CSS\RuleSet;
 
+use Sabberworm\CSS\Parsing\ParserState;
+use Sabberworm\CSS\Parsing\OutputException;
 use Sabberworm\CSS\Property\Selector;
 use Sabberworm\CSS\Rule\Rule;
 use Sabberworm\CSS\Value\RuleValueList;
@@ -9,7 +11,6 @@ use Sabberworm\CSS\Value\Value;
 use Sabberworm\CSS\Value\Size;
 use Sabberworm\CSS\Value\Color;
 use Sabberworm\CSS\Value\URL;
-use Sabberworm\CSS\Parsing\OutputException;
 
 /**
  * Declaration blocks are the parts of a css file which denote the rules belonging to a selector.
@@ -24,6 +25,16 @@ class DeclarationBlock extends RuleSet {
                $this->aSelectors = array();
        }
 
+       public static function parse(ParserState $oParserState) {
+               $aComments = array();
+               $oResult = new DeclarationBlock($oParserState->currentLine());
+               $oResult->setSelector($oParserState->consumeUntil('{', false, true, $aComments));
+               $oResult->setComments($aComments);
+               RuleSet::parseRuleSet($oParserState, $oResult);
+               return $oResult;
+       }
+
+
        public function setSelectors($mSelector) {
                if (is_array($mSelector)) {
                        $this->aSelectors = $mSelector;
@@ -65,6 +76,11 @@ class DeclarationBlock extends RuleSet {
                $this->setSelectors($mSelector);
        }
 
+       /**
+        * Get selectors.
+        *
+        * @return Selector[] Selectors.
+        */
        public function getSelectors() {
                return $this->aSelectors;
        }
@@ -599,9 +615,13 @@ class DeclarationBlock extends RuleSet {
                        // If all the selectors have been removed, this declaration block becomes invalid
                        throw new OutputException("Attempt to print declaration block with missing selector", $this->iLineNo);
                }
-               $sResult = $oOutputFormat->implode($oOutputFormat->spaceBeforeSelectorSeparator() . ',' . $oOutputFormat->spaceAfterSelectorSeparator(), $this->aSelectors) . $oOutputFormat->spaceBeforeOpeningBrace() . '{';
+               $sResult = $oOutputFormat->sBeforeDeclarationBlock;
+               $sResult .= $oOutputFormat->implode($oOutputFormat->spaceBeforeSelectorSeparator() . ',' . $oOutputFormat->spaceAfterSelectorSeparator(), $this->aSelectors);
+               $sResult .= $oOutputFormat->sAfterDeclarationBlockSelectors;
+               $sResult .= $oOutputFormat->spaceBeforeOpeningBrace() . '{';
                $sResult .= parent::render($oOutputFormat);
                $sResult .= '}';
+               $sResult .= $oOutputFormat->sAfterDeclarationBlock;
                return $sResult;
        }
 
index 124be88..e5d5e41 100644 (file)
@@ -2,9 +2,11 @@
 
 namespace Sabberworm\CSS\RuleSet;
 
-use Sabberworm\CSS\Rule\Rule;
-use Sabberworm\CSS\Renderable;
 use Sabberworm\CSS\Comment\Commentable;
+use Sabberworm\CSS\Parsing\ParserState;
+use Sabberworm\CSS\Parsing\UnexpectedTokenException;
+use Sabberworm\CSS\Renderable;
+use Sabberworm\CSS\Rule\Rule;
 
 /**
  * RuleSet is a generic superclass denoting rules. The typical example for rule sets are declaration block.
@@ -22,6 +24,41 @@ abstract class RuleSet implements Renderable, Commentable {
                $this->aComments = array();
        }
 
+       public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet) {
+               while ($oParserState->comes(';')) {
+                       $oParserState->consume(';');
+               }
+               while (!$oParserState->comes('}')) {
+                       $oRule = null;
+                       if($oParserState->getSettings()->bLenientParsing) {
+                               try {
+                                       $oRule = Rule::parse($oParserState);
+                               } catch (UnexpectedTokenException $e) {
+                                       try {
+                                               $sConsume = $oParserState->consumeUntil(array("\n", ";", '}'), true);
+                                               // We need to “unfind” the matches to the end of the ruleSet as this will be matched later
+                                               if($oParserState->streql(substr($sConsume, -1), '}')) {
+                                                       $oParserState->backtrack(1);
+                                               } else {
+                                                       while ($oParserState->comes(';')) {
+                                                               $oParserState->consume(';');
+                                                       }
+                                               }
+                                       } catch (UnexpectedTokenException $e) {
+                                               // We’ve reached the end of the document. Just close the RuleSet.
+                                               return;
+                                       }
+                               }
+                       } else {
+                               $oRule = Rule::parse($oParserState);
+                       }
+                       if($oRule) {
+                               $oRuleSet->addRule($oRule);
+                       }
+               }
+               $oParserState->consume('}');
+       }
+
        /**
         * @return int
         */
@@ -52,6 +89,7 @@ abstract class RuleSet implements Renderable, Commentable {
         * @param (null|string|Rule) $mRule pattern to search for. If null, returns all rules. if the pattern ends with a dash, all rules starting with the pattern are returned as well as one matching the pattern with the dash excluded. passing a Rule behaves like calling getRules($mRule->getRule()).
         * @example $oRuleSet->getRules('font-') //returns an array of all rules either beginning with font- or matching font.
         * @example $oRuleSet->getRules('font') //returns array(0 => $oRule, …) or array().
+        * @return Rule[] Rules.
         */
        public function getRules($mRule = null) {
                if ($mRule instanceof Rule) {
@@ -69,7 +107,7 @@ abstract class RuleSet implements Renderable, Commentable {
 
        /**
         * Override all the rules of this set.
-        * @param array $aRules The rules to override with.
+        * @param Rule[] $aRules The rules to override with.
         */
        public function setRules(array $aRules) {
                $this->aRules = array();
@@ -82,6 +120,7 @@ abstract class RuleSet implements Renderable, Commentable {
         * Returns all rules matching the given pattern and returns them in an associative array with the rule’s name as keys. This method exists mainly for backwards-compatibility and is really only partially useful.
         * @param (string) $mRule pattern to search for. If null, returns all rules. if the pattern ends with a dash, all rules starting with the pattern are returned as well as one matching the pattern with the dash excluded. passing a Rule behaves like calling getRules($mRule->getRule()).
         * Note: This method loses some information: Calling this (with an argument of 'background-') on a declaration block like { background-color: green; background-color; rgba(0, 127, 0, 0.7); } will only yield an associative array containing the rgba-valued rule while @link{getRules()} would yield an indexed array containing both.
+        * @return Rule[] Rules.
         */
        public function getRulesAssoc($mRule = null) {
                $aResult = array();
@@ -92,9 +131,9 @@ abstract class RuleSet implements Renderable, Commentable {
        }
 
        /**
-       * Remove a rule from this RuleSet. This accepts all the possible values that @link{getRules()} accepts. If given a Rule, it will only remove this particular rule (by identity). If given a name, it will remove all rules by that name. Note: this is different from pre-v.2.0 behaviour of PHP-CSS-Parser, where passing a Rule instance would remove all rules with the same name. To get the old behvaiour, use removeRule($oRule->getRule()).
- * @param (null|string|Rule) $mRule pattern to remove. If $mRule is null, all rules are removed. If the pattern ends in a dash, all rules starting with the pattern are removed as well as one matching the pattern with the dash excluded. Passing a Rule behaves matches by identity.
-       */
+        * Remove a rule from this RuleSet. This accepts all the possible values that @link{getRules()} accepts. If given a Rule, it will only remove this particular rule (by identity). If given a name, it will remove all rules by that name. Note: this is different from pre-v.2.0 behaviour of PHP-CSS-Parser, where passing a Rule instance would remove all rules with the same name. To get the old behvaiour, use removeRule($oRule->getRule()).
       * @param (null|string|Rule) $mRule pattern to remove. If $mRule is null, all rules are removed. If the pattern ends in a dash, all rules starting with the pattern are removed as well as one matching the pattern with the dash excluded. Passing a Rule behaves matches by identity.
+        */
        public function removeRule($mRule) {
                if($mRule instanceof Rule) {
                        $sRule = $mRule->getRule();
index 3633abc..941df23 100644 (file)
@@ -4,7 +4,7 @@ namespace Sabberworm\CSS\Value;
 
 class CSSFunction extends ValueList {
 
-       private $sName;
+       protected $sName;
 
        public function __construct($sName, $aArguments, $sSeparator = ',', $iLineNo = 0) {
                if($aArguments instanceof RuleValueList) {
@@ -37,4 +37,4 @@ class CSSFunction extends ValueList {
                return "{$this->sName}({$aArguments})";
        }
 
-}
\ No newline at end of file
+}
index b070008..9f9c050 100644 (file)
@@ -2,6 +2,9 @@
 
 namespace Sabberworm\CSS\Value;
 
+use Sabberworm\CSS\Parsing\ParserState;
+use Sabberworm\CSS\Parsing\SourceException;
+
 class CSSString extends PrimitiveValue {
 
        private $sString;
@@ -11,6 +14,37 @@ class CSSString extends PrimitiveValue {
                parent::__construct($iLineNo);
        }
 
+       public static function parse(ParserState $oParserState) {
+               $sBegin = $oParserState->peek();
+               $sQuote = null;
+               if ($sBegin === "'") {
+                       $sQuote = "'";
+               } else if ($sBegin === '"') {
+                       $sQuote = '"';
+               }
+               if ($sQuote !== null) {
+                       $oParserState->consume($sQuote);
+               }
+               $sResult = "";
+               $sContent = null;
+               if ($sQuote === null) {
+                       // Unquoted strings end in whitespace or with braces, brackets, parentheses
+                       while (!preg_match('/[\\s{}()<>\\[\\]]/isu', $oParserState->peek())) {
+                               $sResult .= $oParserState->parseCharacter(false);
+                       }
+               } else {
+                       while (!$oParserState->comes($sQuote)) {
+                               $sContent = $oParserState->parseCharacter(false);
+                               if ($sContent === null) {
+                                       throw new SourceException("Non-well-formed quoted string {$oParserState->peek(3)}", $oParserState->currentLine());
+                               }
+                               $sResult .= $sContent;
+                       }
+                       $oParserState->consume($sQuote);
+               }
+               return new CSSString($sResult, $oParserState->currentLine());
+       }
+
        public function setString($sString) {
                $this->sString = $sString;
        }
diff --git a/lib/php-css-parser/Value/CalcFunction.php b/lib/php-css-parser/Value/CalcFunction.php
new file mode 100644 (file)
index 0000000..9247520
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+
+namespace Sabberworm\CSS\Value;
+
+use Sabberworm\CSS\Parsing\ParserState;
+use Sabberworm\CSS\Parsing\UnexpectedTokenException;
+
+class CalcFunction extends CSSFunction {
+       const T_OPERAND  = 1;
+       const T_OPERATOR = 2;
+
+       public static function parse(ParserState $oParserState) {
+               $aOperators = array('+', '-', '*', '/');
+               $sFunction = trim($oParserState->consumeUntil('(', false, true));
+               $oCalcList = new CalcRuleValueList($oParserState->currentLine());
+               $oList = new RuleValueList(',', $oParserState->currentLine());
+               $iNestingLevel = 0;
+               $iLastComponentType = NULL;
+               while(!$oParserState->comes(')') || $iNestingLevel > 0) {
+                       $oParserState->consumeWhiteSpace();
+                       if ($oParserState->comes('(')) {
+                               $iNestingLevel++;
+                               $oCalcList->addListComponent($oParserState->consume(1));
+                               continue;
+                       } else if ($oParserState->comes(')')) {
+                               $iNestingLevel--;
+                               $oCalcList->addListComponent($oParserState->consume(1));
+                               continue;
+                       }
+                       if ($iLastComponentType != CalcFunction::T_OPERAND) {
+                               $oVal = Value::parsePrimitiveValue($oParserState);
+                               $oCalcList->addListComponent($oVal);
+                               $iLastComponentType = CalcFunction::T_OPERAND;
+                       } else {
+                               if (in_array($oParserState->peek(), $aOperators)) {
+                                       if (($oParserState->comes('-') || $oParserState->comes('+'))) {
+                                               if ($oParserState->peek(1, -1) != ' ' || !($oParserState->comes('- ') || $oParserState->comes('+ '))) {
+                                                       throw new UnexpectedTokenException(" {$oParserState->peek()} ", $oParserState->peek(1, -1) . $oParserState->peek(2), 'literal', $oParserState->currentLine());
+                                               }
+                                       }
+                                       $oCalcList->addListComponent($oParserState->consume(1));
+                                       $iLastComponentType = CalcFunction::T_OPERATOR;
+                               } else {
+                                       throw new UnexpectedTokenException(
+                                               sprintf(
+                                                       'Next token was expected to be an operand of type %s. Instead "%s" was found.',
+                                                       implode(', ', $aOperators),
+                                                       $oVal
+                                               ),
+                                               '',
+                                               'custom',
+                                               $oParserState->currentLine()
+                                       );
+                               }
+                       }
+               }
+               $oList->addListComponent($oCalcList);
+               $oParserState->consume(')');
+               return new CalcFunction($sFunction, $oList, ',', $oParserState->currentLine());
+       }
+
+}
diff --git a/lib/php-css-parser/Value/CalcRuleValueList.php b/lib/php-css-parser/Value/CalcRuleValueList.php
new file mode 100644 (file)
index 0000000..bde8a9d
--- /dev/null
@@ -0,0 +1,14 @@
+<?php
+
+namespace Sabberworm\CSS\Value;
+
+class CalcRuleValueList extends RuleValueList {
+       public function __construct($iLineNo = 0) {
+               parent::__construct(array(), ',', $iLineNo);
+       }
+
+       public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
+               return $oOutputFormat->implode(' ', $this->aComponents);
+       }
+
+}
index e05b924..c6ed9b1 100644 (file)
@@ -2,12 +2,66 @@
 
 namespace Sabberworm\CSS\Value;
 
+use Sabberworm\CSS\Parsing\ParserState;
+
 class Color extends CSSFunction {
 
        public function __construct($aColor, $iLineNo = 0) {
                parent::__construct(implode('', array_keys($aColor)), $aColor, ',', $iLineNo);
        }
 
+       public static function parse(ParserState $oParserState) {
+               $aColor = array();
+               if ($oParserState->comes('#')) {
+                       $oParserState->consume('#');
+                       $sValue = $oParserState->parseIdentifier(false);
+                       if ($oParserState->strlen($sValue) === 3) {
+                               $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2];
+                       } else if ($oParserState->strlen($sValue) === 4) {
+                               $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2] . $sValue[3] . $sValue[3];
+                       }
+
+                       if ($oParserState->strlen($sValue) === 8) {
+                               $aColor = array(
+                                       'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()),
+                                       'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()),
+                                       'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()),
+                                       'a' => new Size(round(self::mapRange(intval($sValue[6] . $sValue[7], 16), 0, 255, 0, 1), 2), null, true, $oParserState->currentLine())
+                               );
+                       } else {
+                               $aColor = array(
+                                       'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()),
+                                       'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()),
+                                       'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine())
+                               );
+                       }
+               } else {
+                       $sColorMode = $oParserState->parseIdentifier(true);
+                       $oParserState->consumeWhiteSpace();
+                       $oParserState->consume('(');
+                       $iLength = $oParserState->strlen($sColorMode);
+                       for ($i = 0; $i < $iLength; ++$i) {
+                               $oParserState->consumeWhiteSpace();
+                               $aColor[$sColorMode[$i]] = Size::parse($oParserState, true);
+                               $oParserState->consumeWhiteSpace();
+                               if ($i < ($iLength - 1)) {
+                                       $oParserState->consume(',');
+                               }
+                       }
+                       $oParserState->consume(')');
+               }
+               return new Color($aColor, $oParserState->currentLine());
+       }
+
+       private static function mapRange($fVal, $fFromMin, $fFromMax, $fToMin, $fToMax) {
+               $fFromRange = $fFromMax - $fFromMin;
+               $fToRange = $fToMax - $fToMin;
+               $fMultiplier = $fToRange / $fFromRange;
+               $fNewVal = $fVal - $fFromMin;
+               $fNewVal *= $fMultiplier;
+               return $fNewVal + $fToMin;
+       }
+
        public function getColor() {
                return $this->aComponents;
        }
diff --git a/lib/php-css-parser/Value/LineName.php b/lib/php-css-parser/Value/LineName.php
new file mode 100644 (file)
index 0000000..eb7392d
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+
+namespace Sabberworm\CSS\Value;
+
+use Sabberworm\CSS\Parsing\ParserState;
+use Sabberworm\CSS\Parsing\UnexpectedTokenException;
+
+class LineName extends ValueList {
+       public function __construct($aComponents = array(), $iLineNo = 0) {
+               parent::__construct($aComponents, ' ', $iLineNo);
+       }
+
+       public static function parse(ParserState $oParserState) {
+               $oParserState->consume('[');
+               $oParserState->consumeWhiteSpace();
+               $aNames = array();
+               do {
+                       if($oParserState->getSettings()->bLenientParsing) {
+                               try {
+                                       $aNames[] = $oParserState->parseIdentifier();
+                               } catch(UnexpectedTokenException $e) {}
+                       } else {
+                               $aNames[] = $oParserState->parseIdentifier();
+                       }
+                       $oParserState->consumeWhiteSpace();
+               } while (!$oParserState->comes(']'));
+               $oParserState->consume(']');
+               return new LineName($aNames, $oParserState->currentLine());
+       }
+
+
+
+       public function __toString() {
+               return $this->render(new \Sabberworm\CSS\OutputFormat());
+       }
+
+       public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
+               return '[' . parent::render(\Sabberworm\CSS\OutputFormat::createCompact()) . ']';
+       }
+
+}
index 9ad5eb0..f65246b 100644 (file)
@@ -2,12 +2,16 @@
 
 namespace Sabberworm\CSS\Value;
 
+use Sabberworm\CSS\Parsing\ParserState;
+
 class Size extends PrimitiveValue {
 
        const ABSOLUTE_SIZE_UNITS = 'px/cm/mm/mozmm/in/pt/pc/vh/vw/vm/vmin/vmax/rem'; //vh/vw/vm(ax)/vmin/rem are absolute insofar as they don’t scale to the immediate parent (only the viewport)
        const RELATIVE_SIZE_UNITS = '%/em/ex/ch/fr';
        const NON_SIZE_UNITS = 'deg/grad/rad/s/ms/turns/Hz/kHz';
 
+       private static $SIZE_UNITS = null;
+
        private $fSize;
        private $sUnit;
        private $bIsColorComponent;
@@ -19,6 +23,51 @@ class Size extends PrimitiveValue {
                $this->bIsColorComponent = $bIsColorComponent;
        }
 
+       public static function parse(ParserState $oParserState, $bIsColorComponent = false) {
+               $sSize = '';
+               if ($oParserState->comes('-')) {
+                       $sSize .= $oParserState->consume('-');
+               }
+               while (is_numeric($oParserState->peek()) || $oParserState->comes('.')) {
+                       if ($oParserState->comes('.')) {
+                               $sSize .= $oParserState->consume('.');
+                       } else {
+                               $sSize .= $oParserState->consume(1);
+                       }
+               }
+
+               $sUnit = null;
+               $aSizeUnits = self::getSizeUnits();
+               foreach($aSizeUnits as $iLength => &$aValues) {
+                       $sKey = strtolower($oParserState->peek($iLength));
+                       if(array_key_exists($sKey, $aValues)) {
+                               if (($sUnit = $aValues[$sKey]) !== null) {
+                                       $oParserState->consume($iLength);
+                                       break;
+                               }
+                       }
+               }
+               return new Size(floatval($sSize), $sUnit, $bIsColorComponent, $oParserState->currentLine());
+       }
+
+       private static function getSizeUnits() {
+               if(self::$SIZE_UNITS === null) {
+                       self::$SIZE_UNITS = array();
+                       foreach (explode('/', Size::ABSOLUTE_SIZE_UNITS.'/'.Size::RELATIVE_SIZE_UNITS.'/'.Size::NON_SIZE_UNITS) as $val) {
+                               $iSize = strlen($val);
+                               if(!isset(self::$SIZE_UNITS[$iSize])) {
+                                       self::$SIZE_UNITS[$iSize] = array();
+                               }
+                               self::$SIZE_UNITS[$iSize][strtolower($val)] = $val;
+                       }
+
+                       // FIXME: Should we not order the longest units first?
+                       ksort(self::$SIZE_UNITS, SORT_NUMERIC);
+               }
+
+               return self::$SIZE_UNITS;
+       }
+
        public function setUnit($sUnit) {
                $this->sUnit = $sUnit;
        }
index 02cf581..b4f37e1 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace Sabberworm\CSS\Value;
 
+use Sabberworm\CSS\Parsing\ParserState;
 
 class URL extends PrimitiveValue {
 
@@ -12,6 +13,23 @@ class URL extends PrimitiveValue {
                $this->oURL = $oURL;
        }
 
+       public static function parse(ParserState $oParserState) {
+               $bUseUrl = $oParserState->comes('url', true);
+               if ($bUseUrl) {
+                       $oParserState->consume('url');
+                       $oParserState->consumeWhiteSpace();
+                       $oParserState->consume('(');
+               }
+               $oParserState->consumeWhiteSpace();
+               $oResult = new URL(CSSString::parse($oParserState), $oParserState->currentLine());
+               if ($bUseUrl) {
+                       $oParserState->consumeWhiteSpace();
+                       $oParserState->consume(')');
+               }
+               return $oResult;
+       }
+
+
        public function setURL(CSSString $oURL) {
                $this->oURL = $oURL;
        }
index 5d30bd9..fccc26b 100644 (file)
 
 namespace Sabberworm\CSS\Value;
 
+use Sabberworm\CSS\Parsing\ParserState;
+use Sabberworm\CSS\Parsing\UnexpectedTokenException;
 use Sabberworm\CSS\Renderable;
 
 abstract class Value implements Renderable {
-    protected $iLineNo;
-
-    public function __construct($iLineNo = 0) {
-        $this->iLineNo = $iLineNo;
-    }
-    
-    /**
-     * @return int
-     */
-    public function getLineNo() {
-        return $this->iLineNo;
-    }
-
-    //Methods are commented out because re-declaring them here is a fatal error in PHP < 5.3.9
+       protected $iLineNo;
+
+       public function __construct($iLineNo = 0) {
+               $this->iLineNo = $iLineNo;
+       }
+
+       public static function parseValue(ParserState $oParserState, $aListDelimiters = array()) {
+               $aStack = array();
+               $oParserState->consumeWhiteSpace();
+               //Build a list of delimiters and parsed values
+               while (!($oParserState->comes('}') || $oParserState->comes(';') || $oParserState->comes('!') || $oParserState->comes(')') || $oParserState->comes('\\'))) {
+                       if (count($aStack) > 0) {
+                               $bFoundDelimiter = false;
+                               foreach ($aListDelimiters as $sDelimiter) {
+                                       if ($oParserState->comes($sDelimiter)) {
+                                               array_push($aStack, $oParserState->consume($sDelimiter));
+                                               $oParserState->consumeWhiteSpace();
+                                               $bFoundDelimiter = true;
+                                               break;
+                                       }
+                               }
+                               if (!$bFoundDelimiter) {
+                                       //Whitespace was the list delimiter
+                                       array_push($aStack, ' ');
+                               }
+                       }
+                       array_push($aStack, self::parsePrimitiveValue($oParserState));
+                       $oParserState->consumeWhiteSpace();
+               }
+               //Convert the list to list objects
+               foreach ($aListDelimiters as $sDelimiter) {
+                       if (count($aStack) === 1) {
+                               return $aStack[0];
+                       }
+                       $iStartPosition = null;
+                       while (($iStartPosition = array_search($sDelimiter, $aStack, true)) !== false) {
+                               $iLength = 2; //Number of elements to be joined
+                               for ($i = $iStartPosition + 2; $i < count($aStack); $i+=2, ++$iLength) {
+                                       if ($sDelimiter !== $aStack[$i]) {
+                                               break;
+                                       }
+                               }
+                               $oList = new RuleValueList($sDelimiter, $oParserState->currentLine());
+                               for ($i = $iStartPosition - 1; $i - $iStartPosition + 1 < $iLength * 2; $i+=2) {
+                                       $oList->addListComponent($aStack[$i]);
+                               }
+                               array_splice($aStack, $iStartPosition - 1, $iLength * 2 - 1, array($oList));
+                       }
+               }
+               if (!isset($aStack[0])) {
+                       throw new UnexpectedTokenException(" {$oParserState->peek()} ", $oParserState->peek(1, -1) . $oParserState->peek(2), 'literal', $oParserState->currentLine());
+               }
+               return $aStack[0];
+       }
+
+       public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false) {
+               $sResult = $oParserState->parseIdentifier($bIgnoreCase);
+
+               if ($oParserState->comes('(')) {
+                       $oParserState->consume('(');
+                       $aArguments = Value::parseValue($oParserState, array('=', ' ', ','));
+                       $sResult = new CSSFunction($sResult, $aArguments, ',', $oParserState->currentLine());
+                       $oParserState->consume(')');
+               }
+
+               return $sResult;
+       }
+
+       public static function parsePrimitiveValue(ParserState $oParserState) {
+               $oValue = null;
+               $oParserState->consumeWhiteSpace();
+               if (is_numeric($oParserState->peek()) || ($oParserState->comes('-.') && is_numeric($oParserState->peek(1, 2))) || (($oParserState->comes('-') || $oParserState->comes('.')) && is_numeric($oParserState->peek(1, 1)))) {
+                       $oValue = Size::parse($oParserState);
+               } else if ($oParserState->comes('#') || $oParserState->comes('rgb', true) || $oParserState->comes('hsl', true)) {
+                       $oValue = Color::parse($oParserState);
+               } else if ($oParserState->comes('url', true)) {
+                       $oValue = URL::parse($oParserState);
+               } else if ($oParserState->comes('calc', true) || $oParserState->comes('-webkit-calc', true) || $oParserState->comes('-moz-calc', true)) {
+                       $oValue = CalcFunction::parse($oParserState);
+               } else if ($oParserState->comes("'") || $oParserState->comes('"')) {
+                       $oValue = CSSString::parse($oParserState);
+               } else if ($oParserState->comes("progid:") && $oParserState->getSettings()->bLenientParsing) {
+                       $oValue = self::parseMicrosoftFilter($oParserState);
+               } else if ($oParserState->comes("[")) {
+                       $oValue = LineName::parse($oParserState);
+               } else if ($oParserState->comes("U+")) {
+                       $oValue = self::parseUnicodeRangeValue($oParserState);
+               } else {
+                       $oValue = self::parseIdentifierOrFunction($oParserState);
+               }
+               $oParserState->consumeWhiteSpace();
+               return $oValue;
+       }
+
+       private static function parseMicrosoftFilter(ParserState $oParserState) {
+               $sFunction = $oParserState->consumeUntil('(', false, true);
+               $aArguments = Value::parseValue($oParserState, array(',', '='));
+               return new CSSFunction($sFunction, $aArguments, ',', $oParserState->currentLine());
+       }
+
+       private static function parseUnicodeRangeValue(ParserState $oParserState) {
+               $iCodepointMaxLenth = 6; // Code points outside BMP can use up to six digits
+               $sRange = "";
+               $oParserState->consume("U+");
+               do {
+                       if ($oParserState->comes('-')) $iCodepointMaxLenth = 13; // Max length is 2 six digit code points + the dash(-) between them
+                       $sRange .= $oParserState->consume(1);
+               } while (strlen($sRange) < $iCodepointMaxLenth && preg_match("/[A-Fa-f0-9\?-]/", $oParserState->peek()));
+               return "U+{$sRange}";
+       }
+       
+       /**
+        * @return int
+        */
+       public function getLineNo() {
+               return $this->iLineNo;
+       }
+
+       //Methods are commented out because re-declaring them here is a fatal error in PHP < 5.3.9
        //public abstract function __toString();
        //public abstract function render(\Sabberworm\CSS\OutputFormat $oOutputFormat);
 }
index f63dd81..f4a2cc2 100644 (file)
@@ -1,10 +1,7 @@
 PHP CSS Parser
 --------------
 
-Import git ref: c3b01ef0a85824e86fd86a74a8154d8d5c34b0ff
-                (master)
-
-Downloaded from: https://github.com/sabberworm/PHP-CSS-Parser
+Downloaded from: https://github.com/sabberworm/PHP-CSS-Parser/releases/tag/8.3.0
 
 Import procedure:
 
index f27a6f5..a3083ff 100644 (file)
@@ -36,18 +36,24 @@ class core_moodlelib_testcase extends advanced_testcase {
      * It is not possible to directly change the result of get_string in
      * a unit test. Instead, we create a language pack for language 'xx' in
      * dataroot and make langconfig.php with the string we need to change.
-     * The example separator used here is 'X'; on PHP 5.3 and before this
+     * The default example separator used here is 'X'; on PHP 5.3 and before this
      * must be a single byte character due to PHP bug/limitation in
      * number_format, so you can't use UTF-8 characters.
+     *
+     * @param string $decsep Separator character. Defaults to `'X'`.
      */
-    protected function define_local_decimal_separator() {
+    protected function define_local_decimal_separator(string $decsep = 'X') {
         global $SESSION, $CFG;
 
         $SESSION->lang = 'xx';
-        $langconfig = "<?php\n\$string['decsep'] = 'X';";
+        $langconfig = "<?php\n\$string['decsep'] = '$decsep';";
         $langfolder = $CFG->dataroot . '/lang/xx';
         check_dir_exists($langfolder);
         file_put_contents($langfolder . '/langconfig.php', $langconfig);
+
+        // Ensure the new value is picked up and not taken from the cache.
+        $stringmanager = get_string_manager();
+        $stringmanager->reset_caches(true);
     }
 
     public function test_cleanremoteaddr() {
@@ -2257,6 +2263,14 @@ class core_moodlelib_testcase extends advanced_testcase {
         // Localisation off.
         $this->assertEquals('5.43000', format_float(5.43, 5, false));
         $this->assertEquals('5.43', format_float(5.43, 5, false, true));
+
+        // Tests with tilde as localised decimal separator.
+        $this->define_local_decimal_separator('~');
+
+        // Must also work for '~' as decimal separator.
+        $this->assertEquals('5', format_float(5.0001, 3, true, true));
+        $this->assertEquals('5~43000', format_float(5.43, 5));
+        $this->assertEquals('5~43', format_float(5.43, 5, true, true));
     }
 
     /**
index ed721af..505ae73 100644 (file)
     <location>html2text</location>
     <name>HTML2Text</name>
     <license>GPL</license>
-    <version>4.1.0</version>
+    <version>4.2.1</version>
     <licenseversion>2.0+</licenseversion>
   </library>
   <library>
     <location>php-css-parser</location>
     <name>PHP-CSS-Parser</name>
     <license>MIT</license>
-    <version>8.1.0</version>
+    <version>8.3.0</version>
   </library>
   <library>
     <location>rtlcss</location>
index 1f59d6f..edf885b 100644 (file)
@@ -2,11 +2,15 @@ This files describes API changes in core libraries and APIs,
 information provided here is intended especially for developers.
 
 === 3.8 ===
+
 * The yui checknet module is removed. Call \core\session\manager::keepalive instead.
 * The generate_uuid() function has been deprecated. Please use \core\uuid::generate() instead.
 * Remove lib/pear/auth/RADIUS.php (MDL-65746)
+* Core components are now defined in /lib/components.json instead of coded into /lib/classes/component.php
+* Subplugins should now be defined using /db/subplugins.json instead of /db/subplugins.php (which will still work in order to maintain backwards compatibility).
 
 === 3.7 ===
+
 * Nodes in the navigation api can have labels for each group. See set/get_collectionlabel().
 * The method core_user::is_real_user() now returns false for userid = 0 parameter
 * 'mform1' dependencies (in themes, js...) will stop working because a randomly generated string has been added to the id
index b584c62..cd39407 100644 (file)
Binary files a/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-debug.js and b/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-debug.js differ
index 81f74b4..9eb3b20 100644 (file)
Binary files a/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-min.js and b/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker-min.js differ
index b584c62..cd39407 100644 (file)
Binary files a/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker.js and b/lib/yui/build/moodle-core-formchangechecker/moodle-core-formchangechecker.js differ
index f4545f4..59d3283 100644 (file)
@@ -36,6 +36,10 @@ Y.extend(FORMCHANGECHECKER, Y.Base, {
                 return;
             }
 
+            if (!M.core_formchangechecker.stateinformation.formchanged) {
+                M.core_formchangechecker.stateinformation.formchanged = this.get('initialdirtystate');
+            }
+
             // Add a listener here for an editor restore event.
             Y.on(M.core.event.EDITOR_CONTENT_RESTORED, M.core_formchangechecker.reset_form_dirty_state, this);
 
@@ -96,6 +100,9 @@ Y.extend(FORMCHANGECHECKER, Y.Base, {
         ATTRS: {
             formid: {
                 'value': ''
+            },
+            initialdirtystate: {
+                'value': false
             }
         }
     }
diff --git a/mod/assign/db/subplugins.json b/mod/assign/db/subplugins.json
new file mode 100644 (file)
index 0000000..202a240
--- /dev/null
@@ -0,0 +1,6 @@
+{
+    "plugintypes": {
+        "assignsubmission": "mod\/assign\/submission",
+        "assignfeedback": "mod\/assign\/feedback"
+    }
+}
index be0d707..ce8b075 100644 (file)
@@ -22,4 +22,5 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$subplugins = array('assignsubmission'=>'mod/assign/submission', 'assignfeedback'=>'mod/assign/feedback');
+debugging('Use of subplugins.php has been deprecated. Please provide a subplugins.json instead.', DEBUG_DEVELOPER);
+$subplugins = (array) json_decode(file_get_contents(__DIR__ . "/subplugins.json"))->plugintypes;
index e5033dc..7097679 100644 (file)
@@ -131,6 +131,7 @@ $string['couldnotcreatecoursemodule'] = 'Could not create course module.';
 $string['couldnotcreatenewassignmentinstance'] = 'Could not create new assignment instance.';
 $string['couldnotfindassignmenttoupgrade'] = 'Could not find old assignment instance to upgrade.';
 $string['crontask'] = 'Background processing for assignment module';
+$string['currentassigngrade'] = 'Current grade in assignment';
 $string['currentgrade'] = 'Current grade in gradebook';
 $string['currentattempt'] = 'This is attempt {$a}.';
 $string['currentattemptof'] = 'This is attempt {$a->attemptnumber} ( {$a->maxattempts} attempts allowed ).';
@@ -154,7 +155,6 @@ $string['downloadselectedsubmissions'] = 'Download selected submissions';
 $string['duedate'] = 'Due date';
 $string['duedatecolon'] = 'Due date: {$a}';
 $string['duedate_help'] = 'This is when the assignment is due. Submissions will still be allowed after this date, but any assignments submitted after this date will be marked as late. Set an assignment cut-off date to prevent submissions after a certain date.';
-$string['duedateno'] = 'No due date';
 $string['duplicateoverride'] = 'Duplicate override';
 $string['submissionempty'] = 'Nothing was submitted';
 $string['submissionmodified'] = 'You have existing submission data. Please leave this page and try again.';
@@ -350,14 +350,12 @@ $string['moreusers'] = '{$a} more...';
 $string['multipleteams'] = 'Member of more than one group';
 $string['multipleteams_desc'] = 'The assignment requires submission in groups. You are a member of more than one group. To be able to submit you must be a member of only one group. Please contact your teacher to change your group membership.';
 $string['multipleteamsgrader'] = 'Member of more than one group, so unable to make submissions.';
-$string['mysubmission'] = 'My submission: ';
 $string['newsubmissions'] = 'Assignments submitted';
 $string['noattempt'] = 'No attempt';
 $string['noclose'] = 'No close date';
 $string['nofilters'] = 'No filters';
 $string['nofiles'] = 'No files. ';
 $string['nograde'] = 'No grade. ';
-$string['nolatesubmissions'] = 'No late submissions accepted. ';
 $string['nomoresubmissionsaccepted'] = 'Only allowed for participants who have been granted an extension';
 $string['none'] = 'None';
 $string['noonlinesubmissions'] = 'This assignment does not require you to submit anything online';
@@ -365,13 +363,11 @@ $string['noopen'] = 'No open date';
 $string['nooverridedata'] = 'You must override at least one of the assignment settings.';
 $string['nosavebutnext'] = 'Next';
 $string['nosubmission'] = 'Nothing has been submitted for this assignment';
-$string['nosubmissionsacceptedafter'] = 'No submissions accepted after ';
 $string['noteam'] = 'Not a member of any group';
 $string['noteam_desc'] = 'This assignment requires submission in groups. You are not a member of any group, so you cannot create a submission. Please contact your teacher to be added to a group.';
 $string['noteamgrader'] = 'Not a member of any group, so unable to make submissions.';
 $string['notgraded'] = 'Not graded';
 $string['notgradedyet'] = 'Not graded yet';
-$string['notsubmittedyet'] = 'Not submitted yet';
 $string['notifications'] = 'Notifications';
 $string['nousersselected'] = 'No users selected';
 $string['nousers'] = 'No users';
@@ -526,7 +522,6 @@ $string['submissionreceiptsmall'] = 'You have submitted your assignment submissi
 $string['submissionslocked'] = 'This assignment is not accepting submissions';
 $string['submissionslockedshort'] = 'Submission changes not allowed';
 $string['submissions'] = 'Submissions';
-$string['submissionsnotgraded'] = 'Submissions not graded: {$a}';
 $string['submissionsclosed'] = 'Submissions closed';
 $string['submissionsettings'] = 'Submission settings';
 $string['submissionstatement'] = 'Submission statement';
@@ -613,3 +608,11 @@ $string['viewsubmissiongradingtable'] = 'View submission grading table.';
 $string['viewrevealidentitiesconfirm'] = 'View reveal student identities confirmation page.';
 $string['workflowfilter'] = 'Workflow filter';
 $string['xofy'] = '{$a->x} of {$a->y}';
+
+// Deprecated since Moodle 3.8.
+$string['duedateno'] = 'No due date';
+$string['mysubmission'] = 'My submission: ';
+$string['nolatesubmissions'] = 'No late submissions accepted. ';
+$string['nosubmissionsacceptedafter'] = 'No submissions accepted after ';
+$string['notsubmittedyet'] = 'Not submitted yet';
+$string['submissionsnotgraded'] = 'Submissions not graded: {$a}';
index e69de29..41f5cf7 100644 (file)
@@ -0,0 +1,6 @@
+duedateno,mod_assign
+mysubmission,mod_assign
+nolatesubmissions,mod_assign
+nosubmissionsacceptedafter,mod_assign
+notsubmittedyet,mod_assign
+submissionsnotgraded,mod_assign
\ No newline at end of file
index 6f0d59b..e494d63 100644 (file)
@@ -560,315 +560,24 @@ function assign_page_type_list($pagetype, $parentcontext, $currentcontext) {
 }
 
 /**
- * Print an overview of all assignments
- * for the courses.
- *
- * @deprecated since 3.3
- * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
- * @param mixed $courses The list of courses to print the overview for
- * @param array $htmlarray The array of html to return
- * @return true
+ * @deprecated since Moodle 3.3, when the block_course_overview block was removed.
  */
-function assign_print_overview($courses, &$htmlarray) {
-    global $CFG, $DB;
-
-    debugging('The function assign_print_overview() is now deprecated.', DEBUG_DEVELOPER);
-
-    if (empty($courses) || !is_array($courses) || count($courses) == 0) {
-        return true;
-    }
-
-    if (!$assignments = get_all_instances_in_courses('assign', $courses)) {
-        return true;
-    }
-
-    $assignmentids = array();
-
-    // Do assignment_base::isopen() here without loading the whole thing for speed.
-    foreach ($assignments as $key => $assignment) {
-        $time = time();
-        $isopen = false;
-        if ($assignment->duedate) {
-            $duedate = false;
-            if ($assignment->cutoffdate) {
-                $duedate = $assignment->cutoffdate;
-            }
-            if ($duedate) {
-                $isopen = ($assignment->allowsubmissionsfromdate <= $time && $time <= $duedate);
-            } else {
-                $isopen = ($assignment->allowsubmissionsfromdate <= $time);
-            }
-        }
-        if ($isopen) {
-            $assignmentids[] = $assignment->id;
-        }
-    }
-
-    if (empty($assignmentids)) {
-        // No assignments to look at - we're done.
-        return true;
-    }
-
-    // Definitely something to print, now include the constants we need.
-    require_once($CFG->dirroot . '/mod/assign/locallib.php');
-
-    $strduedate = get_string('duedate', 'assign');
-    $strcutoffdate = get_string('nosubmissionsacceptedafter', 'assign');
-    $strnolatesubmissions = get_string('nolatesubmissions', 'assign');
-    $strduedateno = get_string('duedateno', 'assign');
-    $strassignment = get_string('modulename', 'assign');
-
-    // We do all possible database work here *outside* of the loop to ensure this scales.
-    list($sqlassignmentids, $assignmentidparams) = $DB->get_in_or_equal($assignmentids);
-
-    $mysubmissions = null;
-    $unmarkedsubmissions = null;
-
-    foreach ($assignments as $assignment) {
-
-        // Do not show assignments that are not open.
-        if (!in_array($assignment->id, $assignmentids)) {
-            continue;
-        }
-
-        $context = context_module::instance($assignment->coursemodule);
-
-        // Does the submission status of the assignment require notification?
-        if (has_capability('mod/assign:submit', $context, null, false)) {
-            // Does the submission status of the assignment require notification?
-            $submitdetails = assign_get_mysubmission_details_for_print_overview($mysubmissions, $sqlassignmentids,
-                    $assignmentidparams, $assignment);
-        } else {
-            $submitdetails = false;
-        }
-
-        if (has_capability('mod/assign:grade', $context, null, false)) {
-            // Does the grading status of the assignment require notification ?
-            $gradedetails = assign_get_grade_details_for_print_overview($unmarkedsubmissions, $sqlassignmentids,
-                    $assignmentidparams, $assignment, $context);
-        } else {
-            $gradedetails = false;
-        }
-
-        if (empty($submitdetails) && empty($gradedetails)) {
-            // There is no need to display this assignment as there is nothing to notify.
-            continue;
-        }
-
-        $dimmedclass = '';
-        if (!$assignment->visible) {
-            $dimmedclass = ' class="dimmed"';
-        }
-        $href = $CFG->wwwroot . '/mod/assign/view.php?id=' . $assignment->coursemodule;
-        $basestr = '<div class="assign overview">' .
-               '<div class="name">' .
-               $strassignment . ': '.
-               '<a ' . $dimmedclass .
-                   'title="' . $strassignment . '" ' .
-                   'href="' . $href . '">' .
-               format_string($assignment->name) .
-               '</a></div>';
-        if ($assignment->duedate) {
-            $userdate = userdate($assignment->duedate);
-            $basestr .= '<div class="info">' . $strduedate . ': ' . $userdate . '</div>';
-        } else {
-            $basestr .= '<div class="info">' . $strduedateno . '</div>';
-        }
-        if ($assignment->cutoffdate) {
-            if ($assignment->cutoffdate == $assignment->duedate) {
-                $basestr .= '<div class="info">' . $strnolatesubmissions . '</div>';
-            } else {
-                $userdate = userdate($assignment->cutoffdate);
-                $basestr .= '<div class="info">' . $strcutoffdate . ': ' . $userdate . '</div>';
-            }
-        }
-
-        // Show only relevant information.
-        if (!empty($submitdetails)) {
-            $basestr .= $submitdetails;
-        }
-
-        if (!empty($gradedetails)) {
-            $basestr .= $gradedetails;
-        }
-        $basestr .= '</div>';
-
-        if (empty($htmlarray[$assignment->course]['assign'])) {
-            $htmlarray[$assignment->course]['assign'] = $basestr;
-        } else {
-            $htmlarray[$assignment->course]['assign'] .= $basestr;
-        }
-    }
-    return true;
+function assign_print_overview() {
+    throw new coding_exception('assign_print_overview() can not be used any more and is obsolete.');
 }
 
 /**
- * This api generates html to be displayed to students in print overview section, related to their submission status of the given
- * assignment.
- *
- * @deprecated since 3.3
- * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
- * @param array $mysubmissions list of submissions of current user indexed by assignment id.
- * @param string $sqlassignmentids sql clause used to filter open assignments.
- * @param array $assignmentidparams sql params used to filter open assignments.
- * @param stdClass $assignment current assignment
- *
- * @return bool|string html to display , false if nothing needs to be displayed.
- * @throws coding_exception
+ * @deprecated since Moodle 3.3, when the block_course_overview block was removed.
  */
-function assign_get_mysubmission_details_for_print_overview(&$mysubmissions, $sqlassignmentids, $assignmentidparams,
-                                                            $assignment) {
-    global $USER, $DB;
-
-    debugging('The function assign_get_mysubmission_details_for_print_overview() is now deprecated.', DEBUG_DEVELOPER);
-
-    if ($assignment->nosubmissions) {
-        // Offline assignment. No need to display alerts for offline assignments.
-        return false;
-    }
-
-    $strnotsubmittedyet = get_string('notsubmittedyet', 'assign');
-
-    if (!isset($mysubmissions)) {
-
-        // Get all user submissions, indexed by assignment id.
-        $dbparams = array_merge(array($USER->id), $assignmentidparams, array($USER->id));
-        $mysubmissions = $DB->get_records_sql('SELECT a.id AS assignment,
-                                                      a.nosubmissions AS nosubmissions,
-                                                      g.timemodified AS timemarked,
-                                                      g.grader AS grader,
-                                                      g.grade AS grade,
-                                                      s.status AS status
-                                                 FROM {assign} a, {assign_submission} s
-                                            LEFT JOIN {assign_grades} g ON
-                                                      g.assignment = s.assignment AND
-                                                      g.userid = ? AND
-                                                      g.attemptnumber = s.attemptnumber
-                                                WHERE a.id ' . $sqlassignmentids . ' AND
-                                                      s.latest = 1 AND
-                                                      s.assignment = a.id AND
-                                                      s.userid = ?', $dbparams);
-    }
-
-    $submitdetails = '';
-    $submitdetails .= '<div class="details">';
-    $submitdetails .= get_string('mysubmission', 'assign');
-    $submission = false;
-
-    if (isset($mysubmissions[$assignment->id])) {
-        $submission = $mysubmissions[$assignment->id];
-    }
-
-    if ($submission && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
-        // A valid submission already exists, no need to notify students about this.
-        return false;
-    }
-
-    // We need to show details only if a valid submission doesn't exist.
-    if (!$submission ||
-        !$submission->status ||
-        $submission->status == ASSIGN_SUBMISSION_STATUS_DRAFT ||
-        $submission->status == ASSIGN_SUBMISSION_STATUS_NEW
-    ) {
-        $submitdetails .= $strnotsubmittedyet;
-    } else {
-        $submitdetails .= get_string('submissionstatus_' . $submission->status, 'assign');
-    }
-    if ($assignment->markingworkflow) {
-        $workflowstate = $DB->get_field('assign_user_flags', 'workflowstate', array('assignment' =>
-                $assignment->id, 'userid' => $USER->id));
-        if ($workflowstate) {
-            $gradingstatus = 'markingworkflowstate' . $workflowstate;
-        } else {
-            $gradingstatus = 'markingworkflowstate' . ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED;
-        }
-    } else if (!empty($submission->grade) && $submission->grade !== null && $submission->grade >= 0) {
-        $gradingstatus = ASSIGN_GRADING_STATUS_GRADED;
-    } else {
-        $gradingstatus = ASSIGN_GRADING_STATUS_NOT_GRADED;
-    }
-    $submitdetails .= ', ' . get_string($gradingstatus, 'assign');
-    $submitdetails .= '</div>';
-    return $submitdetails;
+function assign_get_mysubmission_details_for_print_overview() {
+    throw new coding_exception('assign_get_mysubmission_details_for_print_overview() can not be used any more and is obsolete.');
 }
 
 /**
- * This api generates html to be displayed to teachers in print overview section, related to the grading status of the given
- * assignment's submissions.
- *
- * @deprecated since 3.3
- * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
- * @param array $unmarkedsubmissions list of submissions of that are currently unmarked indexed by assignment id.
- * @param string $sqlassignmentids sql clause used to filter open assignments.
- * @param array $assignmentidparams sql params used to filter open assignments.
- * @param stdClass $assignment current assignment
- * @param context $context context of the assignment.
- *
- * @return bool|string html to display , false if nothing needs to be displayed.
- * @throws coding_exception
+ * @deprecated since Moodle 3.3, when the block_course_overview block was removed.
  */
-function assign_get_grade_details_for_print_overview(&$unmarkedsubmissions, $sqlassignmentids, $assignmentidparams,
-                                                     $assignment, $context) {
-    global $DB;
-
-    debugging('The function assign_get_grade_details_for_print_overview() is now deprecated.', DEBUG_DEVELOPER);
-
-    if (!isset($unmarkedsubmissions)) {
-        // Build up and array of unmarked submissions indexed by assignment id/ userid
-        // for use where the user has grading rights on assignment.
-        $dbparams = array_merge(array(ASSIGN_SUBMISSION_STATUS_SUBMITTED), $assignmentidparams);
-        $rs = $DB->get_recordset_sql('SELECT s.assignment as assignment,
-                                             s.userid as userid,
-                                             s.id as id,
-                                             s.status as status,
-                                             g.timemodified as timegraded
-                                        FROM {assign_submission} s
-                                   LEFT JOIN {assign_grades} g ON
-                                             s.userid = g.userid AND
-                                             s.assignment = g.assignment AND
-                                             g.attemptnumber = s.attemptnumber
-                                   LEFT JOIN {assign} a ON
-                                             a.id = s.assignment
-                                       WHERE
-                                             ( g.timemodified is NULL OR
-                                             s.timemodified >= g.timemodified OR
-                                             g.grade IS NULL OR
-                                             (g.grade = -1 AND
-                                             a.grade < 0)) AND
-                                             s.timemodified IS NOT NULL AND
-                                             s.status = ? AND
-                                             s.latest = 1 AND
-                                             s.assignment ' . $sqlassignmentids, $dbparams);
-
-        $unmarkedsubmissions = array();
-        foreach ($rs as $rd) {
-            $unmarkedsubmissions[$rd->assignment][$rd->userid] = $rd->id;
-        }
-        $rs->close();
-    }
-
-    // Count how many people can submit.
-    $submissions = 0;
-    if ($students = get_enrolled_users($context, 'mod/assign:view', 0, 'u.id')) {
-        foreach ($students as $student) {
-            if (isset($unmarkedsubmissions[$assignment->id][$student->id])) {
-                $submissions++;
-            }
-        }
-    }
-
-    if ($submissions) {
-        $urlparams = array('id' => $assignment->coursemodule, 'action' => 'grading');
-        $url = new moodle_url('/mod/assign/view.php', $urlparams);
-        $gradedetails = '<div class="details">' .
-                '<a href="' . $url . '">' .
-                get_string('submissionsnotgraded', 'assign', $submissions) .
-                '</a></div>';
-        return $gradedetails;
-    } else {
-        return false;
-    }
-
+function assign_get_grade_details_for_print_overview() {
+    throw new coding_exception('assign_get_grade_details_for_print_overview() can not be used any more and is obsolete.');
 }
 
 /**
index bef787c..bb0843d 100644 (file)
@@ -7586,6 +7586,14 @@ class assign {
             $options = array('' => get_string('markingworkflowstatenotmarked', 'assign')) + $states;
             $mform->addElement('select', 'workflowstate', get_string('markingworkflowstate', 'assign'), $options);
             $mform->addHelpButton('workflowstate', 'markingworkflowstate', 'assign');
+            $gradingstatus = $this->get_grading_status($userid);
+            if ($gradingstatus != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
+                if ($grade->grade && $grade->grade != -1) {
+                    $assigngradestring = html_writer::span(grade_floatval($grade->grade), 'currentgrade');
+                    $label = get_string('currentassigngrade', 'assign');
+                    $mform->addElement('static', 'currentassigngrade', $label, $assigngradestring);
+                }
+            }
         }
 
         if ($this->get_instance()->markingworkflow &&
@@ -7607,6 +7615,7 @@ class assign {
             $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE);
             $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_RELEASED);
         }
+
         $gradestring = '<span class="currentgrade">' . $gradestring . '</span>';
         $mform->addElement('static', 'currentgrade', get_string('currentgrade', 'assign'), $gradestring);
 
index 0d34150..f4cbcef 100644 (file)
@@ -128,8 +128,6 @@ trait mod_assign_test_generator {
             ]);
 
         // Bump all timecreated and timemodified for this user back.
-        // The old assign_print_overview function includes submissions which have been graded where the grade modified
-        // date matches the submission modified date.
         $DB->execute('UPDATE {assign_submission} SET timecreated = timecreated - 1, timemodified = timemodified - 1 WHERE userid = :userid',
             ['userid' => $student->id]);
 
index 759f534..e002e4f 100644 (file)
@@ -46,139 +46,6 @@ class mod_assign_lib_testcase extends advanced_testcase {
     // Use the generator helper.
     use mod_assign_test_generator;
 
-    public function test_assign_print_overview() {
-        global $DB;
-
-        $this->resetAfterTest();
-
-        $course = $this->getDataGenerator()->create_course();
-        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
-        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
-
-        $this->setAdminUser();
-
-        // Assignment with default values.
-        $firstassign = $this->create_instance($course, ['name' => 'First Assignment']);
-
-        // Assignment with submissions.
-        $secondassign = $this->create_instance($course, [
-                'name' => 'Assignment with submissions',
-                'duedate' => time(),
-                'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL,
-                'maxattempts' => 3,
-                'submissiondrafts' => 1,
-                'assignsubmission_onlinetext_enabled' => 1,
-            ]);
-        $this->add_submission($student, $secondassign);
-        $this->submit_for_grading($student, $secondassign);
-        $this->mark_submission($teacher, $secondassign, $student, 50.0);
-
-        // Past assignments should not show up.
-        $pastassign = $this->create_instance($course, [
-                'name' => 'Past Assignment',
-                'duedate' => time() - DAYSECS - 1,
-                'cutoffdate' => time() - DAYSECS,
-                'nosubmissions' => 0,
-                'assignsubmission_onlinetext_enabled' => 1,
-            ]);
-
-        // Open assignments should show up only if relevant.
-        $openassign = $this->create_instance($course, [
-                'name' => 'Open Assignment',
-                'duedate' => time(),
-                'cutoffdate' => time() + DAYSECS,
-                'nosubmissions' => 0,
-                'assignsubmission_onlinetext_enabled' => 1,
-            ]);
-        $pastsubmission = $pastassign->get_user_submission($student->id, true);
-        $opensubmission = $openassign->get_user_submission($student->id, true);
-
-        // Check the overview as the different users.
-        // For students , open assignments should show only when there are no valid submissions.
-        $this->setUser($student);
-        $overview = array();
-        $courses = $DB->get_records('course', array('id' => $course->id));
-        assign_print_overview($courses, $overview);
-        $this->assertDebuggingCalledCount(3);
-        $this->assertEquals(1, count($overview));
-        $this->assertRegExp('/.*Open Assignment.*/', $overview[$course->id]['assign']); // No valid submission.
-        $this->assertNotRegExp('/.*First Assignment.*/', $overview[$course->id]['assign']); // Has valid submission.
-
-        // And now submit the submission.
-        $opensubmission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
-        $openassign->testable_update_submission($opensubmission, $student->id, true, false);
-
-        $overview = array();
-        assign_print_overview($courses, $overview);
-        $this->assertDebuggingCalledCount(3);
-        $this->assertEquals(0, count($overview));
-
-        $this->setUser($teacher);
-        $overview = array();
-        assign_print_overview($courses, $overview);
-        $this->assertDebuggingCalledCount(3);
-        $this->assertEquals(1, count($overview));
-        // Submissions without a grade.
-        $this->assertRegExp('/.*Open Assignment.*/', $overview[$course->id]['assign']);
-        $this->assertNotRegExp('/.*Assignment with submissions.*/', $overview[$course->id]['assign']);
-
-        $this->setUser($teacher);
-        $overview = array();
-        assign_print_overview($courses, $overview);
-        $this->assertDebuggingCalledCount(3);
-        $this->assertEquals(1, count($overview));
-        // Submissions without a grade.
-        $this->assertRegExp('/.*Open Assignment.*/', $overview[$course->id]['assign']);
-        $this->assertNotRegExp('/.*Assignment with submissions.*/', $overview[$course->id]['assign']);
-
-        // Let us grade a submission.
-        $this->setUser($teacher);
-        $data = new stdClass();
-        $data->grade = '50.0';
-        $openassign->testable_apply_grade_to_user($data, $student->id, 0);
-
-        // The assign_print_overview expects the grade date to be after the submission date.
-        $graderecord = $DB->get_record('assign_grades', array('assignment' => $openassign->get_instance()->id,
-            'userid' => $student->id, 'attemptnumber' => 0));
-        $graderecord->timemodified += 1;
-        $DB->update_record('assign_grades', $graderecord);
-
-        $overview = array();
-        assign_print_overview($courses, $overview);
-        // Now assignment 4 should not show up.
-        $this->assertDebuggingCalledCount(3);
-        $this->assertEmpty($overview);
-
-        $this->setUser($teacher);
-        $overview = array();
-        assign_print_overview($courses, $overview);
-        $this->assertDebuggingCalledCount(3);
-        // Now assignment 4 should not show up.
-        $this->assertEmpty($overview);
-    }
-
-    /**
-     * Test that assign_print_overview does not return any assignments which are Open Offline.
-     */
-    public function test_assign_print_overview_open_offline() {
-        $this->resetAfterTest();
-        $course = $this->getDataGenerator()->create_course();
-        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
-
-        $this->setAdminUser();
-        $openassign = $this->create_instance($course, [
-                'duedate' => time() + DAYSECS,
-                'cutoffdate' => time() + (DAYSECS * 2),
-            ]);
-
-        $this->setUser($student);
-        $overview = [];
-        assign_print_overview([$course], $overview);
-
-        $this->assertDebuggingCalledCount(1);
-        $this->assertEquals(0, count($overview));
-    }
-
     /**
      * Test that assign_print_recent_activity shows ungraded submitted assignments.
      */
diff --git a/mod/assignment/db/subplugins.json b/mod/assignment/db/subplugins.json
new file mode 100644 (file)
index 0000000..7c6b9c0
--- /dev/null
@@ -0,0 +1,5 @@
+{
+    "plugintypes": {
+        "assignment": "mod\/assignment\/type"
+    }
+}
index 52e3b41..5ea3af4 100644 (file)
@@ -1,3 +1,26 @@
 <?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/>.
 
-$subplugins = array('assignment'=>'mod/assignment/type');
+/**
+ * Definition of sub-plugins
+ *
+ * @package     mod_assignment
+ * @copyright   1999 onwards Martin Dougiamas  {@link http://moodle.com}
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+debugging('Use of subplugins.php has been deprecated. Please provide a subplugins.json instead.', DEBUG_DEVELOPER);
+$subplugins = (array) json_decode(file_get_contents(__DIR__ . "/subplugins.json"))->plugintypes;
diff --git a/mod/book/db/subplugins.json b/mod/book/db/subplugins.json
new file mode 100644 (file)
index 0000000..97e2827
--- /dev/null
@@ -0,0 +1,5 @@
+{
+    "plugintypes": {
+        "booktool": "mod\/book\/tool"
+    }
+}
index e3e2e90..5348581 100644 (file)
@@ -24,6 +24,5 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$subplugins = array(
-    'booktool'       => 'mod/book/tool',
-);
+debugging('Use of subplugins.php has been deprecated. Please provide a subplugins.json instead.', DEBUG_DEVELOPER);
+$subplugins = (array) json_decode(file_get_contents(__DIR__ . "/subplugins.json"))->plugintypes;
index bd78708..4441233 100644 (file)
@@ -1136,44 +1136,10 @@ function chat_get_post_actions() {
 }
 
 /**
- * @deprecated since 3.3
- * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
- * @global object
- * @global object
- * @param array $courses
- * @param array $htmlarray Passed by reference
+ * @deprecated since Moodle 3.3, when the block_course_overview block was removed.
  */
-function chat_print_overview($courses, &$htmlarray) {
-    global $USER, $CFG;
-
-    debugging('The function chat_print_overview() is now deprecated.', DEBUG_DEVELOPER);
-
-    if (empty($courses) || !is_array($courses) || count($courses) == 0) {
-        return array();
-    }
-
-    if (!$chats = get_all_instances_in_courses('chat', $courses)) {
-        return;
-    }
-
-    $strchat = get_string('modulename', 'chat');
-    $strnextsession  = get_string('nextsession', 'chat');
-
-    foreach ($chats as $chat) {
-        if ($chat->chattime and $chat->schedule) {  // A chat is scheduled.
-            $str = '<div class="chat overview"><div class="name">'.
-                   $strchat.': <a '.($chat->visible ? '' : ' class="dimmed"').
-                   ' href="'.$CFG->wwwroot.'/mod/chat/view.php?id='.$chat->coursemodule.'">'.
-                   $chat->name.'</a></div>';
-            $str .= '<div class="info">'.$strnextsession.': '.userdate($chat->chattime).'</div></div>';
-
-            if (empty($htmlarray[$chat->course]['chat'])) {
-                $htmlarray[$chat->course]['chat'] = $str;
-            } else {
-                $htmlarray[$chat->course]['chat'] .= $str;
-            }
-        }
-    }
+function chat_print_overview() {
+    throw new coding_exception('chat_print_overview() can not be used any more and is obsolete.');
 }
 
 
index 746233c..f9ec368 100644 (file)
@@ -46,14 +46,12 @@ $string['atleastoneoption'] = 'You need to provide at least one possible answer.
 $string['full'] = '(Full)';
 $string['havetologin'] = 'You have to log in before you can submit your choice';
 $string['choice'] = 'Choice';
-$string['choiceactivityname'] = 'Choice: {$a}';
 $string['choice:addinstance'] = 'Add a new choice';
 $string['choiceclose'] = 'Allow responses until';
 $string['choice:deleteresponses'] = 'Modify and delete responses';
 $string['choice:downloadresponses'] = 'Download responses';
 $string['choicefull'] = 'One or more of the options you have selected have already been filled. Your response has not been saved. Please make another selection.';
 $string['choice:choose'] = 'Record a choice';
-$string['choicecloseson'] = 'Choice closes on {$a}';
 $string['choicename'] = 'Choice name';
 $string['choiceopen'] = 'Allow responses from';
 $string['choiceoptions'] = 'Choice options';
@@ -147,4 +145,7 @@ $string['viewchoices'] = 'View choices';
 $string['withselected'] = 'With selected';
 $string['userchoosethisoption'] = 'Users who chose this option';
 $string['yourselection'] = 'Your selection';
-$string['skipresultgraph'] = 'Skip result graph';
+
+// Deprecated since Moodle 3.8.
+$string['choiceactivityname'] = 'Choice: {$a}';
+$string['choicecloseson'] = 'Choice closes on {$a}';
index 4177da3..3e9f384 100644 (file)
@@ -1 +1,2 @@
-skipresultgraph,mod_choice
+choiceactivityname,mod_choice
+choicecloseson,mod_choice
\ No newline at end of file
index c6704c7..ac0da08 100644 (file)
@@ -922,83 +922,10 @@ function choice_page_type_list($pagetype, $parentcontext, $currentcontext) {
 }
 
 /**
- * Prints choice summaries on MyMoodle Page
- *
- * Prints choice name, due date and attempt information on
- * choice activities that have a deadline that has not already passed
- * and it is available for completing.
- *
- * @deprecated since 3.3
- * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
- * @uses CONTEXT_MODULE
- * @param array $courses An array of course objects to get choice instances from.
- * @param array $htmlarray Store overview output array( course ID => 'choice' => HTML output )
+ * @deprecated since Moodle 3.3, when the block_course_overview block was removed.
  */
-function choice_print_overview($courses, &$htmlarray) {
-    global $USER, $DB, $OUTPUT;
-
-    debugging('The function choice_print_overview() is now deprecated.', DEBUG_DEVELOPER);
-
-    if (empty($courses) || !is_array($courses) || count($courses) == 0) {
-        return;
-    }
-    if (!$choices = get_all_instances_in_courses('choice', $courses)) {
-        return;
-    }
-
-    $now = time();
-    foreach ($choices as $choice) {
-        if ($choice->timeclose != 0                                      // If this choice is scheduled.
-            and $choice->timeclose >= $now                               // And the deadline has not passed.
-            and ($choice->timeopen == 0 or $choice->timeopen <= $now)) { // And the choice is available.
-
-            // Visibility.
-            $class = (!$choice->visible) ? 'dimmed' : '';
-
-            // Link to activity.
-            $url = new moodle_url('/mod/choice/view.php', array('id' => $choice->coursemodule));
-            $url = html_writer::link($url, format_string($choice->name), array('class' => $class));
-            $str = $OUTPUT->box(get_string('choiceactivityname', 'choice', $url), 'name');
-
-             // Deadline.
-            $str .= $OUTPUT->box(get_string('choicecloseson', 'choice', userdate($choice->timeclose)), 'info');
-
-            // Display relevant info based on permissions.
-            if (has_capability('mod/choice:readresponses', context_module::instance($choice->coursemodule))) {
-                $attempts = $DB->count_records_sql('SELECT COUNT(DISTINCT userid) FROM {choice_answers} WHERE choiceid = ?',
-                    [$choice->id]);
-                $url = new moodle_url('/mod/choice/report.php', ['id' => $choice->coursemodule]);
-                $str .= $OUTPUT->box(html_writer::link($url, get_string('viewallresponses', 'choice', $attempts)), 'info');
-
-            } else if (has_capability('mod/choice:choose', context_module::instance($choice->coursemodule))) {
-                // See if the user has submitted anything.
-                $answers = $DB->count_records('choice_answers', array('choiceid' => $choice->id, 'userid' => $USER->id));
-                if ($answers > 0) {
-                    // User has already selected an answer, nothing to show.
-                    $str = '';
-                } else {
-                    // User has not made a selection yet.
-                    $str .= $OUTPUT->box(get_string('notanswered', 'choice'), 'info');
-                }
-            } else {
-                // Does not have permission to do anything on this choice activity.
-                $str = '';
-            }
-
-            // Make sure we have something to display.
-            if (!empty($str)) {
-                // Generate the containing div.
-                $str = $OUTPUT->box($str, 'choice overview');
-
-                if (empty($htmlarray[$choice->course]['choice'])) {
-                    $htmlarray[$choice->course]['choice'] = $str;
-                } else {
-                    $htmlarray[$choice->course]['choice'] .= $str;
-                }
-            }
-        }
-    }
-    return;
+function choice_print_overview() {
+    throw new coding_exception('choice_print_overview() can not be used any more and is obsolete.');
 }
 
 
diff --git a/mod/data/db/subplugins.json b/mod/data/db/subplugins.json
new file mode 100644 (file)
index 0000000..67639a6
--- /dev/null
@@ -0,0 +1,6 @@
+{
+    "plugintypes": {
+        "datafield": "mod\/data\/field",
+        "datapreset": "mod\/data\/preset"
+    }
+}
index 2c1a7e9..d0dd67e 100644 (file)
@@ -1,4 +1,26 @@
 <?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/>.
 
-$subplugins = array('datafield'  => 'mod/data/field',
-                    'datapreset' => 'mod/data/preset');
+/**
+ * Definition of sub-plugins
+ *
+ * @package   mod_data
+ * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+debugging('Use of subplugins.php has been deprecated. Please provide a subplugins.json instead.', DEBUG_DEVELOPER);
+$subplugins = (array) json_decode(file_get_contents(__DIR__ . "/subplugins.json"))->plugintypes;
index 74181b6..75583df 100644 (file)
@@ -272,7 +272,7 @@ class subscriptions {
         $sql = "SELECT $fields
                 FROM {user} u
                 JOIN ($esql) je ON je.id = u.id
-               WHERE u.auth <> 'nologin' AND u.suspended = 0
+               WHERE u.auth <> 'nologin' AND u.suspended = 0 AND u.confirmed = 1
             ORDER BY $sort";
 
         return $DB->get_records_sql($sql, $params);
@@ -443,7 +443,7 @@ class subscriptions {
                         ) subscriptions
                         JOIN {user} u ON u.id = subscriptions.userid
                         JOIN ($esql) je ON je.id = u.id
-                        WHERE u.auth <> 'nologin' AND u.suspended = 0
+                        WHERE u.auth <> 'nologin' AND u.suspended = 0 AND u.confirmed = 1
                         ORDER BY u.email ASC";
 
             } else {
@@ -452,7 +452,7 @@ class subscriptions {
                         JOIN ($esql) je ON je.id = u.id
                         JOIN {forum_subscriptions} s ON s.userid = u.id
                         WHERE
-                          s.forum = :forumid AND u.auth <> 'nologin' AND u.suspended = 0
+                          s.forum = :forumid AND u.auth <> 'nologin' AND u.suspended = 0 AND u.confirmed = 1
                         ORDER BY u.email ASC";
             }
             $results = $DB->get_records_sql($sql, $params);
index 37f09d7..5a6fb60 100644 (file)
@@ -1257,9 +1257,13 @@ class mod_forum_external extends external_api {
         $context = context_module::instance($cm->id);
         self::validate_context($context);
 
+        $coursecontext = \context_course::instance($forum->get_course_id());
+        $discussionsubscribe = \mod_forum\subscriptions::get_user_default_subscription($forumrecord, $coursecontext,
+            $cm, null);
+
         // Validate options.
         $options = array(
-            'discussionsubscribe' => true,
+            'discussionsubscribe' => $discussionsubscribe,
             'private'             => false,
             'inlineattachmentsid' => 0,
             'attachmentsid' => null,
@@ -1350,9 +1354,11 @@ class mod_forum_external extends external_api {
                 $completion->update_state($cm, COMPLETION_COMPLETE);
             }
 
-            $settings = new stdClass();
-            $settings->discussionsubscribe = $options['discussionsubscribe'];
-            forum_post_subscription($settings, $forumrecord, $discussionrecord);
+            if ($options['discussionsubscribe']) {
+                $settings = new stdClass();
+                $settings->discussionsubscribe = $options['discussionsubscribe'];
+                forum_post_subscription($settings, $forumrecord, $discussionrecord);
+            }
         } else {
             throw new moodle_exception('couldnotadd', 'forum');
         }
index dd59859..cddc5b1 100644 (file)
@@ -1 +1,3 @@
 inpagereplysubject,mod_forum
+overviewnumpostssince,mod_forum
+overviewnumunread,mod_forum
index 89a544f..5c20a6b 100644 (file)
@@ -438,8 +438,6 @@ $string['numberofreplies'] = 'Number of replies: {$a}';
 $string['olderdiscussions'] = 'Older discussions';
 $string['oldertopics'] = 'Older topics';
 $string['oldpostdays'] = 'Read after days';
-$string['overviewnumpostssince'] = '{$a} posts since last login';
-$string['overviewnumunread'] = '{$a} total unread';
 $string['page-mod-forum-x'] = 'Any forum module page';
 $string['page-mod-forum-view'] = 'Forum module main page';
 $string['page-mod-forum-discuss'] = 'Forum module discussion thread page';
@@ -675,3 +673,5 @@ $string['forumbodydeleted'] = 'The content of this forum post has been removed a
 
 // Deprecated since Moodle 3.8.
 $string['inpagereplysubject'] = 'Re: {$a}';
+$string['overviewnumpostssince'] = '{$a} posts since last login';
+$string['overviewnumunread'] = '{$a} total unread';
index 7763148..db8a3b6 100644 (file)
@@ -540,44 +540,10 @@ function forum_user_complete($course, $user, $mod, $forum) {
 }
 
 /**
- * Filters the forum discussions according to groups membership and config.
- *
- * @deprecated since 3.3
- * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
- * @since  Moodle 2.8, 2.7.1, 2.6.4
- * @param  array $discussions Discussions with new posts array
- * @return array Forums with the number of new posts
+ * @deprecated since Moodle 3.3, when the block_course_overview block was removed.
  */
-function forum_filter_user_groups_discussions($discussions) {
-
-    debugging('The function forum_filter_user_groups_discussions() is now deprecated.', DEBUG_DEVELOPER);
-
-    // Group the remaining discussions posts by their forumid.
-    $filteredforums = array();
-
-    // Discard not visible groups.
-    foreach ($discussions as $discussion) {
-
-        // Course data is already cached.
-        $instances = get_fast_modinfo($discussion->course)->get_instances();
-        $forum = $instances['forum'][$discussion->forum];
-
-        // Continue if the user should not see this discussion.
-        if (!forum_is_user_group_discussion($forum, $discussion->groupid)) {
-            continue;
-        }
-
-        // Grouping results by forum.
-        if (empty($filteredforums[$forum->instance])) {
-            $filteredforums[$forum->instance] = new stdClass();
-            $filteredforums[$forum->instance]->id = $forum->id;
-            $filteredforums[$forum->instance]->count = 0;
-        }
-        $filteredforums[$forum->instance]->count += $discussion->count;
-
-    }
-
-    return $filteredforums;
+function forum_filter_user_groups_discussions() {
+    throw new coding_exception('forum_filter_user_groups_discussions() can not be used any more and is obsolete.');
 }
 
 /**
@@ -607,155 +573,10 @@ function forum_is_user_group_discussion(cm_info $cm, $discussiongroupid) {
 }
 
 /**
- * @deprecated since 3.3
- * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
- * @global object
- * @global object
- * @global object
- * @param array $courses
- * @param array $htmlarray
+ * @deprecated since Moodle 3.3, when the block_course_overview block was removed.
  */
-function forum_print_overview($courses,&$htmlarray) {
-    global $USER, $CFG, $DB, $SESSION;
-
-    debugging('The function forum_print_overview() is now deprecated.', DEBUG_DEVELOPER);
-
-    if (empty($courses) || !is_array($courses) || count($courses) == 0) {
-        return array();
-    }
-
-    if (!$forums = get_all_instances_in_courses('forum',$courses)) {
-        return;
-    }
-
-    // Courses to search for new posts
-    $coursessqls = array();
-    $params = array();
-    foreach ($courses as $course) {
-
-        // If the user has never entered into the course all posts are pending
-        if ($course->lastaccess == 0) {
-            $coursessqls[] = '(d.course = ?)';
-            $params[] = $course->id;
-
-        // Only posts created after the course last access
-        } else {
-            $coursessqls[] = '(d.course = ? AND p.created > ?)';
-            $params[] = $course->id;
-            $params[] = $course->lastaccess;
-        }
-    }
-    $params[] = $USER->id;
-    $coursessql = implode(' OR ', $coursessqls);
-
-    $sql = "SELECT d.id, d.forum, d.course, d.groupid, COUNT(*) as count "
-                .'FROM {forum_discussions} d '
-                .'JOIN {forum_posts} p ON p.discussion = d.id '
-                ."WHERE ($coursessql) "
-                .'AND p.deleted <> 1 '
-                .'AND p.userid != ? '
-                .'AND (d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?)) '
-                .'GROUP BY d.id, d.forum, d.course, d.groupid '
-                .'ORDER BY d.course, d.forum';
-    $params[] = time();
-    $params[] = time();
-
-    // Avoid warnings.
-    if (!$discussions = $DB->get_records_sql($sql, $params)) {
-        $discussions = array();
-    }
-
-    $forumsnewposts = forum_filter_user_groups_discussions($discussions);
-
-    // also get all forum tracking stuff ONCE.
-    $trackingforums = array();
-    foreach ($forums as $forum) {
-        if (forum_tp_can_track_forums($forum)) {
-            $trackingforums[$forum->id] = $forum;
-        }
-    }
-
-    if (count($trackingforums) > 0) {
-        $cutoffdate = isset($CFG->forum_oldpostdays) ? (time() - ($CFG->forum_oldpostdays*24*60*60)) : 0;
-        $sql = 'SELECT d.forum,d.course,COUNT(p.id) AS count '.
-            ' FROM {forum_posts} p '.
-            ' JOIN {forum_discussions} d ON p.discussion = d.id '.
-            ' LEFT JOIN {forum_read} r ON r.postid = p.id AND r.userid = ? WHERE p.deleted <> 1 AND (';
-        $params = array($USER->id);
-
-        foreach ($trackingforums as $track) {
-            $sql .= '(d.forum = ? AND (d.groupid = -1 OR d.groupid = 0 OR d.groupid = ?)) OR ';
-            $params[] = $track->id;
-            if (isset($SESSION->currentgroup[$track->course])) {
-                $groupid =  $SESSION->currentgroup[$track->course];
-            } else {
-                // get first groupid
-                $groupids = groups_get_all_groups($track->course, $USER->id);
-                if ($groupids) {
-                    reset($groupids);
-                    $groupid = key($groupids);
-                    $SESSION->currentgroup[$track->course] = $groupid;
-                } else {
-                    $groupid = 0;
-                }
-                unset($groupids);
-            }
-            $params[] = $groupid;
-        }
-        $sql = substr($sql,0,-3); // take off the last OR
-        $sql .= ') AND p.modified >= ? AND r.id is NULL ';
-        $sql .= 'AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?)) ';
-        $sql .= 'GROUP BY d.forum,d.course';
-        $params[] = $cutoffdate;
-        $params[] = time();
-        $params[] = time();
-
-        if (!$unread = $DB->get_records_sql($sql, $params)) {
-            $unread = array();
-        }
-    } else {
-        $unread = array();
-    }
-
-    if (empty($unread) and empty($forumsnewposts)) {
-        return;
-    }
-
-    $strforum = get_string('modulename','forum');
-
-    foreach ($forums as $forum) {
-        $str = '';
-        $count = 0;
-        $thisunread = 0;
-        $showunread = false;
-        // either we have something from logs, or trackposts, or nothing.
-        if (array_key_exists($forum->id, $forumsnewposts) && !empty($forumsnewposts[$forum->id])) {
-            $count = $forumsnewposts[$forum->id]->count;
-        }
-        if (array_key_exists($forum->id,$unread)) {
-            $thisunread = $unread[$forum->id]->count;
-            $showunread = true;
-        }
-        if ($count > 0 || $thisunread > 0) {
-            $str .= '<div class="overview forum"><div class="name">'.$strforum.': <a title="'.$strforum.'" href="'.$CFG->wwwroot.'/mod/forum/view.php?f='.$forum->id.'">'.
-                $forum->name.'</a></div>';
-            $str .= '<div class="info"><span class="postsincelogin">';
-            $str .= get_string('overviewnumpostssince', 'forum', $count)."</span>";
-            if (!empty($showunread)) {
-                $str .= '<div class="unreadposts">'.get_string('overviewnumunread', 'forum', $thisunread).'</div>';
-            }
-            $str .= '</div></div>';
-        }
-        if (!empty($str)) {
-            if (!array_key_exists($forum->course,$htmlarray)) {
-                $htmlarray[$forum->course] = array();
-            }
-            if (!array_key_exists('forum',$htmlarray[$forum->course])) {
-                $htmlarray[$forum->course]['forum'] = ''; // initialize, avoid warnings
-            }
-            $htmlarray[$forum->course]['forum'] .= $str;
-        }
-    }
+function forum_print_overview() {
+    throw new coding_exception('forum_print_overview() can not be used any more and is obsolete.');
 }
 
 /**
index ec2fa05..49af7e2 100644 (file)
@@ -35,6 +35,8 @@ $name    = optional_param('name', '', PARAM_CLEAN);
 $confirm = optional_param('confirm', 0, PARAM_INT);
 $groupid = optional_param('groupid', null, PARAM_INT);
 $subject = optional_param('subject', '', PARAM_TEXT);
+
+// Values posted via the inpage reply form.
 $prefilledpost = optional_param('post', '', PARAM_TEXT);
 $prefilledpostformat = optional_param('postformat', FORMAT_MOODLE, PARAM_INT);
 $prefilledprivatereply = optional_param('privatereply', false, PARAM_BOOL);
@@ -778,6 +780,25 @@ $mformpost->set_data(
     (isset($discussion->id) ? array('discussion' => $discussion->id) : array())
 );
 
+// If we are being redirected via a no_submit_button press OR if the message is being prefilled.
+// then set the initial 'dirty' state.
+// - A prefilled post will exist when being redirected from the inpage reply form.
+// - A no_submit_button press occurs when being redirected from the inpage add new discussion post form.
+$dirty = $prefilledpost ? true : false;
+if ($mformpost->no_submit_button_pressed()) {
+    $data = $mformpost->get_submitted_data();
+
+    // If a no submit button has been pressed but the default values haven't been then reset the form change.
+    if (!$dirty && isset($data->message['text']) && !empty(trim($data->message['text']))) {
+        $dirty = true;
+    }
+
+    if (!$dirty && isset($data->message['message']) && !empty(trim($data->message['message']))) {
+        $dirty = true;
+    }
+}
+$mformpost->set_initial_dirty_state($dirty);
+
 if ($mformpost->is_cancelled()) {
     if (!isset($discussion->id) || $forum->type === 'single') {
         // Single forums don't have a discussion page.
index 93632b4..3db6787 100644 (file)
                                     <td scope="col" class="p-0 text-center align-middle">
                                         {{#unread}}
                                             {{! TODO Rewrite as AJAX}}
-                                            <div class="p-3 w-100 h-100 d-block">
+                                            <span class="p-1 w-100 h-100 d-block unread">
                                                 <a href="{{{discussion.urls.viewfirstunread}}}">{{unread}}</a>
                                                 <a href="{{{discussion.urls.markasread}}}">{{#pix}}t/markasread, core, {{#str}}markalldread, mod_forum{{/str}}{{/pix}}</a>
-                                            </div>
+                                            </span>
                                         {{/unread}}
                                         {{^unread}}
                                             <span class="p-3 w-100 h-100 d-block">
diff --git a/mod/forum/tests/behat/app_basic_usage.feature b/mod/forum/tests/behat/app_basic_usage.feature
deleted file mode 100644 (file)
index 1cd4db6..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-@mod @mod_forum @app @javascript
-Feature: Test basic usage in app
-  In order to participate in the forum while using the mobile app
-  As a student
-  I need basic forum functionality to work
-
-  Background:
-    Given the following "courses" exist:
-      | fullname | shortname |
-      | Course 1 | C1        |
-    And the following "users" exist:
-      | username |
-      | student1 |
-    And the following "course enrolments" exist:
-      | user     | course | role    |
-      | student1 | C1     | student |
-    And the following "activities" exist:
-      | activity   | name            | intro       | course | idnumber | groupmode |
-      | forum      | Test forum name | Test forum  | C1     | forum    | 0         |
-
-  Scenario: Student starts a discussion
-    When I enter the app
-    And I log in as "student1"
-    And I press "Course 1" near "Course overview" in the app
-    And I press "Test forum name" in the app
-    And I press "Add a new discussion topic" in the app
-    And I set the field "Subject" to "My happy subject" in the app
-    And I set the field "Message" to "An awesome message" in the app
-    And I press "Post to forum" in the app
-    Then I should see "My happy subject"
-    And I should see "An awesome message"
-
-  Scenario: Student posts a reply
-    When I enter the app
-    And I log in as "student1"
-    And I press "Course 1" near "Course overview" in the app
-    And I press "Test forum name" in the app
-    And I press "Add a new discussion topic" in the app
-    And I set the field "Subject" to "DiscussionSubject" in the app
-    And I set the field "Message" to "DiscussionMessage" in the app
-    And I press "Post to forum" in the app
-    And I press "DiscussionSubject" in the app
-    And I press "Reply" in the app
-    And I set the field "Message" to "ReplyMessage" in the app
-    And I press "Post to forum" in the app
-    Then I should see "DiscussionMessage"
-    And I should see "ReplyMessage"
-
-  Scenario: Test that 'open in browser' works for forum
-    When I enter the app
-    And I change viewport size to "360x640"
-    And I log in as "student1"
-    And I press "Course 1" near "Course overview" in the app
-    And I press "Test forum name" in the app
-    And I press the page menu button in the app
-    And I press "Open in browser" in the app
-    And I switch to the browser tab opened by the app
-    And I log in as "student1"
-    Then I should see "Test forum name"
-    And I should see "Add a new discussion topic"
-    And I close the browser tab opened by the app
-    And I press the back button in the app
index 8b6fd97..97db997 100644 (file)
@@ -1691,6 +1691,92 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
         }
     }
 
+    /**
+     * Test add_discussion_post and auto subscription to a discussion.
+     */
+    public function test_add_discussion_post_subscribe_discussion() {
+        global $USER;
+
+        $this->resetAfterTest(true);
+
+        self::setAdminUser();
+
+        $user = self::getDataGenerator()->create_user();
+        $admin = get_admin();
+        // Create course to add the module.
+        $course = self::getDataGenerator()->create_course(array('groupmode' => VISIBLEGROUPS, 'groupmodeforce' => 0));
+
+        $this->getDataGenerator()->enrol_user($user->id, $course->id);
+
+        // Forum with tracking off.
+        $record = new stdClass();
+        $record->course = $course->id;
+        $forum = self::getDataGenerator()->create_module('forum', $record);
+        $cm = get_coursemodule_from_id('forum', $forum->cmid, 0, false, MUST_EXIST);
+
+        // Add discussions to the forums.
+        $record = new stdClass();
+        $record->course = $course->id;
+        $record->userid = $admin->id;
+        $record->forum = $forum->id;
+        $discussion1 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+        $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
+        // Try to post as user.
+        self::setUser($user);
+        // Enable auto subscribe discussion.
+        $USER->autosubscribe = true;
+        // Add a discussion post in a forum discussion where the user is not subscribed (auto-subscribe preference enabled).
+        mod_forum_external::add_discussion_post($discussion1->firstpost, 'some subject', 'some text here...');
+
+        $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id);
+        $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
+        // We receive the discussion and the post.
+        $this->assertEquals(2, count($posts['posts']));
+        // The user should be subscribed to the discussion after adding a discussion post.
+        $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion1->id, $cm));
+
+        // Disable auto subscribe discussion.
+        $USER->autosubscribe = false;
+        $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion1->id, $cm));
+        // Add a discussion post in a forum discussion where the user is subscribed (auto-subscribe preference disabled).
+        mod_forum_external::add_discussion_post($discussion1->firstpost, 'some subject 1', 'some text here 1...');
+
+        $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id);
+        $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
+        // We receive the discussion and the post.
+        $this->assertEquals(3, count($posts['posts']));
+        // The user should still be subscribed to the discussion after adding a discussion post.
+        $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion1->id, $cm));
+
+        $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion2->id, $cm));
+        // Add a discussion post in a forum discussion where the user is not subscribed (auto-subscribe preference disabled).
+        mod_forum_external::add_discussion_post($discussion2->firstpost, 'some subject 2', 'some text here 2...');
+
+        $posts = mod_forum_external::get_forum_discussion_posts($discussion2->id);
+        $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
+        // We receive the discussion and the post.
+        $this->assertEquals(2, count($posts['posts']));
+        // The user should still not be subscribed to the discussion after adding a discussion post.
+        $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion2->id, $cm));
+
+        // Passing a value for the discussionsubscribe option parameter.
+        $this->assertFalse(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion2->id, $cm));
+        // Add a discussion post in a forum discussion where the user is not subscribed (auto-subscribe preference disabled),
+        // and the option parameter 'discussionsubscribe' => true in the webservice.
+        $option = array('name' => 'discussionsubscribe', 'value' => true);
+        $options[] = $option;
+        mod_forum_external::add_discussion_post($discussion2->firstpost, 'some subject 2', 'some text here 2...',
+            $options);
+
+        $posts = mod_forum_external::get_forum_discussion_posts($discussion2->id);
+        $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
+        // We receive the discussion and the post.
+        $this->assertEquals(3, count($posts['posts']));
+        // The user should now be subscribed to the discussion after adding a discussion post.
+        $this->assertTrue(\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussion2->id, $cm));
+    }
+
     /*
      * Test add_discussion. A basic test since all the API functions are already covered by unit tests.
      */
index 80e2773..8c696fe 100644 (file)
@@ -2524,273 +2524,6 @@ class mod_forum_lib_testcase extends advanced_testcase {
         $this->assertArrayHasKey('forumdiscussions', $nodes->getValue($tree));
     }
 
-    public function test_print_overview() {
-        $this->resetAfterTest();
-        $course1 = self::getDataGenerator()->create_course();
-        $course2 = self::getDataGenerator()->create_course();
-
-        // Create an author user.
-        $author = self::getDataGenerator()->create_user();
-        $this->getDataGenerator()->enrol_user($author->id, $course1->id);
-        $this->getDataGenerator()->enrol_user($author->id, $course2->id);
-
-        // Create a viewer user.
-        $viewer = self::getDataGenerator()->create_user((object) array('trackforums' => 1));
-        $this->getDataGenerator()->enrol_user($viewer->id, $course1->id);
-        $this->getDataGenerator()->enrol_user($viewer->id, $course2->id);
-
-        // Create two forums - one in each course.
-        $record = new stdClass();
-        $record->course = $course1->id;
-        $forum1 = self::getDataGenerator()->create_module('forum', (object) array('course' => $course1->id));
-        $forum2 = self::getDataGenerator()->create_module('forum', (object) array('course' => $course2->id));
-
-        // A standard post in the forum.
-        $record = new stdClass();
-        $record->course = $course1->id;
-        $record->userid = $author->id;
-        $record->forum = $forum1->id;
-        $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
-
-        $this->setUser($viewer->id);
-        $courses = array(
-            $course1->id => clone $course1,
-            $course2->id => clone $course2,
-        );
-
-        foreach ($courses as $courseid => $course) {
-            $courses[$courseid]->lastaccess = 0;
-        }
-        $results = array();
-        forum_print_overview($courses, $results);
-        $this->assertDebuggingCalledCount(2);
-
-        // There should be one entry for course1, and no others.
-        $this->assertCount(1, $results);
-
-        // There should be one entry for a forum in course1.
-        $this->assertCount(1, $results[$course1->id]);
-        $this->assertArrayHasKey('forum', $results[$course1->id]);
-    }
-
-    public function test_print_overview_groups() {
-        $this->resetAfterTest();
-        $course1 = self::getDataGenerator()->create_course();
-        $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course1->id));
-        $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course1->id));
-
-        // Create an author user.
-        $author = self::getDataGenerator()->create_user();
-        $this->getDataGenerator()->enrol_user($author->id, $course1->id);
-
-        // Create two viewer users - one in each group.
-        $viewer1 = self::getDataGenerator()->create_user((object) array('trackforums' => 1));
-        $this->getDataGenerator()->enrol_user($viewer1->id, $course1->id);
-        $this->getDataGenerator()->create_group_member(array('userid' => $viewer1->id, 'groupid' => $group1->id));
-
-        $viewer2 = self::getDataGenerator()->create_user((object) array('trackforums' => 1));
-        $this->getDataGenerator()->enrol_user($viewer2->id, $course1->id);
-        $this->getDataGenerator()->create_group_member(array('userid' => $viewer2->id, 'groupid' => $group2->id));
-
-        // Create a forum.
-        $record = new stdClass();
-        $record->course = $course1->id;
-        $forum1 = self::getDataGenerator()->create_module('forum', (object) array(
-            'course'        => $course1->id,
-            'groupmode'     => SEPARATEGROUPS,
-        ));
-
-        // A post in the forum for group1.
-        $record = new stdClass();
-        $record->course     = $course1->id;
-        $record->userid     = $author->id;
-        $record->forum      = $forum1->id;
-        $record->groupid    = $group1->id;
-        $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
-
-        $course1->lastaccess = 0;
-        $courses = array($course1->id => $course1);
-
-        // As viewer1 (same group as post).
-        $this->setUser($viewer1->id);
-        $results = array();
-        forum_print_overview($courses, $results);
-        $this->assertDebuggingCalledCount(2);
-
-        // There should be one entry for course1.
-        $this->assertCount(1, $results);
-
-        // There should be one entry for a forum in course1.
-        $this->assertCount(1, $results[$course1->id]);
-        $this->assertArrayHasKey('forum', $results[$course1->id]);
-
-        $this->setUser($viewer2->id);
-        $results = array();
-        forum_print_overview($courses, $results);
-        $this->assertDebuggingCalledCount(2);
-
-        // There should be one entry for course1.
-        $this->assertCount(0, $results);
-    }
-
-    /**
-     * @dataProvider print_overview_timed_provider
-     */
-    public function test_print_overview_timed($config, $hasresult) {
-        $this->resetAfterTest();
-        $course1 = self::getDataGenerator()->create_course();
-
-        // Create an author user.
-        $author = self::getDataGenerator()->create_user();
-        $this->getDataGenerator()->enrol_user($author->id, $course1->id);
-
-        // Create a viewer user.
-        $viewer = self::getDataGenerator()->create_user((object) array('trackforums' => 1));
-        $this->getDataGenerator()->enrol_user($viewer->id, $course1->id);
-
-        // Create a forum.
-        $record = new stdClass();
-        $record->course = $course1->id;
-        $forum1 = self::getDataGenerator()->create_module('forum', (object) array('course' => $course1->id));
-
-        // A timed post with a timestart in the past (24 hours ago).
-        $record = new stdClass();
-        $record->course = $course1->id;
-        $record->userid = $author->id;
-        $record->forum = $forum1->id;
-        if (isset($config['timestartmodifier'])) {
-            $record->timestart = time() + $config['timestartmodifier'];
-        }
-        if (isset($config['timeendmodifier'])) {
-            $record->timeend = time() + $config['timeendmodifier'];
-        }
-        $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
-
-        $course1->lastaccess = 0;
-        $courses = array($course1->id => $course1);
-
-        // As viewer, check the forum_print_overview result.
-        $this->setUser($viewer->id);
-        $results = array();
-        forum_print_overview($courses, $results);
-        $this->assertDebuggingCalledCount(2);
-
-        if ($hasresult) {
-            // There should be one entry for course1.
-            $this->assertCount(1, $results);
-
-            // There should be one entry for a forum in course1.
-            $this->assertCount(1, $results[$course1->id]);
-            $this->assertArrayHasKey('forum', $results[$course1->id]);
-        } else {
-            // There should be no entries for any course.
-            $this->assertCount(0, $results);
-        }
-    }
-
-    /**
-     * @dataProvider print_overview_timed_provider
-     */
-    public function test_print_overview_timed_groups($config, $hasresult) {
-        $this->resetAfterTest();
-        $course1 = self::getDataGenerator()->create_course();
-        $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course1->id));
-        $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course1->id));
-
-        // Create an author user.
-        $author = self::getDataGenerator()->create_user();
-        $this->getDataGenerator()->enrol_user($author->id, $course1->id);
-
-        // Create two viewer users - one in each group.
-        $viewer1 = self::getDataGenerator()->create_user((object) array('trackforums' => 1));
-        $this->getDataGenerator()->enrol_user($viewer1->id, $course1->id);
-        $this->getDataGenerator()->create_group_member(array('userid' => $viewer1->id, 'groupid' => $group1->id));
-
-        $viewer2 = self::getDataGenerator()->create_user((object) array('trackforums' => 1));
-        $this->getDataGenerator()->enrol_user($viewer2->id, $course1->id);
-        $this->getDataGenerator()->create_group_member(array('userid' => $viewer2->id, 'groupid' => $group2->id));
-
-        // Create a forum.
-        $record = new stdClass();
-        $record->course = $course1->id;
-        $forum1 = self::getDataGenerator()->create_module('forum', (object) array(
-            'course'        => $course1->id,
-            'groupmode'     => SEPARATEGROUPS,
-        ));
-
-        // A post in the forum for group1.
-        $record = new stdClass();
-        $record->course     = $course1->id;
-        $record->userid     = $author->id;
-        $record->forum      = $forum1->id;
-        $record->groupid    = $group1->id;
-        if (isset($config['timestartmodifier'])) {
-            $record->timestart = time() + $config['timestartmodifier'];
-        }
-        if (isset($config['timeendmodifier'])) {
-            $record->timeend = time() + $config['timeendmodifier'];
-        }
-        $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
-
-        $course1->lastaccess = 0;
-        $courses = array($course1->id => $course1);
-
-        // As viewer1 (same group as post).
-        $this->setUser($viewer1->id);
-        $results = array();
-        forum_print_overview($courses, $results);
-        $this->assertDebuggingCalledCount(2);
-
-        if ($hasresult) {
-            // There should be one entry for course1.
-            $this->assertCount(1, $results);
-
-            // There should be one entry for a forum in course1.
-            $this->assertCount(1, $results[$course1->id]);
-            $this->assertArrayHasKey('forum', $results[$course1->id]);
-        } else {
-            // There should be no entries for any course.
-            $this->assertCount(0, $results);
-        }
-
-        $this->setUser($viewer2->id);
-        $results = array();
-        forum_print_overview($courses, $results);
-        $this->assertDebuggingCalledCount(2);
-
-        // There should be one entry for course1.
-        $this->assertCount(0, $results);
-    }
-
-    public function print_overview_timed_provider() {
-        return array(
-            'timestart_past' => array(
-                'discussionconfig' => array(
-                    'timestartmodifier' => -86000,
-                ),
-                'hasresult'         => true,
-            ),
-            'timestart_future' => array(
-                'discussionconfig' => array(
-                    'timestartmodifier' => 86000,
-                ),
-                'hasresult'         => false,
-            ),
-            'timeend_past' => array(
-                'discussionconfig' => array(
-                    'timeendmodifier'   => -86000,
-                ),
-                'hasresult'         => false,
-            ),
-            'timeend_future' => array(
-                'discussionconfig' => array(
-                    'timeendmodifier'   => 86000,
-                ),
-                'hasresult'         => true,
-            ),
-        );
-    }
-
     /**
      * Test test_pinned_discussion_with_group.
      */
index 4b1654c..7688acb 100644 (file)
@@ -137,6 +137,7 @@ class mod_forum_mail_testcase extends advanced_testcase {
     }
 
     public function test_forced_subscription() {
+        global $DB;
         $this->resetAfterTest(true);
 
         // Create a course, with a forum.
@@ -145,8 +146,14 @@ class mod_forum_mail_testcase extends advanced_testcase {
         $options = array('course' => $course->id, 'forcesubscribe' => FORUM_FORCESUBSCRIBE);
         $forum = $this->getDataGenerator()->create_module('forum', $options);
 
-        // Create two users enrolled in the course as students.
-        list($author, $recipient) = $this->helper_create_users($course, 2);
+        // Create users enrolled in the course as students.
+        list($author, $recipient, $unconfirmed, $deleted) = $this->helper_create_users($course, 4);
+
+        // Make the third user unconfirmed (thence inactive) to make sure it does not break the notifications.
+        $DB->set_field('user', 'confirmed', 0, ['id' => $unconfirmed->id]);
+
+        // Mark the fourth user as deleted to make sure it does not break the notifications.
+        $DB->set_field('user', 'deleted', 1, ['id' => $deleted->id]);
 
         // Post a discussion to the forum.
         list($discussion, $post) = $this->helper_post_to_forum($forum, $author);
@@ -160,11 +167,21 @@ class mod_forum_mail_testcase extends advanced_testcase {
                 'userid' => $recipient->id,
                 'messages' => 1,
    &nb