Merge branch 'MDL-61451-master' of git://github.com/cescobedo/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 3 Apr 2018 23:39:36 +0000 (01:39 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 3 Apr 2018 23:39:36 +0000 (01:39 +0200)
280 files changed:
.travis.yml
admin/settings/plugins.php
admin/tool/mobile/classes/api.php
admin/tool/mobile/classes/external.php
admin/tool/mobile/tests/externallib_test.php
admin/tool/usertours/classes/manager.php
admin/tool/usertours/classes/step.php
admin/user.php
analytics/classes/local/target/base.php
analytics/tests/course_test.php
auth/classes/digital_consent.php
auth/email/auth.php
backup/moodle2/backup_plan_builder.class.php
backup/moodle2/backup_qtype_extrafields_plugin.class.php [new file with mode: 0644]
backup/moodle2/restore_plan_builder.class.php
backup/moodle2/restore_qtype_extrafields_plugin.class.php [new file with mode: 0644]
backup/util/plan/base_task.class.php
blocks/myprofile/block_myprofile.php
calendar/classes/local/event/data_access/event_vault.php
calendar/externallib.php
calendar/lib.php
calendar/type/gregorian/classes/privacy/provider.php [new file with mode: 0644]
calendar/type/gregorian/lang/en/calendartype_gregorian.php
calendar/view.php
composer.json
composer.lock
course/classes/search/section.php [new file with mode: 0644]
course/tests/behat/course_controls.feature
course/tests/behat/paged_course_navigation.feature
course/tests/search_test.php
enrol/database/lib.php
enrol/database/tests/sync_test.php
files/renderer.php
grade/export/lib.php
group/index.php
install.php
install/lang/af/admin.php
install/lang/de_ch/langconfig.php [new file with mode: 0644]
install/lang/pt/error.php
install/lang/sk/install.php
iplookup/tests/geoip_test.php
lang/en/cache.php
lang/en/message.php
lang/en/question.php
lang/en/search.php
lib/adminlib.php
lib/classes/event/course_module_completion_updated.php
lib/classes/event/message_deleted.php
lib/classes/event/message_sent.php
lib/classes/event/message_viewed.php
lib/classes/event/notification_sent.php [new file with mode: 0644]
lib/classes/event/notification_viewed.php [new file with mode: 0644]
lib/classes/message/manager.php
lib/classes/plugin_manager.php
lib/classes/task/messaging_cleanup_task.php
lib/coursecatlib.php
lib/db/caches.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/deprecatedlib.php
lib/dml/mariadb_native_moodle_database.php
lib/dml/moodle_database.php
lib/dml/mysqli_native_moodle_database.php
lib/dml/pgsql_native_moodle_database.php
lib/dml/sqlsrv_native_moodle_database.php
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-debug.js
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-min.js
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button.js
lib/editor/atto/plugins/table/yui/src/button/js/button.js
lib/editor/tinymce/plugins/ctrlhelp/classes/privacy/provider.php [new file with mode: 0644]
lib/editor/tinymce/plugins/ctrlhelp/lang/en/tinymce_ctrlhelp.php
lib/editor/tinymce/plugins/managefiles/classes/privacy/provider.php [new file with mode: 0644]
lib/editor/tinymce/plugins/managefiles/lang/en/tinymce_managefiles.php
lib/editor/tinymce/plugins/moodleemoticon/classes/privacy/provider.php [new file with mode: 0644]
lib/editor/tinymce/plugins/moodleemoticon/lang/en/tinymce_moodleemoticon.php
lib/editor/tinymce/plugins/moodleimage/classes/privacy/provider.php [new file with mode: 0644]
lib/editor/tinymce/plugins/moodleimage/lang/en/tinymce_moodleimage.php
lib/editor/tinymce/plugins/moodlemedia/classes/privacy/provider.php [new file with mode: 0644]
lib/editor/tinymce/plugins/moodlemedia/lang/en/tinymce_moodlemedia.php
lib/editor/tinymce/plugins/moodlenolink/classes/privacy/provider.php [new file with mode: 0644]
lib/editor/tinymce/plugins/moodlenolink/lang/en/tinymce_moodlenolink.php
lib/editor/tinymce/plugins/pdw/classes/privacy/provider.php [new file with mode: 0644]
lib/editor/tinymce/plugins/pdw/lang/en/tinymce_pdw.php
lib/editor/tinymce/plugins/spellchecker/classes/privacy/provider.php [new file with mode: 0644]
lib/editor/tinymce/plugins/spellchecker/lang/en/tinymce_spellchecker.php
lib/editor/tinymce/plugins/wrap/classes/privacy/provider.php [new file with mode: 0644]
lib/editor/tinymce/plugins/wrap/lang/en/tinymce_wrap.php
lib/externallib.php
lib/messagelib.php
lib/moodlelib.php
lib/phpunit/classes/message_sink.php
lib/phpunit/classes/phpmailer_sink.php
lib/phpunit/classes/util.php
lib/questionlib.php
lib/scssphp/Base/Range.php
lib/scssphp/Block.php
lib/scssphp/Colors.php
lib/scssphp/Compiler.php
lib/scssphp/Compiler/Environment.php
lib/scssphp/Exception/CompilerException.php
lib/scssphp/Exception/ParserException.php
lib/scssphp/Exception/RangeException.php
lib/scssphp/Exception/ServerException.php
lib/scssphp/Formatter.php
lib/scssphp/Formatter/Compact.php
lib/scssphp/Formatter/Compressed.php
lib/scssphp/Formatter/Crunched.php
lib/scssphp/Formatter/Debug.php
lib/scssphp/Formatter/Expanded.php
lib/scssphp/Formatter/Nested.php
lib/scssphp/Formatter/OutputBlock.php
lib/scssphp/Node.php
lib/scssphp/Node/Number.php
lib/scssphp/Parser.php
lib/scssphp/Server.php [deleted file]
lib/scssphp/SourceMap/Base64VLQEncoder.php [new file with mode: 0644]
lib/scssphp/SourceMap/SourceMapGenerator.php [new file with mode: 0644]
lib/scssphp/Type.php
lib/scssphp/Util.php
lib/scssphp/Version.php
lib/scssphp/moodle_readme.txt
lib/testing/generator/data_generator.php
lib/tests/coursecatlib_test.php
lib/tests/message_test.php
lib/tests/messagelib_test.php
lib/tests/moodlelib_test.php
lib/tests/questionlib_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/xmldb/xmldb_structure.php
media/player/videojs/styles.css
message/classes/api.php
message/classes/helper.php
message/classes/search/base_message.php
message/classes/search/message_received.php
message/classes/search/message_sent.php
message/externallib.php
message/index.php
message/lib.php
message/output/airnotifier/classes/privacy/provider.php [new file with mode: 0644]
message/output/airnotifier/lang/en/message_airnotifier.php
message/output/airnotifier/tests/privacy_test.php [new file with mode: 0644]
message/output/popup/amd/build/notification_repository.min.js
message/output/popup/amd/src/notification_repository.js
message/output/popup/classes/api.php
message/output/popup/classes/output/popup_notification.php
message/output/popup/db/install.xml
message/output/popup/externallib.php
message/output/popup/message_output_popup.php
message/output/popup/tests/base.php
message/output/popup/tests/behat/message_popover_unread.feature
message/output/popup/version.php
message/tests/api_test.php
message/tests/events_test.php
message/tests/externallib_test.php
message/tests/messagelib_test.php
message/tests/search_received_test.php
message/tests/search_sent_test.php
message/upgrade.txt
mod/assign/tests/locallib_test.php
mod/book/edit.php
mod/feedback/classes/responses_table.php
mod/feedback/item/multichoicerated/lib.php
mod/feedback/lang/en/feedback.php
mod/folder/download_folder.php
mod/glossary/tests/behat/import_entries.feature
mod/glossary/view.php
mod/lesson/locallib.php
mod/lesson/pagetypes/branchtable.php
mod/lesson/pagetypes/matching.php
mod/lesson/pagetypes/multichoice.php
mod/lesson/pagetypes/numerical.php
mod/lesson/pagetypes/shortanswer.php
mod/lesson/pagetypes/truefalse.php
mod/lesson/tests/behat/lesson_exit_enter_clusters.feature [new file with mode: 0644]
mod/lti/lang/en/lti.php
mod/lti/locallib.php
mod/lti/service/memberships/classes/local/resources/contextmemberships.php [moved from mod/lti/service/memberships/classes/local/resource/contextmemberships.php with 99% similarity]
mod/lti/service/memberships/classes/local/resources/linkmemberships.php [moved from mod/lti/service/memberships/classes/local/resource/linkmemberships.php with 98% similarity]
mod/lti/service/memberships/classes/local/service/memberships.php
mod/lti/service/memberships/db/renamedclasses.php [new file with mode: 0644]
mod/lti/service/profile/classes/local/resources/profile.php [moved from mod/lti/service/profile/classes/local/resource/profile.php with 98% similarity]
mod/lti/service/profile/classes/local/service/profile.php
mod/lti/service/profile/db/renamedclasses.php [new file with mode: 0644]
mod/lti/service/toolproxy/classes/local/resources/toolproxy.php [moved from mod/lti/service/toolproxy/classes/local/resource/toolproxy.php with 98% similarity]
mod/lti/service/toolproxy/classes/local/service/toolproxy.php
mod/lti/service/toolproxy/db/renamedclasses.php [new file with mode: 0644]
mod/lti/service/toolsettings/classes/local/resources/contextsettings.php [moved from mod/lti/service/toolsettings/classes/local/resource/contextsettings.php with 97% similarity]
mod/lti/service/toolsettings/classes/local/resources/linksettings.php [moved from mod/lti/service/toolsettings/classes/local/resource/linksettings.php with 96% similarity]
mod/lti/service/toolsettings/classes/local/resources/systemsettings.php [moved from mod/lti/service/toolsettings/classes/local/resource/systemsettings.php with 99% similarity]
mod/lti/service/toolsettings/classes/local/service/toolsettings.php
mod/lti/service/toolsettings/db/renamedclasses.php [new file with mode: 0644]
mod/quiz/backup/moodle2/backup_quiz_stepslib.php
mod/quiz/backup/moodle2/restore_quiz_stepslib.php
mod/quiz/classes/form/randomquestion_form.php
mod/quiz/lang/en/quiz.php
mod/quiz/locallib.php
mod/quiz/report/overview/db/install.xml
mod/quiz/report/overview/db/upgrade.php
mod/quiz/report/overview/version.php
mod/quiz/tests/fixtures/random_by_tag_quiz.mbz [new file with mode: 0644]
mod/quiz/tests/locallib_test.php
mod/quiz/tests/tags_test.php [new file with mode: 0644]
mod/scorm/lib.php
phpunit.xml.dist
plagiarism/classes/privacy/legacy_polyfill.php [new file with mode: 0644]
plagiarism/classes/privacy/plagiarism_provider.php
plagiarism/tests/privacy_legacy_polyfill_test.php [new file with mode: 0644]
privacy/classes/local/metadata/types/plugintype_link.php
privacy/classes/local/metadata/types/subsystem_link.php
privacy/classes/local/request/moodle_content_writer.php
privacy/classes/manager.php
privacy/classes/tests/request/content_writer.php
privacy/tests/legacy_polyfill_test.php
privacy/tests/manager_test.php
privacy/tests/moodle_content_writer_test.php
question/amd/build/edit_tags.min.js
question/amd/build/repository.min.js
question/amd/src/edit_tags.js
question/amd/src/repository.js
question/behaviour/manualgraded/behaviour.php
question/classes/bank/tags_action_column.php
question/classes/bank/view.php
question/classes/external.php
question/engine/tests/helpers.php
question/format.php
question/format/xml/format.php
question/format/xml/tests/xmlformat_test.php
question/lib.php
question/question.php
question/tests/behat/move_question_categories.feature
question/tests/externallib_test.php
question/tests/generator/lib.php
question/type/calculatedsimple/tests/helper.php
question/type/ddwtos/tests/questiontype_test.php
question/type/essay/backup/moodle2/backup_qtype_essay_plugin.class.php
question/type/essay/db/install.xml
question/type/essay/db/upgrade.php
question/type/essay/edit_essay_form.php
question/type/essay/lang/en/qtype_essay.php
question/type/essay/question.php
question/type/essay/questiontype.php
question/type/essay/renderer.php
question/type/essay/tests/behat/file_type_restriction.feature [new file with mode: 0644]
question/type/essay/tests/fixtures/testquestion.moodle.xml
question/type/essay/tests/helper.php
question/type/essay/tests/walkthrough_test.php
question/type/essay/version.php
question/type/gapselect/tests/edit_form_test.php
question/type/gapselect/tests/helper.php
question/type/gapselect/tests/questiontype_test.php
question/type/gapselect/tests/walkthrough_test.php
question/type/questionbase.php
question/type/shortanswer/backup/moodle2/backup_qtype_shortanswer_plugin.class.php
question/type/shortanswer/backup/moodle2/restore_qtype_shortanswer_plugin.class.php
question/type/tags_form.php
question/type/upgrade.txt
report/security/locallib.php
repository/filepicker.js
search/classes/engine.php
search/classes/manager.php
search/classes/output/form/search.php
search/engine/simpledb/classes/engine.php [new file with mode: 0644]
search/engine/simpledb/db/install.php [new file with mode: 0644]
search/engine/simpledb/db/install.xml [new file with mode: 0644]
search/engine/simpledb/db/uninstall.php [new file with mode: 0644]
search/engine/simpledb/lang/en/search_simpledb.php [new file with mode: 0644]
search/engine/simpledb/tests/engine_test.php [new file with mode: 0644]
search/engine/simpledb/version.php [moved from message/output/popup/db/events.php with 68% similarity]
search/engine/solr/classes/engine.php
search/engine/solr/lang/en/search_solr.php
search/engine/solr/tests/engine_test.php
search/index.php
search/tests/engine_test.php
search/upgrade.txt
theme/boost/templates/core/filemanager_modal_generallayout.mustache
user/classes/participants_table.php
version.php
webservice/lib.php

index 9e436f5..a1217e8 100644 (file)
@@ -248,7 +248,7 @@ script:
     - >
       if [ "$TASK" = 'PHPUNIT' ];
       then
-        vendor/bin/phpunit;
+        vendor/bin/phpunit --fail-on-risky --disallow-test-output --verbose;
       fi
 
     - >
index c55b1f1..9a2202f 100644 (file)
@@ -556,7 +556,7 @@ if ($hassiteconfig) {
     // Search engine selection.
     $temp->add(new admin_setting_heading('searchengineheading', new lang_string('searchengine', 'admin'), ''));
     $temp->add(new admin_setting_configselect('searchengine',
-                                new lang_string('selectsearchengine', 'admin'), '', 'solr', $engines));
+                                new lang_string('selectsearchengine', 'admin'), '', 'simpledb', $engines));
     $temp->add(new admin_setting_heading('searchoptionsheading', new lang_string('searchoptions', 'admin'), ''));
     $temp->add(new admin_setting_configcheckbox('searchindexwhendisabled',
             new lang_string('searchindexwhendisabled', 'admin'), new lang_string('searchindexwhendisabled_desc', 'admin'),
index 4841569..160c4f0 100644 (file)
@@ -151,6 +151,11 @@ class api {
             'tool_mobile_disabledfeatures' => get_config('tool_mobile', 'disabledfeatures'),
             'country' => clean_param($CFG->country, PARAM_NOTAGS),
             'agedigitalconsentverification' => \core_auth\digital_consent::is_age_digital_consent_verification_enabled(),
+            'autolang' => $CFG->autolang,
+            'lang' => clean_param($CFG->lang, PARAM_LANG),  // Avoid breaking WS because of incorrect package langs.
+            'langmenu' => $CFG->langmenu,
+            'langlist' => $CFG->langlist,
+            'locale' => $CFG->locale,
         );
 
         $typeoflogin = get_config('tool_mobile', 'typeoflogin');
index 65a5ccf..7df769b 100644 (file)
@@ -168,6 +168,12 @@ class external extends external_api {
                     (only if age verification is enabled).', VALUE_OPTIONAL),
                 'supportemail' => new external_value(PARAM_EMAIL, 'Site support contact email
                     (only if age verification is enabled).', VALUE_OPTIONAL),
+                'autolang' => new external_value(PARAM_INT, 'Whether to detect default language
+                    from browser setting.', VALUE_OPTIONAL),
+                'lang' => new external_value(PARAM_LANG, 'Default language for the site.', VALUE_OPTIONAL),
+                'langmenu' => new external_value(PARAM_INT, 'Whether the language menu should be displayed.', VALUE_OPTIONAL),
+                'langlist' => new external_value(PARAM_RAW, 'Languages on language menu.', VALUE_OPTIONAL),
+                'locale' => new external_value(PARAM_RAW, 'Sitewide locale.', VALUE_OPTIONAL),
                 'warnings' => new external_warnings(),
             )
         );
index 19483ee..e61657c 100644 (file)
@@ -88,6 +88,11 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
             'launchurl' => "$CFG->wwwroot/$CFG->admin/tool/mobile/launch.php",
             'country' => $CFG->country,
             'agedigitalconsentverification' => \core_auth\digital_consent::is_age_digital_consent_verification_enabled(),
+            'autolang' => $CFG->autolang,
+            'lang' => $CFG->lang,
+            'langmenu' => $CFG->langmenu,
+            'langlist' => $CFG->langlist,
+            'locale' => $CFG->locale,
             'warnings' => array()
         );
         $this->assertEquals($expected, $result);
@@ -101,6 +106,8 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
         set_config('logocompact', 'mock.png', 'core_admin');
         set_config('forgottenpasswordurl', 'mailto:fake@email.zy'); // Test old hack.
         set_config('agedigitalconsentverification', 1);
+        set_config('autolang', 1);
+        set_config('lang', 'a_b');  // Set invalid lang.
 
         list($authinstructions, $notusedformat) = external_format_text($authinstructions, FORMAT_MOODLE, $context->id);
         $expected['registerauth'] = 'email';
@@ -110,6 +117,8 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
         $expected['agedigitalconsentverification'] = true;
         $expected['supportname'] = $CFG->supportname;
         $expected['supportemail'] = $CFG->supportemail;
+        $expected['autolang'] = '1';
+        $expected['lang'] = ''; // Expect empty because it was set to an invalid lang.
 
         if ($logourl = $OUTPUT->get_logo_url()) {
             $expected['logourl'] = $logourl->out(false);
index b481d2b..15f142c 100644 (file)
@@ -819,7 +819,7 @@ class manager {
         $existingtourrecords->close();
 
         foreach ($shippedtours as $filename => $version) {
-            $filepath = $CFG->dirroot . '/admin/tool/usertours/tours/' . $filename;
+            $filepath = $CFG->dirroot . "/{$CFG->admin}/tool/usertours/tours/" . $filename;
             $tourjson = file_get_contents($filepath);
             $tour = self::import_tour_from_json($tourjson);
 
index 6049e8f..4819ac0 100644 (file)
@@ -483,10 +483,9 @@ class step {
             $record = $this->to_record();
             unset($record->id);
             $this->id = $DB->insert_record('tool_usertours_steps', $record);
+            $this->get_tour()->reset_step_sortorder();
         }
 
-        $this->get_tour()->reset_step_sortorder();
-
         $this->reload();
 
         // Notify of a change to the step configuration.
index ac0f5f9..0caf98f 100644 (file)
 
     // Carry on with the user listing
     $context = context_system::instance();
-    $extracolumns = get_extra_user_fields($context);
+    // These columns are always shown in the users list.
+    $requiredcolumns = array('city', 'country', 'lastaccess');
+    // Extra columns containing the extra user fields, excluding the required columns (city and country, to be specific).
+    $extracolumns = get_extra_user_fields($context, $requiredcolumns);
     // Get all user name fields as an array.
     $allusernamefields = get_all_user_name_fields(false, null, null, null, true);
-    $columns = array_merge($allusernamefields, $extracolumns, array('city', 'country', 'lastaccess'));
+    $columns = array_merge($allusernamefields, $extracolumns, $requiredcolumns);
 
     foreach ($columns as $column) {
         $string[$column] = get_user_field_name($column);
 
     } else {
 
-        $countries = get_string_manager()->get_list_of_countries(false);
+        $countries = get_string_manager()->get_list_of_countries(true);
         if (empty($mnethosts)) {
             $mnethosts = $DB->get_records('mnet_host', null, 'id', 'id,wwwroot,name');
         }
index 3e84120..638b18b 100644 (file)
@@ -205,10 +205,10 @@ abstract class base extends \core_analytics\calculable {
                 $message->contexturlname = $message->subject;
                 $message->courseid = $coursecontext->instanceid;
 
-                $message->fullmessage = get_string('insightinfomessage', 'analytics', $insighturl->out());
+                $message->fullmessage = get_string('insightinfomessage', 'analytics', $insighturl->out(false));
                 $message->fullmessageformat = FORMAT_PLAIN;
                 $message->fullmessagehtml = get_string('insightinfomessagehtml', 'analytics', $insighturl->out());
-                $message->smallmessage = get_string('insightinfomessage', 'analytics', $insighturl->out());
+                $message->smallmessage = get_string('insightinfomessage', 'analytics', $insighturl->out(false));
                 $message->contexturl = $insighturl->out(false);
 
                 message_send($message);
index 0383c08..00cfea3 100644 (file)
@@ -36,7 +36,7 @@ class core_analytics_course_testcase extends advanced_testcase {
     public function setUp() {
         global $DB;
 
-        $this->course = $this->getDataGenerator()->create_course();
+        $this->course = $this->getDataGenerator()->create_course(['startdate' => 0]);
         $this->stu1 = $this->getDataGenerator()->create_user();
         $this->stu2 = $this->getDataGenerator()->create_user();
         $this->both = $this->getDataGenerator()->create_user();
index e3e7e59..3f7e3af 100644 (file)
@@ -71,7 +71,7 @@ class digital_consent {
     public static function parse_age_digital_consent_map($ageconsentmap) {
 
         $ageconsentmapparsed = array();
-        $countries = get_string_manager()->get_list_of_countries();
+        $countries = get_string_manager()->get_list_of_countries(true);
         $isdefaultvaluepresent = false;
         $lines = preg_split('/\r|\n/', $ageconsentmap, -1, PREG_SPLIT_NO_EMPTY);
         foreach ($lines as $line) {
index f1cbc11..f207f03 100644 (file)
@@ -113,7 +113,7 @@ class auth_plugin_email extends auth_plugin_base {
      * @since Moodle 3.2
      */
     public function user_signup_with_confirmation($user, $notify=true, $confirmationurl = null) {
-        global $CFG, $DB;
+        global $CFG, $DB, $SESSION;
         require_once($CFG->dirroot.'/user/profile/lib.php');
         require_once($CFG->dirroot.'/user/lib.php');
 
@@ -130,6 +130,11 @@ class auth_plugin_email extends auth_plugin_base {
         // Save any custom profile field information.
         profile_save_data($user);
 
+        // Save wantsurl against user's profile, so we can return them there upon confirmation.
+        if (!empty($SESSION->wantsurl)) {
+            set_user_preference('auth_email_wantsurl', $SESSION->wantsurl, $user);
+        }
+
         // Trigger event.
         \core\event\user_created::create_from_userid($user->id)->trigger();
 
@@ -166,7 +171,7 @@ class auth_plugin_email extends auth_plugin_base {
      * @param string $confirmsecret
      */
     function user_confirm($username, $confirmsecret) {
-        global $DB;
+        global $DB, $SESSION;
         $user = get_complete_user_data('username', $username);
 
         if (!empty($user)) {
@@ -178,6 +183,13 @@ class auth_plugin_email extends auth_plugin_base {
 
             } else if ($user->secret == $confirmsecret) {   // They have provided the secret key to get in
                 $DB->set_field("user", "confirmed", 1, array("id"=>$user->id));
+
+                if ($wantsurl = get_user_preferences('auth_email_wantsurl', false, $user)) {
+                    // Ensure user gets returned to page they were trying to access before signing up.
+                    $SESSION->wantsurl = $wantsurl;
+                    unset_user_preference('auth_email_wantsurl', $user);
+                }
+
                 return AUTH_CONFIRM_OK;
             }
         } else {
index c359096..c3c200a 100644 (file)
@@ -37,6 +37,7 @@ require_once($CFG->dirroot . '/backup/moodle2/backup_default_block_task.class.ph
 require_once($CFG->dirroot . '/backup/moodle2/backup_xml_transformer.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_qtype_plugin.class.php');
+require_once($CFG->dirroot . '/backup/moodle2/backup_qtype_extrafields_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_gradingform_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_format_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_local_plugin.class.php');
diff --git a/backup/moodle2/backup_qtype_extrafields_plugin.class.php b/backup/moodle2/backup_qtype_extrafields_plugin.class.php
new file mode 100644 (file)
index 0000000..d7db6ff
--- /dev/null
@@ -0,0 +1,83 @@
+<?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/>.
+
+/**
+ * Defines backup_qtype_extrafields_plugin class
+ *
+ * @package    core_backup
+ * @copyright  2012 Oleg Sychev, Volgograd State Technical University
+ * @author     Valeriy Streltsov <vostreltsov@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/question/engine/bank.php');
+
+/**
+ * Class extending backup_qtype_plugin in order to use extra fields method
+ *
+ * See qtype_shortanswer for an example
+ *
+ * @copyright  2012 Oleg Sychev, Volgograd State Technical University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class backup_qtype_extrafields_plugin extends backup_qtype_plugin {
+
+    /**
+     * Returns the qtype information to attach to question element.
+     */
+    protected function define_question_plugin_structure() {
+        $qtypeobj = question_bank::get_qtype($this->pluginname);
+
+        // Define the virtual plugin element with the condition to fulfill.
+        $plugin = $this->get_plugin_element(null, '../../qtype', $qtypeobj->name());
+
+        // Create one standard named plugin element (the visible container).
+        $pluginwrapper = new backup_nested_element($this->get_recommended_name());
+
+        // Connect the visible container ASAP.
+        $plugin->add_child($pluginwrapper);
+
+        // This qtype uses standard question_answers, add them here
+        // to the tree before any other information that will use them.
+        $this->add_question_question_answers($pluginwrapper);
+        $answers = $pluginwrapper->get_child('answers');
+        $answer = $answers->get_child('answer');
+
+        // Extra question fields.
+        $extraquestionfields = $qtypeobj->extra_question_fields();
+        if (!empty($extraquestionfields)) {
+            $tablename = array_shift($extraquestionfields);
+            $child = new backup_nested_element($qtypeobj->name(), array('id'), $extraquestionfields);
+            $pluginwrapper->add_child($child);
+            $child->set_source_table($tablename, array($qtypeobj->questionid_column_name() => backup::VAR_PARENTID));
+        }
+
+        // Extra answer fields.
+        $extraanswerfields = $qtypeobj->extra_answer_fields();
+        if (!empty($extraanswerfields)) {
+            $tablename = array_shift($extraanswerfields);
+            $child = new backup_nested_element('extraanswerdata', array('id'), $extraanswerfields);
+            $answer->add_child($child);
+            $child->set_source_table($tablename, array('answerid' => backup::VAR_PARENTID));
+        }
+
+        // Don't need to annotate ids nor files.
+        return $plugin;
+    }
+}
index ae48ec9..02dc062 100644 (file)
@@ -36,6 +36,7 @@ require_once($CFG->dirroot . '/backup/moodle2/restore_block_task.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/restore_default_block_task.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/restore_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/restore_qtype_plugin.class.php');
+require_once($CFG->dirroot . '/backup/moodle2/restore_qtype_extrafields_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/restore_format_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/restore_local_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/restore_theme_plugin.class.php');
@@ -46,6 +47,7 @@ require_once($CFG->dirroot . '/backup/moodle2/restore_gradingform_plugin.class.p
 require_once($CFG->dirroot . '/backup/moodle2/restore_enrol_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_qtype_plugin.class.php');
+require_once($CFG->dirroot . '/backup/moodle2/backup_qtype_extrafields_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_format_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_local_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_theme_plugin.class.php');
diff --git a/backup/moodle2/restore_qtype_extrafields_plugin.class.php b/backup/moodle2/restore_qtype_extrafields_plugin.class.php
new file mode 100644 (file)
index 0000000..e976f38
--- /dev/null
@@ -0,0 +1,131 @@
+<?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/>.
+
+/**
+ * Defines restore_qtype_extrafields_plugin class
+ *
+ * @package    core_backup
+ * @copyright  2012 Oleg Sychev, Volgograd State Technical University
+ * @author     Valeriy Streltsov <vostreltsov@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/question/engine/bank.php');
+
+/**
+ * Class extending restore_qtype_plugin in order to use extra fields method
+ *
+ * See qtype_shortanswer for an example
+ *
+ * @copyright  2012 Oleg Sychev, Volgograd State Technical University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class restore_qtype_extrafields_plugin extends restore_qtype_plugin {
+
+    /**
+     * Question type class for a particular question type
+     * @var question_type
+     */
+    protected $qtypeobj;
+
+    /**
+     * Constructor
+     *
+     * @param string $plugintype plugin type
+     * @param string $pluginname plugin name
+     * @param restore_step $step step
+     */
+    public function __construct($plugintype, $pluginname, $step) {
+        parent::__construct($plugintype, $pluginname, $step);
+        $this->qtypeobj = question_bank::get_qtype($this->pluginname);
+    }
+
+    /**
+     * Returns the paths to be handled by the plugin at question level.
+     */
+    protected function define_question_plugin_structure() {
+        $paths = array();
+
+        // This qtype uses question_answers, add them.
+        $this->add_question_question_answers($paths);
+
+        // Add own qtype stuff.
+        $elepath = $this->get_pathfor('/' . $this->qtypeobj->name());
+        $paths[] = new restore_path_element($this->qtypeobj->name(), $elepath);
+
+        $elepath = $this->get_pathfor('/answers/answer/extraanswerdata');
+        $paths[] = new restore_path_element('extraanswerdata', $elepath);
+
+        return $paths;
+    }
+
+    /**
+     * Processes the extra answer data
+     *
+     * @param array $data extra answer data
+     */
+    public function process_extraanswerdata($data) {
+        global $DB;
+
+        $extra = $this->qtypeobj->extra_answer_fields();
+        $tablename = array_shift($extra);
+
+        $oldquestionid = $this->get_old_parentid('question');
+        $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
+
+        if ($questioncreated) {
+            $data['answerid'] = $this->get_mappingid('question_answer', $data['id']);
+            $DB->insert_record($tablename, $data);
+        } else {
+            $DB->update_record($tablename, $data);
+        }
+    }
+
+    /**
+     * Process the qtype/... element.
+     *
+     * @param array $data question data
+     */
+    public function really_process_extra_question_fields($data) {
+        global $DB;
+
+        $oldid = $data['id'];
+
+        // Detect if the question is created or mapped.
+        $oldquestionid = $this->get_old_parentid('question');
+        $newquestionid = $this->get_new_parentid('question');
+        $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
+
+        // If the question has been created by restore, we need to create its qtype_... too.
+        if ($questioncreated) {
+            $extraquestionfields = $this->qtypeobj->extra_question_fields();
+            $tablename = array_shift($extraquestionfields);
+
+            // Adjust some columns.
+            $qtfield = $this->qtypeobj->questionid_column_name();
+            $data[$qtfield] = $newquestionid;
+
+            // Insert record.
+            $newitemid = $DB->insert_record($tablename, $data);
+
+            // Create mapping.
+            $this->set_mapping($tablename, $oldid, $newitemid);
+        }
+    }
+}
index 19cb072..ce30c06 100644 (file)
@@ -209,7 +209,7 @@ abstract class base_task implements checksumable, executable, loggable {
         }
         // Everything has been destroyed recursively, now we can reset safely
         $this->steps = array();
-        $this->setting = array();
+        $this->settings = array();
         $this->plan = null;
     }
 
index fa9c166..b596f15 100644 (file)
@@ -75,7 +75,7 @@ class block_myprofile extends block_base {
         $this->content->text .= '<div class="myprofileitem fullname">'.fullname($USER).'</div>';
 
         if(!isset($this->config->display_country) || $this->config->display_country == 1) {
-            $countries = get_string_manager()->get_list_of_countries();
+            $countries = get_string_manager()->get_list_of_countries(true);
             if (isset($countries[$USER->country])) {
                 $this->content->text .= '<div class="myprofileitem country">';
                 $this->content->text .= get_string('country') . ': ' . $countries[$USER->country];
index 246cd9f..6c42d3c 100644 (file)
@@ -201,10 +201,6 @@ class event_vault implements event_vault_interface {
         event_interface $afterevent = null,
         $limitnum = 20
     ) {
-        $categoryids = array_map(function($category) {
-            return $category->id;
-        }, \coursecat::get_all());
-
         $courseids = array_map(function($course) {
             return $course->id;
         }, enrol_get_all_users_courses($user->id));
@@ -227,7 +223,7 @@ class event_vault implements event_vault_interface {
             [$user->id],
             $groupids ? $groupids : null,
             $courseids ? $courseids : null,
-            $categoryids ? $categoryids : null,
+            null, // All categories.
             true,
             true,
             function ($event) {
index e644e39..10fa760 100644 (file)
@@ -181,11 +181,17 @@ class core_calendar_external extends external_api {
         $funcparam = array('courses' => array(), 'groups' => array(), 'categories' => array());
         $hassystemcap = has_capability('moodle/calendar:manageentries', context_system::instance());
         $warnings = array();
+        $coursecategories = array();
 
-        // Let us findout courses that we can return events from.
+        // Let us find out courses and their categories that we can return events from.
         if (!$hassystemcap) {
-            $courses = enrol_get_my_courses('id');
-            $courses = array_keys($courses);
+            $courseobjs = enrol_get_my_courses();
+            $courses = array_keys($courseobjs);
+
+            $coursecategories = array_flip(array_map(function($course) {
+                return $course->category;
+            }, $courseobjs));
+
             foreach ($params['events']['courseids'] as $id) {
                try {
                     $context = context_course::instance($id);
@@ -203,6 +209,14 @@ class core_calendar_external extends external_api {
         } else {
             $courses = $params['events']['courseids'];
             $funcparam['courses'] = $courses;
+
+            if (!empty($courses)) {
+                list($wheresql, $sqlparams) = $DB->get_in_or_equal($courses);
+                $wheresql = "id $wheresql";
+                $coursecategories = array_flip(array_map(function($course) {
+                    return $course->category;
+                }, $DB->get_records_select('course', $wheresql, $sqlparams, '', 'category')));
+            }
         }
 
         // Let us findout groups that we can return events from.
@@ -223,56 +237,55 @@ class core_calendar_external extends external_api {
 
         $categories = array();
         if ($hassystemcap || !empty($courses)) {
-
-            $coursecategories = array();
-            if (!empty($courses)) {
-                list($wheresql, $sqlparams) = $DB->get_in_or_equal($courses);
-                $wheresql = "id $wheresql";
-                $courseswithcategory = $DB->get_records_select('course', $wheresql, $sqlparams);
-
-                // Grab the list of course categories for the requested course list.
-                foreach ($courseswithcategory as $course) {
-                    if (empty($course->visible)) {
-                        if (!has_capability('moodle/course:viewhidden', context_course::instance($course->id))) {
-                            continue;
+            // Use the category id as the key in the following array. That way we do not have to remove duplicates and
+            // have a faster lookup later.
+            $categories = [];
+
+            if (!empty($params['events']['categoryids'])) {
+                $catobjs = \coursecat::get_many(array_merge($params['events']['categoryids'], array_keys($coursecategories)));
+                foreach ($catobjs as $catobj) {
+                    if (isset($coursecategories[$catobj->id]) ||
+                            has_capability('moodle/category:manage', $catobj->get_context())) {
+                        // If the user has access to a course in this category or can manage the category,
+                        // then they can see all parent categories too.
+                        $categories[$catobj->id] = true;
+                        foreach ($catobj->get_parents() as $catid) {
+                            $categories[$catid] = true;
                         }
                     }
-                    $category = \coursecat::get($course->category);
-                    // Fetch parent categories.
-                    $coursecategories = array_merge($coursecategories, [$category->id], $category->get_parents());
                 }
-            }
-
-            foreach (\coursecat::get_all() as $category) {
-                // Skip categories not requested.
-                if (!empty($params['events']['categoryids'])) {
-                    if (!in_array($category->id, $params['events']['categoryids'])) {
-                        continue;
-                    }
-                }
-
-                if (has_capability('moodle/category:manage', $category->get_context())) {
-                    // If a user can manage a category, then they can see all child categories. as well as all parent categories.
-                    $categories[] = $category->id;
-
-                    foreach (\coursecat::get_all() as $cat) {
-                        if (array_search($category->id, $cat->get_parents()) !== false) {
-                            $categories[] = $cat->id;
+                $funcparam['categories'] = array_keys($categories);
+            } else {
+                // Fetch all categories where this user has any enrolment, and all categories that this user can manage.
+                $calcatcache = cache::make('core', 'calendar_categories');
+                // Do not use cache if the user has the system capability as $coursecategories might not represent the
+                // courses the user is enrolled in.
+                $categories = (!$hassystemcap) ? $calcatcache->get('site') : false;
+                if ($categories !== false) {
+                    // The ids are stored in a list in the cache.
+                    $funcparam['categories'] = $categories;
+                    $categories = array_flip($categories);
+                } else {
+                    $categories = [];
+                    foreach (\coursecat::get_all() as $category) {
+                        if (isset($coursecategories[$category->id]) ||
+                                has_capability('moodle/category:manage', $category->get_context(), $USER, false)) {
+                            // If the user has access to a course in this category or can manage the category,
+                            // then they can see all parent categories too.
+                            $categories[$category->id] = true;
+                            foreach ($category->get_parents() as $catid) {
+                                $categories[$catid] = true;
+                            }
                         }
                     }
-                    $categories = array_merge($categories, $category->get_parents());
-                } else if (in_array($category->id, $coursecategories)) {
-
-                    // The user has access to a course in this category.
-                    // Fetch all of the parents too.
-                    $categories = array_merge($categories, [$category->id], $category->get_parents());
-                    $categories[] = $category->id;
+                    $funcparam['categories'] = array_keys($categories);
+                    if (!$hassystemcap) {
+                        $calcatcache->set('site', $funcparam['categories']);
+                    }
                 }
             }
         }
 
-        $funcparam['categories'] = array_unique($categories);
-
         // Do we need user events?
         if (!empty($params['options']['userevents'])) {
             $funcparam['users'] = array($USER->id);
@@ -327,7 +340,7 @@ class core_calendar_external extends external_api {
                 // Can the user actually see this event?
                 $eventobj = calendar_event::load($eventobj);
                 if ((($eventobj->courseid == $SITE->id) && (empty($eventobj->categoryid))) ||
-                            (!empty($eventobj->categoryid) && in_array($eventobj->categoryid, $categories)) ||
+                            (!empty($eventobj->categoryid) && isset($categories[$eventobj->categoryid])) ||
                             (!empty($eventobj->groupid) && in_array($eventobj->groupid, $groups)) ||
                             (!empty($eventobj->courseid) && in_array($eventobj->courseid, $courses)) ||
                             ($USER->id == $eventobj->userid) ||
index 0879883..69ad4a7 100644 (file)
@@ -1132,7 +1132,7 @@ class calendar_information {
      * course will be used.
      *
      * @param   stdClass    $course The current course being viewed.
-     * @param   int[]       $courses The list of all courses currently accessible.
+     * @param   stdClass[]  $courses The list of all courses currently accessible.
      * @param   stdClass    $category The current category to show.
      */
     public function set_sources(stdClass $course, array $courses, stdClass $category = null) {
@@ -1171,17 +1171,9 @@ class calendar_information {
 
             // Build the category list.
             // This includes the current category.
-            $this->categories = [$category->id];
-
-            // All of its descendants.
-            foreach (\coursecat::get_all() as $cat) {
-                if (array_search($category->id, $cat->get_parents()) !== false) {
-                    $this->categories[] = $cat->id;
-                }
-            }
-
-            // And all of its parents.
-            $this->categories = array_merge($this->categories, $category->get_parents());
+            $this->categories = $category->get_parents();
+            $this->categories[] = $category->id;
+            $this->categories = array_merge($this->categories, $category->get_all_children_ids());
         } else if (SITEID === $this->courseid) {
             // The site was requested.
             // Fetch all categories where this user has any enrolment, and all categories that this user can manage.
@@ -1191,26 +1183,25 @@ class calendar_information {
                 return $course->category;
             }, $courses));
 
-            $categories = [];
-            foreach (\coursecat::get_all() as $category) {
-                if (has_capability('moodle/category:manage', $category->get_context(), $USER, false)) {
-                    // If a user can manage a category, then they can see all child categories. as well as all parent categories.
-                    $categories[] = $category->id;
-                    foreach (\coursecat::get_all() as $cat) {
-                        if (array_search($category->id, $cat->get_parents()) !== false) {
-                            $categories[] = $cat->id;
+            $calcatcache = cache::make('core', 'calendar_categories');
+            $this->categories = $calcatcache->get('site');
+            if ($this->categories === false) {
+                // Use the category id as the key in the following array. That way we do not have to remove duplicates.
+                $categories = [];
+                foreach (\coursecat::get_all() as $category) {
+                    if (isset($coursecategories[$category->id]) ||
+                            has_capability('moodle/category:manage', $category->get_context(), $USER, false)) {
+                        // If the user has access to a course in this category or can manage the category,
+                        // then they can see all parent categories too.
+                        $categories[$category->id] = true;
+                        foreach ($category->get_parents() as $catid) {
+                            $categories[$catid] = true;
                         }
                     }
-                    $categories = array_merge($categories, $category->get_parents());
-                } else if (isset($coursecategories[$category->id])) {
-                    // The user has access to a course in this category.
-                    // Fetch all of the parents too.
-                    $categories = array_merge($categories, [$category->id], $category->get_parents());
-                    $categories[] = $category->id;
                 }
+                $this->categories = array_keys($categories);
+                $calcatcache->set('site', $this->categories);
             }
-
-            $this->categories = array_unique($categories);
         }
     }
 
diff --git a/calendar/type/gregorian/classes/privacy/provider.php b/calendar/type/gregorian/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..8f7f017
--- /dev/null
@@ -0,0 +1,46 @@
+<?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/>.
+
+/**
+ * Privacy Subsystem implementation for block_activity_modules.
+ *
+ * @package    calendartype_gregorian
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace calendartype_gregorian\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for calendartype_gregorian implementing null_provider.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
index 9d2c5ba..8471cf8 100644 (file)
@@ -24,3 +24,4 @@
 
 $string['name'] = 'Gregorian';
 $string['pluginname'] = 'Gregorian calendar type';
+$string['privacy:metadata'] = 'The Gregorian Calendar format plugin does not store any personal data.';
index b7fba4d..df616e1 100644 (file)
@@ -132,4 +132,5 @@ echo html_writer::end_tag('div');
 list($data, $template) = calendar_get_footer_options($calendar);
 echo $renderer->render_from_template($template, $data);
 
+echo $renderer->complete_layout();
 echo $OUTPUT->footer();
index 70c46b7..3c6ddd6 100644 (file)
@@ -5,7 +5,7 @@
     "type": "project",
     "homepage": "https://moodle.org",
     "require-dev": {
-        "phpunit/phpunit": "6.4.*",
+        "phpunit/phpunit": "6.5.*",
         "phpunit/dbUnit": "3.0.*",
         "moodlehq/behat-extension": "3.35.1",
         "mikey179/vfsStream": "^1.6"
index b9fe34a..c79a846 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "content-hash": "93454a669db9cfbc99f35f7bacd6ac82",
+    "content-hash": "f047772340a956bbf2c02d91536b11c6",
     "packages": [],
     "packages-dev": [
         {
         },
         {
             "name": "behat/mink-extension",
-            "version": "2.3.0",
+            "version": "2.3.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Behat/MinkExtension.git",
-                "reference": "badc565b7a1d05c4a4bf49c789045bcf7af6c6de"
+                "reference": "80f7849ba53867181b7e412df9210e12fba50177"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Behat/MinkExtension/zipball/badc565b7a1d05c4a4bf49c789045bcf7af6c6de",
-                "reference": "badc565b7a1d05c4a4bf49c789045bcf7af6c6de",
+                "url": "https://api.github.com/repos/Behat/MinkExtension/zipball/80f7849ba53867181b7e412df9210e12fba50177",
+                "reference": "80f7849ba53867181b7e412df9210e12fba50177",
                 "shasum": ""
             },
             "require": {
                 "test",
                 "web"
             ],
-            "time": "2017-11-24T19:30:49+00:00"
+            "time": "2018-02-06T15:36:30+00:00"
         },
         {
             "name": "behat/mink-goutte-driver",
         },
         {
             "name": "phpdocumentor/reflection-docblock",
-            "version": "4.2.0",
+            "version": "4.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
-                "reference": "66465776cfc249844bde6d117abff1d22e06c2da"
+                "reference": "94fd0001232e47129dd3504189fa1c7225010d08"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/66465776cfc249844bde6d117abff1d22e06c2da",
-                "reference": "66465776cfc249844bde6d117abff1d22e06c2da",
+                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08",
+                "reference": "94fd0001232e47129dd3504189fa1c7225010d08",
                 "shasum": ""
             },
             "require": {
                 }
             ],
             "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
-            "time": "2017-11-27T17:38:31+00:00"
+            "time": "2017-11-30T07:14:17+00:00"
         },
         {
             "name": "phpdocumentor/type-resolver",
         },
         {
             "name": "phpspec/prophecy",
-            "version": "1.7.3",
+            "version": "1.7.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpspec/prophecy.git",
-                "reference": "e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf"
+                "reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf",
-                "reference": "e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf",
+                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/dfd6be44111a7c41c2e884a336cc4f461b3b2401",
+                "reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401",
                 "shasum": ""
             },
             "require": {
             },
             "require-dev": {
                 "phpspec/phpspec": "^2.5|^3.2",
-                "phpunit/phpunit": "^4.8.35 || ^5.7"
+                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5"
             },
             "type": "library",
             "extra": {
                 "spy",
                 "stub"
             ],
-            "time": "2017-11-24T13:59:53+00:00"
+            "time": "2018-02-19T10:16:54+00:00"
         },
         {
             "name": "phpunit/dbunit",
         },
         {
             "name": "phpunit/phpunit",
-            "version": "6.4.4",
+            "version": "6.5.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "562f7dc75d46510a4ed5d16189ae57fbe45a9932"
+                "reference": "6bd77b57707c236833d2b57b968e403df060c9d9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/562f7dc75d46510a4ed5d16189ae57fbe45a9932",
-                "reference": "562f7dc75d46510a4ed5d16189ae57fbe45a9932",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6bd77b57707c236833d2b57b968e403df060c9d9",
+                "reference": "6bd77b57707c236833d2b57b968e403df060c9d9",
                 "shasum": ""
             },
             "require": {
                 "phar-io/version": "^1.0",
                 "php": "^7.0",
                 "phpspec/prophecy": "^1.7",
-                "phpunit/php-code-coverage": "^5.2.2",
-                "phpunit/php-file-iterator": "^1.4.2",
+                "phpunit/php-code-coverage": "^5.3",
+                "phpunit/php-file-iterator": "^1.4.3",
                 "phpunit/php-text-template": "^1.2.1",
                 "phpunit/php-timer": "^1.0.9",
-                "phpunit/phpunit-mock-objects": "^4.0.3",
-                "sebastian/comparator": "^2.0.2",
+                "phpunit/phpunit-mock-objects": "^5.0.5",
+                "sebastian/comparator": "^2.1",
                 "sebastian/diff": "^2.0",
                 "sebastian/environment": "^3.1",
                 "sebastian/exporter": "^3.1",
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "6.4.x-dev"
+                    "dev-master": "6.5.x-dev"
                 }
             },
             "autoload": {
                 "testing",
                 "xunit"
             ],
-            "time": "2017-11-08T11:26:09+00:00"
+            "time": "2018-02-26T07:01:09+00:00"
         },
         {
             "name": "phpunit/phpunit-mock-objects",
-            "version": "4.0.4",
+            "version": "5.0.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
-                "reference": "2f789b59ab89669015ad984afa350c4ec577ade0"
+                "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/2f789b59ab89669015ad984afa350c4ec577ade0",
-                "reference": "2f789b59ab89669015ad984afa350c4ec577ade0",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/33fd41a76e746b8fa96d00b49a23dadfa8334cdf",
+                "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf",
                 "shasum": ""
             },
             "require": {
                 "doctrine/instantiator": "^1.0.5",
                 "php": "^7.0",
                 "phpunit/php-text-template": "^1.2.1",
-                "sebastian/exporter": "^3.0"
+                "sebastian/exporter": "^3.1"
             },
             "conflict": {
                 "phpunit/phpunit": "<6.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^6.0"
+                "phpunit/phpunit": "^6.5"
             },
             "suggest": {
                 "ext-soap": "*"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "4.0.x-dev"
+                    "dev-master": "5.0.x-dev"
                 }
             },
             "autoload": {
             "authors": [
                 {
                     "name": "Sebastian Bergmann",
-                    "email": "sb@sebastian-bergmann.de",
+                    "email": "sebastian@phpunit.de",
                     "role": "lead"
                 }
             ],
                 "mock",
                 "xunit"
             ],
-            "time": "2017-08-03T14:08:16+00:00"
+            "time": "2018-01-06T05:45:45+00:00"
         },
         {
             "name": "psr/container",
         },
         {
             "name": "sebastian/comparator",
-            "version": "2.1.2",
+            "version": "2.1.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/comparator.git",
-                "reference": "11c07feade1d65453e06df3b3b90171d6d982087"
+                "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/11c07feade1d65453e06df3b3b90171d6d982087",
-                "reference": "11c07feade1d65453e06df3b3b90171d6d982087",
+                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9",
+                "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9",
                 "shasum": ""
             },
             "require": {
                 "php": "^7.0",
-                "sebastian/diff": "^2.0",
+                "sebastian/diff": "^2.0 || ^3.0",
                 "sebastian/exporter": "^3.1"
             },
             "require-dev": {
                 "compare",
                 "equality"
             ],
-            "time": "2018-01-12T06:34:42+00:00"
+            "time": "2018-02-01T13:46:46+00:00"
         },
         {
             "name": "sebastian/diff",
         },
         {
             "name": "symfony/browser-kit",
-            "version": "v3.4.3",
+            "version": "v3.4.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/browser-kit.git",
         },
         {
             "name": "symfony/class-loader",
-            "version": "v3.4.3",
+            "version": "v3.4.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/class-loader.git",
         },
         {
             "name": "symfony/config",
-            "version": "v3.4.3",
+            "version": "v3.4.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/config.git",
-                "reference": "cfd5c972f7b4992a5df41673d25d980ab077aa5b"
+                "reference": "05e10567b529476a006b00746c5f538f1636810e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/config/zipball/cfd5c972f7b4992a5df41673d25d980ab077aa5b",
-                "reference": "cfd5c972f7b4992a5df41673d25d980ab077aa5b",
+                "url": "https://api.github.com/repos/symfony/config/zipball/05e10567b529476a006b00746c5f538f1636810e",
+                "reference": "05e10567b529476a006b00746c5f538f1636810e",
                 "shasum": ""
             },
             "require": {
             },
             "require-dev": {
                 "symfony/dependency-injection": "~3.3|~4.0",
+                "symfony/event-dispatcher": "~3.3|~4.0",
                 "symfony/finder": "~3.3|~4.0",
                 "symfony/yaml": "~3.0|~4.0"
             },
             ],
             "description": "Symfony Config Component",
             "homepage": "https://symfony.com",
-            "time": "2018-01-03T07:37:34+00:00"
+            "time": "2018-02-14T10:03:57+00:00"
         },
         {
             "name": "symfony/console",
-            "version": "v3.3.15",
+            "version": "v3.3.16",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "56f07f63c80baeb43ab259abc42c6cecf6461fae"
+                "reference": "af7ec995de93671c03cc1b4e3176c8588bc79dcc"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/56f07f63c80baeb43ab259abc42c6cecf6461fae",
-                "reference": "56f07f63c80baeb43ab259abc42c6cecf6461fae",
+                "url": "https://api.github.com/repos/symfony/console/zipball/af7ec995de93671c03cc1b4e3176c8588bc79dcc",
+                "reference": "af7ec995de93671c03cc1b4e3176c8588bc79dcc",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Console Component",
             "homepage": "https://symfony.com",
-            "time": "2018-01-03T07:37:11+00:00"
+            "time": "2018-01-29T09:02:23+00:00"
         },
         {
             "name": "symfony/css-selector",
-            "version": "v3.4.3",
+            "version": "v3.4.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/css-selector.git",
-                "reference": "e66394bc7610e69279bfdb3ab11b4fe65403f556"
+                "reference": "544655f1fc078a9cd839fdda2b7b1e64627c826a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/css-selector/zipball/e66394bc7610e69279bfdb3ab11b4fe65403f556",
-                "reference": "e66394bc7610e69279bfdb3ab11b4fe65403f556",
+                "url": "https://api.github.com/repos/symfony/css-selector/zipball/544655f1fc078a9cd839fdda2b7b1e64627c826a",
+                "reference": "544655f1fc078a9cd839fdda2b7b1e64627c826a",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony CssSelector Component",
             "homepage": "https://symfony.com",
-            "time": "2018-01-03T07:37:34+00:00"
+            "time": "2018-02-03T14:55:07+00:00"
         },
         {
             "name": "symfony/debug",
-            "version": "v3.4.3",
+            "version": "v3.4.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/debug.git",
-                "reference": "603b95dda8b00020e4e6e60dc906e7b715b1c245"
+                "reference": "9b1071f86e79e1999b3d3675d2e0e7684268b9bc"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/debug/zipball/603b95dda8b00020e4e6e60dc906e7b715b1c245",
-                "reference": "603b95dda8b00020e4e6e60dc906e7b715b1c245",
+                "url": "https://api.github.com/repos/symfony/debug/zipball/9b1071f86e79e1999b3d3675d2e0e7684268b9bc",
+                "reference": "9b1071f86e79e1999b3d3675d2e0e7684268b9bc",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Debug Component",
             "homepage": "https://symfony.com",
-            "time": "2018-01-03T17:14:19+00:00"
+            "time": "2018-02-28T21:49:22+00:00"
         },
         {
             "name": "symfony/dependency-injection",
-            "version": "v3.3.15",
+            "version": "v3.3.16",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dependency-injection.git",
-                "reference": "e63cd8163d72a2a7722a40790ae80f2c0c8b50d1"
+                "reference": "54243abc4e1a1a15e274e391bd6f7090b44711f1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/e63cd8163d72a2a7722a40790ae80f2c0c8b50d1",
-                "reference": "e63cd8163d72a2a7722a40790ae80f2c0c8b50d1",
+                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/54243abc4e1a1a15e274e391bd6f7090b44711f1",
+                "reference": "54243abc4e1a1a15e274e391bd6f7090b44711f1",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony DependencyInjection Component",
             "homepage": "https://symfony.com",
-            "time": "2018-01-03T07:37:11+00:00"
+            "time": "2018-01-29T09:02:23+00:00"
         },
         {
             "name": "symfony/dom-crawler",
-            "version": "v3.4.3",
+            "version": "v3.4.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dom-crawler.git",
-                "reference": "09bd97b844b3151fab82f2fdd62db9c464b3910a"
+                "reference": "2bb5d3101cc01f4fe580e536daf4f1959bc2d24d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/09bd97b844b3151fab82f2fdd62db9c464b3910a",
-                "reference": "09bd97b844b3151fab82f2fdd62db9c464b3910a",
+                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/2bb5d3101cc01f4fe580e536daf4f1959bc2d24d",
+                "reference": "2bb5d3101cc01f4fe580e536daf4f1959bc2d24d",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony DomCrawler Component",
             "homepage": "https://symfony.com",
-            "time": "2018-01-03T07:37:34+00:00"
+            "time": "2018-02-22T10:48:49+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v3.4.3",
+            "version": "v3.4.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "26b87b6bca8f8f797331a30b76fdae5342dc26ca"
+                "reference": "58990682ac3fdc1f563b7e705452921372aad11d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/26b87b6bca8f8f797331a30b76fdae5342dc26ca",
-                "reference": "26b87b6bca8f8f797331a30b76fdae5342dc26ca",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/58990682ac3fdc1f563b7e705452921372aad11d",
+                "reference": "58990682ac3fdc1f563b7e705452921372aad11d",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony EventDispatcher Component",
             "homepage": "https://symfony.com",
-            "time": "2018-01-03T07:37:34+00:00"
+            "time": "2018-02-14T10:03:57+00:00"
         },
         {
             "name": "symfony/filesystem",
-            "version": "v3.4.3",
+            "version": "v3.4.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
-                "reference": "e078773ad6354af38169faf31c21df0f18ace03d"
+                "reference": "253a4490b528597aa14d2bf5aeded6f5e5e4a541"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/e078773ad6354af38169faf31c21df0f18ace03d",
-                "reference": "e078773ad6354af38169faf31c21df0f18ace03d",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/253a4490b528597aa14d2bf5aeded6f5e5e4a541",
+                "reference": "253a4490b528597aa14d2bf5aeded6f5e5e4a541",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Filesystem Component",
             "homepage": "https://symfony.com",
-            "time": "2018-01-03T07:37:34+00:00"
+            "time": "2018-02-22T10:48:49+00:00"
         },
         {
             "name": "symfony/polyfill-mbstring",
-            "version": "v1.6.0",
+            "version": "v1.7.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-mbstring.git",
-                "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296"
+                "reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
-                "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/78be803ce01e55d3491c1397cf1c64beb9c1b63b",
+                "reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.6-dev"
+                    "dev-master": "1.7-dev"
                 }
             },
             "autoload": {
                 "portable",
                 "shim"
             ],
-            "time": "2017-10-11T12:05:26+00:00"
+            "time": "2018-01-30T19:27:44+00:00"
         },
         {
             "name": "symfony/process",
-            "version": "v2.8.33",
+            "version": "v2.8.36",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/process.git",
-                "reference": "ea3226daa3c6789efa39570bfc6e5d55f7561a0a"
+                "reference": "756f614c5061729ea245ac6717231f7e3bfb74f9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/process/zipball/ea3226daa3c6789efa39570bfc6e5d55f7561a0a",
-                "reference": "ea3226daa3c6789efa39570bfc6e5d55f7561a0a",
+                "url": "https://api.github.com/repos/symfony/process/zipball/756f614c5061729ea245ac6717231f7e3bfb74f9",
+                "reference": "756f614c5061729ea245ac6717231f7e3bfb74f9",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Process Component",
             "homepage": "https://symfony.com",
-            "time": "2018-01-03T07:36:31+00:00"
+            "time": "2018-02-12T17:44:58+00:00"
         },
         {
             "name": "symfony/translation",
-            "version": "v3.3.15",
+            "version": "v3.3.16",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
-                "reference": "96be707e96ba9ac04964d8f556d0fbb33329411c"
+                "reference": "90cb5ca3eb84b3053fef876e11e405fd819487fc"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/translation/zipball/96be707e96ba9ac04964d8f556d0fbb33329411c",
-                "reference": "96be707e96ba9ac04964d8f556d0fbb33329411c",
+                "url": "https://api.github.com/repos/symfony/translation/zipball/90cb5ca3eb84b3053fef876e11e405fd819487fc",
+                "reference": "90cb5ca3eb84b3053fef876e11e405fd819487fc",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Translation Component",
             "homepage": "https://symfony.com",
-            "time": "2018-01-03T07:37:11+00:00"
+            "time": "2018-01-18T14:19:00+00:00"
         },
         {
             "name": "symfony/yaml",
-            "version": "v3.3.15",
+            "version": "v3.3.16",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/yaml.git",
-                "reference": "7c80d81b5805589be151b85b0df785f0dc3269cf"
+                "reference": "af615970e265543a26ee712c958404eb9b7ac93d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/yaml/zipball/7c80d81b5805589be151b85b0df785f0dc3269cf",
-                "reference": "7c80d81b5805589be151b85b0df785f0dc3269cf",
+                "url": "https://api.github.com/repos/symfony/yaml/zipball/af615970e265543a26ee712c958404eb9b7ac93d",
+                "reference": "af615970e265543a26ee712c958404eb9b7ac93d",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Yaml Component",
             "homepage": "https://symfony.com",
-            "time": "2018-01-03T07:37:11+00:00"
+            "time": "2018-01-20T15:04:53+00:00"
         },
         {
             "name": "theseer/tokenizer",
         },
         {
             "name": "webmozart/assert",
-            "version": "1.2.0",
+            "version": "1.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/webmozart/assert.git",
-                "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f"
+                "reference": "0df1908962e7a3071564e857d86874dad1ef204a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/webmozart/assert/zipball/2db61e59ff05fe5126d152bd0655c9ea113e550f",
-                "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f",
+                "url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a",
+                "reference": "0df1908962e7a3071564e857d86874dad1ef204a",
                 "shasum": ""
             },
             "require": {
                 "check",
                 "validate"
             ],
-            "time": "2016-11-23T20:04:58+00:00"
+            "time": "2018-01-29T19:49:41+00:00"
         }
     ],
     "aliases": [],
diff --git a/course/classes/search/section.php b/course/classes/search/section.php
new file mode 100644 (file)
index 0000000..11f6866
--- /dev/null
@@ -0,0 +1,195 @@
+<?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/>.
+
+/**
+ * Search area for course sections (title and summary).
+ *
+ * @package core_course
+ * @copyright 2018 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_course\search;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Search area for course sections (title and summary).
+ *
+ * Note this does not include the activities within the section, as these have their own search
+ * areas.
+ *
+ * @package core_course
+ * @copyright 2018 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class section extends \core_search\base {
+    /**
+     * Sections are indexed at course context.
+     *
+     * @var array
+     */
+    protected static $levels = [CONTEXT_COURSE];
+
+    /**
+     * Returns recordset containing required data for indexing course sections.
+     *
+     * @param int $modifiedfrom timestamp
+     * @param \context|null $context Restriction context
+     * @return \moodle_recordset|null Recordset or null if no change possible
+     */
+    public function get_document_recordset($modifiedfrom = 0, \context $context = null) {
+        global $DB;
+
+        list ($contextjoin, $contextparams) = $this->get_course_level_context_restriction_sql($context, 'c');
+        if ($contextjoin === null) {
+            return null;
+        }
+
+        $comparetext = $DB->sql_compare_text('cs.summary', 1);
+
+        return $DB->get_recordset_sql("
+                SELECT cs.id,
+                       cs.course,
+                       cs.section,
+                       cs.name,
+                       cs.summary,
+                       cs.summaryformat,
+                       cs.timemodified
+                  FROM {course_sections} cs
+                  JOIN {course} c ON c.id = cs.course
+          $contextjoin
+                 WHERE cs.timemodified >= ?
+                   AND (cs.name != ? OR $comparetext != ?)
+              ORDER BY cs.timemodified ASC", array_merge($contextparams, [$modifiedfrom, '', '']));
+    }
+
+    /**
+     * Returns the document associated with this section.
+     *
+     * @param \stdClass $record
+     * @param array $options
+     * @return \core_search\document
+     */
+    public function get_document($record, $options = array()) {
+        // Get the context, modinfo, and section.
+        try {
+            $context = \context_course::instance($record->course);
+        } catch (\moodle_exception $ex) {
+            // Notify it as we run here as admin, we should see everything.
+            debugging('Error retrieving ' . $this->areaid . ' ' . $record->id .
+                    ' document, not all required data is available: ' . $ex->getMessage(),
+                    DEBUG_DEVELOPER);
+            return false;
+        }
+
+        // Title - use default if none given.
+        $title = get_section_name($record->course, $record->section);
+
+        // Prepare associative array with data from DB.
+        $doc = \core_search\document_factory::instance($record->id, $this->componentname, $this->areaname);
+        $doc->set('title', content_to_text($title, false));
+        $doc->set('content', content_to_text($record->summary, $record->summaryformat));
+        $doc->set('contextid', $context->id);
+        $doc->set('courseid', $record->course);
+        $doc->set('owneruserid', \core_search\manager::NO_OWNER_ID);
+        $doc->set('modified', $record->timemodified);
+
+        return $doc;
+    }
+
+    /**
+     * Whether the user can access the section or not.
+     *
+     * @param int $id The course section id.
+     * @return int One of the \core_search\manager:ACCESS_xx constants
+     */
+    public function check_access($id) {
+        global $DB;
+
+        // Check we can get the section and the course modinfo.
+        $sectionrec = $DB->get_record('course_sections', ['id' => $id], '*', IGNORE_MISSING);
+        if (!$sectionrec) {
+            return \core_search\manager::ACCESS_DELETED;
+        }
+        try {
+            $modinfo = get_fast_modinfo($sectionrec->course);
+        } catch (\moodle_exception $e) {
+            return \core_search\manager::ACCESS_DELETED;
+        }
+        $section = $modinfo->get_section_info($sectionrec->section, IGNORE_MISSING);
+        if (!$section) {
+            return \core_search\manager::ACCESS_DELETED;
+        }
+
+        // Check access to course and that the section is visible to current user.
+        if (can_access_course($modinfo->get_course()) && $section->uservisible) {
+            return \core_search\manager::ACCESS_GRANTED;
+        }
+
+        return \core_search\manager::ACCESS_DENIED;
+    }
+
+    /**
+     * Gets a link to the section.
+     *
+     * @param \core_search\document $doc
+     * @return \moodle_url
+     */
+    public function get_doc_url(\core_search\document $doc) {
+        global $DB;
+        $section = $DB->get_field('course_sections', 'section', ['id' => $doc->get('itemid')], MUST_EXIST);
+        $format = course_get_format($doc->get('courseid'));
+        return $format->get_view_url($section);
+    }
+
+    /**
+     * Gets a link to the section.
+     *
+     * @param \core_search\document $doc
+     * @return \moodle_url
+     */
+    public function get_context_url(\core_search\document $doc) {
+        return $this->get_doc_url($doc);
+    }
+
+    /**
+     * Returns true to include summary files in the index.
+     *
+     * @return bool True
+     */
+    public function uses_file_indexing() {
+        return true;
+    }
+
+    /**
+     * Return the file area that is used for summary files.
+     *
+     * @return array File area name
+     */
+    public function get_search_fileareas() {
+        return ['section'];
+    }
+
+    /**
+     * Returns the moodle component name, as used in the files table.
+     *
+     * @return string Component name
+     */
+    public function get_component_name() {
+        return 'course';
+    }
+}
index f28f75e..0cb64fe 100644 (file)
@@ -21,8 +21,8 @@ Feature: Course activity controls works as expected
       | username | firstname | lastname | email |
       | teacher1 | Teacher | 1 | teacher1@example.com |
     And the following "courses" exist:
-      | fullname | shortname | format | coursedisplay | numsections |
-      | Course 1 | C1 | <courseformat> | <coursedisplay> | 5 |
+      | fullname | shortname | format | coursedisplay | numsections | startdate |
+      | Course 1 | C1 | <courseformat> | <coursedisplay> | 5 | 0 |
     And the following "course enrolments" exist:
       | user | course | role |
       | teacher1 | C1 | editingteacher |
@@ -93,8 +93,8 @@ Feature: Course activity controls works as expected
       | username | firstname | lastname | email |
       | teacher1 | Teacher | 1 | teacher1@example.com |
     And the following "courses" exist:
-      | fullname | shortname | format | coursedisplay | numsections |
-      | Course 1 | C1 | <courseformat> | <coursedisplay> | 5 |
+      | fullname | shortname | format | coursedisplay | numsections | startdate |
+      | Course 1 | C1 | <courseformat> | <coursedisplay> | 5 | 0 |
     And the following "course enrolments" exist:
       | user | course | role |
       | teacher1 | C1 | editingteacher |
index 3e88929..92fd475 100644 (file)
@@ -7,8 +7,8 @@ Feature: Course paged mode
   @javascript @_cross_browser
   Scenario Outline: Weekly and topics course formats with Javascript enabled
     Given the following "courses" exist:
-      | fullname | shortname | category | format | coursedisplay | numsections |
-      | Course 1 | C1 | 0 | <courseformat> | 1 | 3 |
+      | fullname | shortname | category | format | coursedisplay | numsections | startdate |
+      | Course 1 | C1 | 0 | <courseformat> | 1 | 3 | 0 |
     And I log in as "admin"
     And I am on "Course 1" course homepage
     Then I click on <section2> "link" in the <section2> "section"
@@ -40,8 +40,8 @@ Feature: Course paged mode
 
   Scenario Outline: Weekly and topics course formats with Javascript disabled
     Given the following "courses" exist:
-      | fullname | shortname | category | format | coursedisplay | numsections |
-      | Course 1 | C1 | 0 | <courseformat> | 1 | 3 |
+      | fullname | shortname | category | format | coursedisplay | numsections | startdate |
+      | Course 1 | C1 | 0 | <courseformat> | 1 | 3 | 0 |
     And I log in as "admin"
     And I am on "Course 1" course homepage
     Then I click on <section2> "link" in the <section2> "section"
index b609280..9751f1a 100644 (file)
@@ -43,11 +43,17 @@ class course_search_testcase extends advanced_testcase {
      */
     protected $mycoursesareaid = null;
 
+    /**
+     * @var string Area id for sections
+     */
+    protected $sectionareaid = null;
+
     public function setUp() {
         $this->resetAfterTest(true);
         set_config('enableglobalsearch', true);
 
         $this->mycoursesareaid = \core_search\manager::generate_areaid('core_course', 'mycourse');
+        $this->sectionareaid = \core_search\manager::generate_areaid('core_course', 'section');
 
         // Set \core_search::instance to the mock_search_engine as we don't require the search engine to be working to test this.
         $search = testable_core_search::instance();
@@ -240,4 +246,204 @@ class course_search_testcase extends advanced_testcase {
         $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($course2->id));
         $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($course3->id));
     }
+
+    /**
+     * Indexing section contents.
+     */
+    public function test_section_indexing() {
+        global $DB, $USER;
+
+        // Returns the instance as long as the area is supported.
+        $searcharea = \core_search\manager::get_search_area($this->sectionareaid);
+        $this->assertInstanceOf('\core_course\search\section', $searcharea);
+
+        // Create some courses in categories, and a forum.
+        $generator = $this->getDataGenerator();
+        $cat1 = $generator->create_category();
+        $cat2 = $generator->create_category(['parent' => $cat1->id]);
+        $course1 = $generator->create_course(['category' => $cat1->id]);
+        $course2 = $generator->create_course(['category' => $cat2->id]);
+        $forum = $generator->create_module('forum', ['course' => $course1->id]);
+
+        // Edit 2 sections on course 1 and one on course 2.
+        $existing = $DB->get_record('course_sections', ['course' => $course1->id, 'section' => 2]);
+        $course1section2id = $existing->id;
+        $new = clone($existing);
+        $new->name = 'Frogs';
+        course_update_section($course1->id, $existing, $new);
+
+        $existing = $DB->get_record('course_sections', ['course' => $course1->id, 'section' => 3]);
+        $course1section3id = $existing->id;
+        $new = clone($existing);
+        $new->summary = 'Frogs';
+        $new->summaryformat = FORMAT_HTML;
+        course_update_section($course1->id, $existing, $new);
+
+        $existing = $DB->get_record('course_sections', ['course' => $course2->id, 'section' => 1]);
+        $course2section1id = $existing->id;
+        $new = clone($existing);
+        $new->summary = 'Frogs';
+        $new->summaryformat = FORMAT_HTML;
+        course_update_section($course2->id, $existing, $new);
+
+        // Bodge timemodified into a particular order.
+        $DB->set_field('course_sections', 'timemodified', 1, ['id' => $course1section3id]);
+        $DB->set_field('course_sections', 'timemodified', 2, ['id' => $course1section2id]);
+        $DB->set_field('course_sections', 'timemodified', 3, ['id' => $course2section1id]);
+
+        // All records.
+        $results = self::recordset_to_ids($searcharea->get_document_recordset(0));
+        $this->assertEquals([$course1section3id, $course1section2id, $course2section1id], $results);
+
+        // Records after time 2.
+        $results = self::recordset_to_ids($searcharea->get_document_recordset(2));
+        $this->assertEquals([$course1section2id, $course2section1id], $results);
+
+        // Records after time 10 (there aren't any).
+        $results = self::recordset_to_ids($searcharea->get_document_recordset(10));
+        $this->assertEquals([], $results);
+
+        // Find the first block to use for a block context.
+        $blockid = array_values($DB->get_records('block_instances', null, 'id', 'id', 0, 1))[0]->id;
+        $blockcontext = context_block::instance($blockid);
+
+        // Check with block context - should be null.
+        $this->assertNull($searcharea->get_document_recordset(0, $blockcontext));
+
+        // Check with user context - should be null.
+        $this->setAdminUser();
+        $usercontext = context_user::instance($USER->id);
+        $this->assertNull($searcharea->get_document_recordset(0, $usercontext));
+
+        // Check with module context - should be null.
+        $modcontext = context_module::instance($forum->cmid);
+        $this->assertNull($searcharea->get_document_recordset(0, $modcontext));
+
+        // Check with course context - should return specific course entries.
+        $coursecontext = context_course::instance($course1->id);
+        $results = self::recordset_to_ids($searcharea->get_document_recordset(0, $coursecontext));
+        $this->assertEquals([$course1section3id, $course1section2id], $results);
+        $results = self::recordset_to_ids($searcharea->get_document_recordset(2, $coursecontext));
+        $this->assertEquals([$course1section2id], $results);
+
+        // Check with category context - should return course in categories and subcategories.
+        $catcontext = context_coursecat::instance($cat1->id);
+        $results = self::recordset_to_ids($searcharea->get_document_recordset(0, $catcontext));
+        $this->assertEquals([$course1section3id, $course1section2id, $course2section1id], $results);
+        $catcontext = context_coursecat::instance($cat2->id);
+        $results = self::recordset_to_ids($searcharea->get_document_recordset(0, $catcontext));
+        $this->assertEquals([$course2section1id], $results);
+
+        // Check with system context - should return everything (same as null, tested first).
+        $systemcontext = context_system::instance();
+        $results = self::recordset_to_ids($searcharea->get_document_recordset(0, $systemcontext));
+        $this->assertEquals([$course1section3id, $course1section2id, $course2section1id], $results);
+    }
+
+    /**
+     * Document contents for sections.
+     */
+    public function test_section_document() {
+        global $DB;
+
+        $searcharea = \core_search\manager::get_search_area($this->sectionareaid);
+
+        // Create a course.
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+
+        // Test with default title.
+        $sectionrec = (object)['id' => 123, 'course' => $course->id,
+                'section' => 3, 'timemodified' => 456,
+                'summary' => 'Kermit', 'summaryformat' => FORMAT_HTML];
+        $doc = $searcharea->get_document($sectionrec);
+        $this->assertInstanceOf('\core_search\document', $doc);
+        $this->assertEquals(123, $doc->get('itemid'));
+        $this->assertEquals($this->sectionareaid . '-123', $doc->get('id'));
+        $this->assertEquals($course->id, $doc->get('courseid'));
+        $this->assertFalse($doc->is_set('userid'));
+        $this->assertEquals(\core_search\manager::NO_OWNER_ID, $doc->get('owneruserid'));
+        $this->assertEquals('Topic 3', $doc->get('title'));
+        $this->assertEquals('Kermit', $doc->get('content'));
+
+        // Test with user-set title.
+        $DB->set_field('course_sections', 'name', 'Frogs',
+                ['course' => $course->id, 'section' => 3]);
+        rebuild_course_cache($course->id, true);
+        $doc = $searcharea->get_document($sectionrec);
+        $this->assertEquals('Frogs', $doc->get('title'));
+    }
+
+    /**
+     * Document access for sections.
+     */
+    public function test_section_access() {
+        global $DB;
+
+        $searcharea = \core_search\manager::get_search_area($this->sectionareaid);
+
+        // Create a course.
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+
+        // Create 2 users - student and manager. Initially, student is not even enrolled.
+        $student = $generator->create_user();
+        $manager = $generator->create_user();
+        $generator->enrol_user($manager->id, $course->id, 'manager');
+
+        // Two sections have content - one is hidden.
+        $DB->set_field('course_sections', 'name', 'Frogs',
+                ['course' => $course->id, 'section' => 1]);
+        $DB->set_field('course_sections', 'name', 'Toads',
+                ['course' => $course->id, 'section' => 2]);
+        $DB->set_field('course_sections', 'visible', '0',
+                ['course' => $course->id, 'section' => 2]);
+
+        // Make the modified time be in order of sections.
+        $DB->execute('UPDATE {course_sections} SET timemodified = section');
+
+        // Get the two document objects.
+        $rs = $searcharea->get_document_recordset();
+        $documents = [];
+        $index = 0;
+        foreach ($rs as $rec) {
+            $documents[$index++] = $searcharea->get_document($rec);
+        }
+        $this->assertCount(2, $documents);
+
+        // Log in as admin and check access.
+        $this->setAdminUser();
+        $this->assertEquals(\core_search\manager::ACCESS_GRANTED,
+                $searcharea->check_access($documents[0]->get('itemid')));
+        $this->assertEquals(\core_search\manager::ACCESS_GRANTED,
+                $searcharea->check_access($documents[1]->get('itemid')));
+
+        // Log in as manager and check access.
+        $this->setUser($manager);
+        $this->assertEquals(\core_search\manager::ACCESS_GRANTED,
+                $searcharea->check_access($documents[0]->get('itemid')));
+        $this->assertEquals(\core_search\manager::ACCESS_GRANTED,
+                $searcharea->check_access($documents[1]->get('itemid')));
+
+        // Log in as student and check access - none yet.
+        $this->setUser($student);
+        $this->assertEquals(\core_search\manager::ACCESS_DENIED,
+                $searcharea->check_access($documents[0]->get('itemid')));
+        $this->assertEquals(\core_search\manager::ACCESS_DENIED,
+                $searcharea->check_access($documents[1]->get('itemid')));
+
+        // Enrol student - now they should get access but not to the hidden one.
+        $generator->enrol_user($student->id, $course->id, 'student');
+        $this->assertEquals(\core_search\manager::ACCESS_GRANTED,
+                $searcharea->check_access($documents[0]->get('itemid')));
+        $this->assertEquals(\core_search\manager::ACCESS_DENIED,
+                $searcharea->check_access($documents[1]->get('itemid')));
+
+        // Delete the course and check it returns deleted.
+        delete_course($course, false);
+        $this->assertEquals(\core_search\manager::ACCESS_DELETED,
+                $searcharea->check_access($documents[0]->get('itemid')));
+        $this->assertEquals(\core_search\manager::ACCESS_DELETED,
+                $searcharea->check_access($documents[1]->get('itemid')));
+    }
 }
index 11500fb..941fb99 100644 (file)
@@ -774,6 +774,7 @@ class enrol_database_plugin extends enrol_plugin {
                 $template->summary        = '';
                 $template->summaryformat  = FORMAT_HTML;
                 $template->format         = $courseconfig->format;
+                $template->numsections    = $courseconfig->numsections;
                 $template->newsitems      = $courseconfig->newsitems;
                 $template->showgrades     = $courseconfig->showgrades;
                 $template->showreports    = $courseconfig->showreports;
index 03ea36f..d7eacac 100644 (file)
@@ -730,6 +730,13 @@ class enrol_database_testcase extends advanced_testcase {
         $this->assertEquals(1, $DB->count_records('course', array('idnumber' => 'yy')));
         $this->assertEquals(1, $DB->count_records('course', array('shortname' => 'xx')));
 
+        // Check default number of sections matches with the created course sections.
+
+        $recordcourse1 = $DB->get_record('course', $course1);
+        $courseconfig = get_config('moodlecourse');
+        $numsections = $DB->count_records('course_sections', array('course' => $recordcourse1->id));
+        // To compare numsections we have to add topic 0 to default numsections.
+        $this->assertEquals(($courseconfig->numsections + 1), $numsections);
 
         // Test category mapping via idnumber.
 
index f5f39d8..1cf7e30 100644 (file)
@@ -574,9 +574,11 @@ class core_files_renderer extends plugin_renderer_base {
         $rv = '
 <div tabindex="0" class="file-picker fp-generallayout" role="dialog" aria-live="assertive">
     <div class="fp-repo-area">
-        <ul class="fp-list">
-            <li class="fp-repo">
-                <a href="#"><img class="fp-repo-icon" alt=" " width="16" height="16" />&nbsp;<span class="fp-repo-name"></span></a>
+        <ul class="fp-list" role="tablist">
+            <li class="fp-repo" role="tab" aria-selected="false" tabindex="-1">
+                <a href="#" tabindex="-1"><img class="fp-repo-icon" alt=" " width="16" height="16" />&nbsp;
+                    <span class="fp-repo-name"></span>
+                </a>
             </li>
         </ul>
     </div>
index 755fa8a..6dabe7a 100644 (file)
@@ -270,7 +270,7 @@ abstract class grade_export {
         if ($grade_item->itemtype == 'mod') {
             $column->name = get_string('modulename', $grade_item->itemmodule).get_string('labelsep', 'langconfig').$grade_item->get_name();
         } else {
-            $column->name = $grade_item->get_name();
+            $column->name = $grade_item->get_name(true);
         }
 
         // We can't have feedback and display type at the same time.
index 8150e19..79c9e21 100644 (file)
@@ -60,8 +60,8 @@ require_login($course);
 $context = context_course::instance($course->id);
 require_capability('moodle/course:managegroups', $context);
 
-$PAGE->requires->js('/group/clientlib.js');
-$PAGE->requires->js('/group/module.js');
+$PAGE->requires->js('/group/clientlib.js', true);
+$PAGE->requires->js('/group/module.js', true);
 
 // Check for multiple/no group errors
 if (!$singlegroup) {
index 0a5501b..8868c6a 100644 (file)
@@ -166,7 +166,7 @@ $CFG->tempdir              = $CFG->dataroot.'/temp';
 $CFG->cachedir             = $CFG->dataroot.'/cache';
 $CFG->localcachedir        = $CFG->dataroot.'/localcache';
 $CFG->admin                = $config->admin;
-$CFG->docroot              = 'http://docs.moodle.org';
+$CFG->docroot              = 'https://docs.moodle.org';
 $CFG->langotherroot        = $CFG->dataroot.'/lang';
 $CFG->langlocalroot        = $CFG->dataroot.'/lang';
 $CFG->directorypermissions = isset($distro->directorypermissions) ? $distro->directorypermissions : 00777; // let distros set dir permissions
index e378767..17e85cf 100644 (file)
@@ -40,3 +40,6 @@ $string['cliunknowoption'] = 'Nie-erkende opsies:
   {$a}
 Gebruik die --help opsie.';
 $string['cliyesnoprompt'] = 'Tik y (beteken ja) of n (beteken nee)';
+$string['environmentrequireinstall'] = 'moet geïnstalleer en geaktiveer wees';
+$string['environmentrequireversion'] = 'weergawe {$a->needed} word benodig en jy het tans {$a->current}';
+$string['upgradekeyset'] = 'Opgradering sleutel (los skoon om nie te stel nie)';
diff --git a/install/lang/de_ch/langconfig.php b/install/lang/de_ch/langconfig.php
new file mode 100644 (file)
index 0000000..e2b19d7
--- /dev/null
@@ -0,0 +1,35 @@
+<?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/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package   installer
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['parentlanguage'] = 'de';
+$string['thisdirection'] = 'ltr';
+$string['thislanguage'] = 'Schweizer Hochdeutsch';
index ba3228f..4c46793 100644 (file)
@@ -36,8 +36,8 @@ $string['cannotcreatetempdir'] = 'Não é possível criar a pasta de ficheiros t
 $string['cannotdownloadcomponents'] = 'Não é possível descarregar os componentes.';
 $string['cannotdownloadzipfile'] = 'Não é possível descarregar o ficheiro ZIP.';
 $string['cannotfindcomponent'] = 'Não é possível encontrar o componente';
-$string['cannotsavemd5file'] = 'Não é possível gravar o ficheiro md5';
-$string['cannotsavezipfile'] = 'Não é possível gravar o ficheiro ZIP';
+$string['cannotsavemd5file'] = 'Não é possível guardar o ficheiro md5';
+$string['cannotsavezipfile'] = 'Não é possível guardar o ficheiro ZIP';
 $string['cannotunzipfile'] = 'Não é possível descompactar o ficheiro ZIP';
 $string['componentisuptodate'] = 'O componente está atualizado.';
 $string['dmlexceptiononinstall'] = '<p>Ocorreu um erro de base de dados [{$a->errorcode}].<br />{$a->debuginfo}</p>';
index c428260..6c9b91d 100644 (file)
@@ -34,15 +34,18 @@ $string['admindirname'] = 'Adresár pre správu (admin)';
 $string['availablelangs'] = 'Dostupné jazykové balíčky';
 $string['chooselanguagehead'] = 'Vyberte jazyk';
 $string['chooselanguagesub'] = 'Zvoľte si jazyk pre inštaláciu. Tento jazyk bude tiež použitý ako východzí jazyk portálu, ale môže byť neskôr zmenený.';
-$string['clialreadyinstalled'] = 'Súbor config.php už existuje. Použite admin/cli/upgrade.php ak chcete aktualizovať váš portál.';
+$string['clialreadyconfigured'] = 'Konfiguračný súbor config.php už existuje. Spusťte admin/cli/install_database.php, ak chcete inštalovať Moodle pre tento portál.';
+$string['clialreadyinstalled'] = 'Súbor config.php už existuje. Použite admin/cli/install_database.php ak chcete aktualizovať váš portál.';
 $string['cliinstallheader'] = 'Moodle {$a} inštalačný program z príkazového riadku';
 $string['databasehost'] = 'Databázový server';
 $string['databasename'] = 'Názov databázy';
 $string['databasetypehead'] = 'Vyberte ovládač pre databázu';
 $string['dataroot'] = 'Adresár pre údaje';
+$string['datarootpermission'] = 'Prístupové práva k údajovému adresáru';
 $string['dbprefix'] = 'Predpona tabuliek';
 $string['dirroot'] = 'Adresár Moodle';
 $string['environmenthead'] = 'Kontrola programového prostredia...';
+$string['environmentsub2'] = 'Každé vydanie Moodle vyžaduje určitú minimálnu verziu PHP a niekoľko povinných rozšírení PHP. Plná kontrola prostredia sa vykonáva pred každou inštaláciou a aktualizáciou. Prosím, kontaktujte správcu servera, ak neviete, ako nainštalovať novú verziu, alebo povoliť rozšírenia PHP.';
 $string['errorsinenvironment'] = 'Kontrola programového prostredia zlyhala!';
 $string['installation'] = 'Inštalácia';
 $string['langdownloaderror'] = 'Bohužiaľ, jazyk "{$a}" sa nepodarilo nainštalovať. Inštalácia bude pokračovať v angličtine.';
@@ -62,18 +65,19 @@ $string['pathserrcreatedataroot'] = 'Inštalátor nemôže vytvoriť dátový ad
 $string['pathshead'] = 'Vytvoriť cesty';
 $string['pathsrodataroot'] = 'Kmeňový adresár nie je zapisovateľný';
 $string['pathsroparentdataroot'] = 'Nadriadený adresár ({$a->parent}) nie je zapisovateľný. Inštalátor nemôže vytvoriť dátový adresár ({$a->dataroot}). ';
-$string['pathssubdataroot'] = 'Potrebujete adresár, kam Moodle bude ukladať nahrané súbory. Adresár by mal povoliť čítanie a zapisovanie údajov používateľom web serveru, ale nesmie byť prístupný priamo cez web rozhranie. Pokiaľ ešte neexistuje, inštalácia sa pokúsi o jeho vytvorenie. ';
-$string['pathssubdirroot'] = 'Plná cesta adresára moodle inštalácie';
+$string['pathssubdataroot'] = '<p>Potrebujete adresár, kam Moodle bude ukladať nahrané súbory.</p>
+<p>Adresár by mal povoliť čítanie a zapisovanie údajov používateľom web serveru (webový server býva väčšinou spustený pod používateľom "www-data" alebo "apache"), ale nesmie byť prístupný priamo cez web rozhranie. Pokiaľ ešte neexistuje, inštalácia sa pokúsi o jeho vytvorenie. </p>';
+$string['pathssubdirroot'] = '<p>Absolútna cesta adresára Moodle inštalácie.</p>';
 $string['pathsunsecuredataroot'] = 'Umiestnenie dátového adresára nie je bezpečné';
 $string['pathswrongadmindir'] = 'Administrátorský adresár neexistuje';
 $string['phpextension'] = 'Rozšírenie PHP {$a}';
 $string['phpversion'] = 'Verzia PHP';
-$string['phpversionhelp'] = '<p>Moodle si vyžaduje verziu PHP aspoň  4.3.0 alebo 5.1.0 (5.0.x obsahuje veľa známych chýb).</p>
+$string['phpversionhelp'] = '<p>Moodle si vyžaduje verziu PHP aspoň  5.6.5 alebo 7.1 (7.0.x má určité obmedzenia jadra).</p>
 <p>Vy máte momentálne nainštalovanú túto verziu {$a}.</p>
-<p>Musíte aktualizovať PHP alebo sa presunúť na hostiteľský počítač s novšou verziou PHP!<br />(V prípade 5.0.X môžete tiež prejsť na verziu 4.4.x)</p>';
+<p>Musíte aktualizovať PHP alebo sa presunúť na hostiteľský počítač s novšou verziou PHP!</p>';
 $string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
 $string['welcomep20'] = 'Podarilo so vám úspešne nainštalovať a spustiť balíček <strong>{$a->packname} {$a->packversion}</strong>. Gratulujeme!';
-$string['welcomep30'] = '<strong>{$a->installername}</strong> obsahuje aplikáciu k vytvoreniu prostredia, v ktorom bude prevádzkovaný váš <strong>Moodle</strong>. Menovite sa jedná o:';
+$string['welcomep30'] = 'Táto verzia <strong>{$a->installername}</strong> obsahuje aplikáciu k vytvoreniu prostredia, v ktorom bude prevádzkovaný váš <strong>Moodle</strong>. Menovite sa jedná o:';
 $string['welcomep40'] = 'Balíček tiež obsahuje <strong>Moodle vo verzii {$a->moodlerelease} ({$a->moodleversion})</strong>.';
 $string['welcomep50'] = 'Použitie všetkých aplikácií v tomto balíčku je viazané ich príslušnými licenciami. Kompletný balíček <strong>{$a->installername}</strong> je software s <a href="http://www.opensource.org/docs/definition_plain.html"> otvoreným kódom (open source)</a> a je šírený pod licenciou <a href="http://www.gnu.org/copyleft/gpl.html">GPL</a>.';
 $string['welcomep60'] = 'Nasledujúce stránky vás povedú v nekoľkých jednoduchých krokoch nastavením <strong>Moodle</strong> na vašom počítači. Môžete prijať východzie nastavenie, alebo si ich upraviť podľa svojich potrieb.';
index 6f164a7..b65be59 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+global $CFG;
+
+require_once("{$CFG->libdir}/filelib.php");
+require_once("{$CFG->dirroot}/iplookup/lib.php");
+
 
 /**
  * GeoIp data file parsing test.
  */
 class core_iplookup_geoip_testcase extends advanced_testcase {
-
     public function setUp() {
-        global $CFG;
-        require_once("$CFG->libdir/filelib.php");
-        require_once("$CFG->dirroot/iplookup/lib.php");
-
         if (!PHPUNIT_LONGTEST) {
-            // this may take a long time
+            // These tests are intensive and required downloads.
             $this->markTestSkipped('PHPUNIT_LONGTEST is not defined');
         }
 
         $this->resetAfterTest();
+    }
+
 
-        // let's store the file somewhere
+    /**
+     * Setup the GeoIP2File system.
+     */
+    public function setup_geoip2file() {
+        global $CFG;
+
+        // Store the file somewhere where it won't be wiped out..
         $gzfile = "$CFG->dataroot/phpunit/geoip/GeoLite2-City.mmdb.gz";
         check_dir_exists(dirname($gzfile));
         if (file_exists($gzfile) and (filemtime($gzfile) < time() - 60*60*24*30)) {
-            // delete file if older than 1 month
+            // Delete file if older than 1 month.
             unlink($gzfile);
         }
 
@@ -80,32 +88,42 @@ class core_iplookup_geoip_testcase extends advanced_testcase {
         $CFG->geoip2file = $geoipfile;
     }
 
-    public function test_ipv4() {
-        $result = iplookup_find_location('192.30.255.112');
-
-        $this->assertEquals('array', gettype($result));
-        $this->assertEquals('San Francisco', $result['city']);
-        $this->assertEquals(-122.3933, $result['longitude'], 'Coordinates are out of accepted tolerance', 0.01);
-        $this->assertEquals(37.7697, $result['latitude'], 'Coordinates are out of accepted tolerance', 0.01);
+    /**
+     * Test the format of data returned in the iplookup_find_location function.
+     *
+     * @dataProvider ip_provider
+     * @param   string  $ip The IP to test
+     */
+    public function test_ip($ip) {
+        $this->setup_geoip2file();
+
+        // Note: The results we get from the iplookup tests are beyond our control.
+        // We used to check a specific IP to a known location, but these have become less reliable and change too
+        // frequently to be used for testing.
+
+        $result = iplookup_find_location($ip);
+
+        $this->assertInternalType('array', $result);
+        $this->assertInternalType('float', $result['latitude']);
+        $this->assertInternalType('float', $result['longitude']);
+        $this->assertInternalType('string', $result['city']);
+        $this->assertInternalType('string', $result['country']);
+        $this->assertInternalType('array', $result['title']);
+        $this->assertInternalType('string', $result['title'][0]);
+        $this->assertInternalType('string', $result['title'][1]);
         $this->assertNull($result['error']);
-        $this->assertEquals('array', gettype($result['title']));
-        $this->assertEquals('San Francisco', $result['title'][0]);
-        $this->assertEquals('United States', $result['title'][1]);
     }
 
-    public function test_ipv6() {
-        // NOTE: these tests can be altered by the geoip dataset, there has been an attempt to get
-        // a 'reliable' result.
-
-        $result = iplookup_find_location('2607:f010:3fe:fff1::ff:fe00:25');
-
-        $this->assertEquals('array', gettype($result));
-        $this->assertEquals('Los Angeles', $result['city']);
-        $this->assertEquals(-118.2987, $result['longitude'], 'Coordinates are out of accepted tolerance', 0.01);
-        $this->assertEquals(33.7866, $result['latitude'], 'Coordinates are out of accepted tolerance', 0.01);
-        $this->assertNull($result['error']);
-        $this->assertEquals('array', gettype($result['title']));
-        $this->assertEquals('Los Angeles', $result['title'][0]);
-        $this->assertEquals('United States', $result['title'][1]);
+    /**
+     * Data provider for IP lookup test.
+     *
+     * @return array
+     */
+    public function ip_provider() {
+        return [
+            'IPv4: Sample suggested by maxmind themselves' => ['24.24.24.24'],
+            'IPv4: github.com' => ['192.30.255.112'],
+            'IPv6: UCLA' => ['2607:f010:3fe:fff1::ff:fe00:25'],
+        ];
     }
 }
index 44de1dc..d35ddcb 100644 (file)
@@ -37,6 +37,7 @@ $string['caching'] = 'Caching';
 $string['cacheadmin'] = 'Cache administration';
 $string['cacheconfig'] = 'Configuration';
 $string['cachedef_calendar_subscriptions'] = 'Calendar subscriptions';
+$string['cachedef_calendar_categories'] = 'Calendar course categories that a user can access';
 $string['cachedef_capabilities'] = 'System capabilities list';
 $string['cachedef_config'] = 'Config settings';
 $string['cachedef_coursecat'] = 'Course categories lists for particular user';
index 91eac47..a97ebbb 100644 (file)
@@ -45,6 +45,8 @@ $string['emailtagline'] = 'This is a copy of a message sent to you at "{$a->site
 $string['enabled'] = 'Enabled';
 $string['errorcallingprocessor'] = 'Error calling defined output';
 $string['errortranslatingdefault'] = 'Error translating default setting provided by plugin, using system defaults instead.';
+$string['eventnotificationviewed'] = 'Notification viewed';
+$string['eventnotificationsent'] = 'Notification sent';
 $string['eventmessagecontactadded'] = 'Message contact added';
 $string['eventmessagecontactblocked'] = 'Message contact blocked';
 $string['eventmessagecontactremoved'] = 'Message contact removed';
index ead22db..90b5b8f 100644 (file)
@@ -290,6 +290,7 @@ $string['stoponerror'] = 'Stop on error';
 $string['stoponerror_help'] = 'This setting determines whether the import process stops when an error is detected, resulting in no questions being imported, or whether any questions containing errors are ignored and any valid questions are imported.';
 $string['tofilecategory'] = 'Write category to file';
 $string['tofilecontext'] = 'Write context to file';
+$string['topfor'] = 'Top for {$a}';
 $string['uninstallbehaviour'] = 'Uninstall this question behaviour.';
 $string['uninstallqtype'] = 'Uninstall this question type.';
 $string['unknown'] = 'Unknown';
index 4d515bb..6afcbd4 100644 (file)
@@ -81,6 +81,9 @@ $string['notitle'] = 'No title';
 $string['normalsearch'] = 'Normal search';
 $string['openedon'] = 'opened on';
 $string['optimize'] = 'Optimize';
+$string['order'] = 'Results order';
+$string['order_location'] = 'Prioritise results related to {$a}';
+$string['order_relevance'] = 'Most relevant results first';
 $string['priority'] = 'Priority';
 $string['priority_reindexing'] = 'Reindexing';
 $string['priority_normal'] = 'Normal';
@@ -97,6 +100,7 @@ $string['search'] = 'Search';
 $string['search:message_received'] = 'Messages - received';
 $string['search:message_sent'] = 'Messages - sent';
 $string['search:mycourse'] = 'My courses';
+$string['search:section'] = 'Course sections';
 $string['search:user'] = 'Users';
 $string['searcharea'] = 'Search area';
 $string['searching'] = 'Searching in ...';
index 30f756a..b14741e 100644 (file)
@@ -2960,6 +2960,7 @@ class admin_setting_configselect extends admin_setting {
     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
         // Look for optgroup and single options.
         if (is_array($choices)) {
+            $this->choices = [];
             foreach ($choices as $key => $val) {
                 if (is_array($val)) {
                     $this->optgroups[$key] = $val;
index ab03bb9..f3f4697 100644 (file)
@@ -81,7 +81,7 @@ class course_module_completion_updated extends base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/report/completion/index.php', array('course' => $this->courseid));
+        return new \moodle_url('/report/progress/index.php', array('course' => $this->courseid));
     }
 
     /**
index 7d5087e..b991853 100644 (file)
@@ -32,7 +32,6 @@ defined('MOODLE_INTERNAL') || die();
  * @property-read array $other {
  *      Extra information about event.
  *
- *      - string $messagetable: the table we marked the message as deleted from (message/message_read).
  *      - int messageid: the id of the message.
  *      - int useridfrom: the id of the user who received the message.
  *      - int useridto: the id of the user who sent the message.
@@ -51,11 +50,11 @@ class message_deleted extends base {
      * @param int $userfromid the user who the message was from.
      * @param int $usertoid the user who the message was sent to.
      * @param int $userdeleted the user who deleted it.
-     * @param string $messagetable the table we are marking the message as deleted in.
      * @param int $messageid the id of the message that was deleted.
+     * @param int $muaid The id in the message_user_actions table
      * @return message_deleted
      */
-    public static function create_from_ids($userfromid, $usertoid, $userdeleted, $messagetable, $messageid) {
+    public static function create_from_ids($userfromid, $usertoid, $userdeleted, $messageid, $muaid) {
         // Check who was deleting the message.
         if ($userdeleted == $userfromid) {
             $relateduserid = $usertoid;
@@ -66,11 +65,11 @@ class message_deleted extends base {
         // We set the userid to the user who deleted the message, nothing to do
         // with whether or not they sent or received the message.
         $event = self::create(array(
+            'objectid' => $muaid,
             'userid' => $userdeleted,
             'context' => \context_system::instance(),
             'relateduserid' => $relateduserid,
             'other' => array(
-                'messagetable' => $messagetable,
                 'messageid' => $messageid,
                 'useridfrom' => $userfromid,
                 'useridto' => $usertoid
@@ -84,7 +83,8 @@ class message_deleted extends base {
      * Init method.
      */
     protected function init() {
-        $this->data['crud'] = 'u';
+        $this->data['objecttable'] = 'message_user_actions';
+        $this->data['crud'] = 'c';
         $this->data['edulevel'] = self::LEVEL_OTHER;
     }
 
@@ -126,10 +126,6 @@ class message_deleted extends base {
             throw new \coding_exception('The \'relateduserid\' must be set.');
         }
 
-        if (!isset($this->other['messagetable'])) {
-            throw new \coding_exception('The \'messagetable\' value must be set in other.');
-        }
-
         if (!isset($this->other['messageid'])) {
             throw new \coding_exception('The \'messageid\' value must be set in other.');
         }
@@ -143,11 +139,13 @@ class message_deleted extends base {
         }
     }
 
+    public static function get_objectid_mapping() {
+        return array('db' => 'message_user_actions', 'restore' => base::NOT_MAPPED);
+    }
+
     public static function get_other_mapping() {
         // Messages are not backed up, so no need to map them on restore.
         $othermapped = array();
-        // The messageid table varies so it cannot be mapped.
-        $othermapped['messageid'] = array('db' => base::NOT_MAPPED, 'restore' => base::NOT_MAPPED);
         $othermapped['useridfrom'] = array('db' => 'user', 'restore' => base::NOT_MAPPED);
         $othermapped['useridto'] = array('db' => 'user', 'restore' => base::NOT_MAPPED);
         return $othermapped;
index e330919..9b3a8c5 100644 (file)
@@ -32,7 +32,6 @@ defined('MOODLE_INTERNAL') || die();
  * @property-read array $other {
  *      Extra information about event.
  *
- *      - int messageid: the id of the message.
  *      - int courseid: the id of the related course.
  * }
  *
@@ -69,14 +68,11 @@ class message_sent extends base {
         }
 
         $event = self::create(array(
+            'objectid' => $messageid,
             'userid' => $userfromid,
             'context' => \context_system::instance(),
             'relateduserid' => $usertoid,
             'other' => array(
-                // In earlier versions it can either be the id in the 'message_read' or 'message' table.
-                // Now it is always the id from 'message' table. Please note that the record is still moved
-                // to the 'message_read' table later when message marked as read.
-                'messageid' => $messageid,
                 'courseid' => $courseid
             )
         ));
@@ -88,6 +84,7 @@ class message_sent extends base {
      * Init method.
      */
     protected function init() {
+        $this->data['objecttable'] = 'messages';
         $this->data['crud'] = 'c';
         $this->data['edulevel'] = self::LEVEL_OTHER;
     }
@@ -133,8 +130,9 @@ class message_sent extends base {
         // The add_to_log function was only ever called when we sent a message from one user to another. We do not want
         // to return the legacy log data if we are sending a system message, so check that the userid is valid.
         if (\core_user::is_real_user($this->userid)) {
+            $messageid = $this->other['messageid'] ?? $this->objectid; // For BC we may have 'messageid' in other.
             return array(SITEID, 'message', 'write', 'index.php?user=' . $this->userid . '&id=' . $this->relateduserid .
-                '&history=1#m' . $this->other['messageid'], $this->userid);
+                '&history=1#m' . $messageid, $this->userid);
         }
 
         return null;
@@ -153,26 +151,18 @@ class message_sent extends base {
             throw new \coding_exception('The \'relateduserid\' must be set.');
         }
 
-        if (!isset($this->other['messageid'])) {
-            throw new \coding_exception('The \'messageid\' value must be set in other.');
-        }
-
         if (!isset($this->other['courseid'])) {
             throw new \coding_exception('The \'courseid\' value must be set in other.');
         }
     }
 
     public static function get_objectid_mapping() {
-        // Messages are not backed up, so no need to map them.
-        return false;
+        return array('db' => 'messages', 'restore' => base::NOT_MAPPED);
     }
 
     public static function get_other_mapping() {
-        // Messages are not backed up, so no need to map them on restore.
         $othermapped = array();
-        // The messages table could vary for older events - so cannot be mapped.
-        $othermapped['messageid'] = array('db' => base::NOT_MAPPED, 'restore' => base::NOT_MAPPED);
-        $othermapped['courseid'] = array('db' => base::NOT_MAPPED, 'restore' => base::NOT_MAPPED);
+        $othermapped['courseid'] = array('db' => 'course', 'restore' => base::NOT_MAPPED);
         return $othermapped;
     }
 }
index 8e12947..35ae1ff 100644 (file)
@@ -46,7 +46,7 @@ class message_viewed extends base {
      * Init method.
      */
     protected function init() {
-        $this->data['objecttable'] = 'message_read';
+        $this->data['objecttable'] = 'message_user_actions';
         $this->data['crud'] = 'c';
         $this->data['edulevel'] = self::LEVEL_OTHER;
     }
@@ -97,15 +97,13 @@ class message_viewed extends base {
     }
 
     public static function get_objectid_mapping() {
-        // Messages are not backed up, so no need to map them.
-        return array('db' => 'message_read', 'restore' => base::NOT_MAPPED);
+        return array('db' => 'message_user_actions', 'restore' => base::NOT_MAPPED);
     }
 
     public static function get_other_mapping() {
         // Messages are not backed up, so no need to map them on restore.
         $othermapped = array();
-        // The messages table could vary for older events - so cannot be mapped.
-        $othermapped['messageid'] = array('db' => base::NOT_MAPPED, 'restore' => base::NOT_MAPPED);
+        $othermapped['messageid'] = array('db' => 'messages', 'restore' => base::NOT_MAPPED);
         return $othermapped;
     }
 }
diff --git a/lib/classes/event/notification_sent.php b/lib/classes/event/notification_sent.php
new file mode 100644 (file)
index 0000000..1c2080c
--- /dev/null
@@ -0,0 +1,144 @@
+<?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/>.
+
+/**
+ * Notification sent event.
+ *
+ * @package    core
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Notification sent event class.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      - int courseid: the id of the related course.
+ * }
+ *
+ * @package    core
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class notification_sent extends base {
+
+    /**
+     * Create event using ids.
+     *
+     * @param int $userfromid
+     * @param int $usertoid
+     * @param int $notificationid
+     * @param int $courseid course id the event is related with - SITEID if no relation exists.
+     * @return notification_sent
+     */
+    public static function create_from_ids($userfromid, $usertoid, $notificationid, $courseid) {
+        // We may be sending a notification from the 'noreply' address, which means we are not actually sending a
+        // notification from a valid user. In this case, we will set the userid to 0.
+        // Check if the userid is valid.
+        if (!\core_user::is_real_user($userfromid)) {
+            $userfromid = 0;
+        }
+
+        $event = self::create(
+            [
+                'objectid' => $notificationid,
+                'userid' => $userfromid,
+                'context' => \context_system::instance(),
+                'relateduserid' => $usertoid,
+                'other' => [
+                    'courseid' => $courseid
+                ]
+            ]
+        );
+
+        return $event;
+    }
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'notifications';
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventnotificationsent', 'message');
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/message/output/popup/notifications.php', array('notificationid' => $this->objectid));
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        // Check if we are sending from a valid user.
+        if (\core_user::is_real_user($this->userid)) {
+            return "The user with id '$this->userid' sent a notification to the user with id '$this->relateduserid'.";
+        }
+
+        return "A notification was sent by the system to the user with id '$this->relateduserid'.";
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+
+        if (!isset($this->other['courseid'])) {
+            throw new \coding_exception('The \'courseid\' value must be set in other.');
+        }
+    }
+
+    public static function get_objectid_mapping() {
+        return array('db' => 'notifications', 'restore' => base::NOT_MAPPED);
+    }
+
+    public static function get_other_mapping() {
+        $othermapped = array();
+        $othermapped['courseid'] = array('db' => 'course', 'restore' => base::NOT_MAPPED);
+        return $othermapped;
+    }
+}
diff --git a/lib/classes/event/notification_viewed.php b/lib/classes/event/notification_viewed.php
new file mode 100644 (file)
index 0000000..f33c333
--- /dev/null
@@ -0,0 +1,126 @@
+<?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/>.
+
+/**
+ * Notification viewed event.
+ *
+ * @package    core
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Notification viewed event class.
+ *
+ * @package    core
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class notification_viewed extends base {
+
+    /**
+     * Create event using ids.
+     *
+     * @param int $userfromid
+     * @param int $usertoid
+     * @param int $notificationid
+     * @return notification_viewed
+     */
+    public static function create_from_ids($userfromid, $usertoid, $notificationid) {
+        // We may be sending a notification from the 'noreply' address, which means we are not actually sending a
+        // notification from a valid user. In this case, we will set the userid to 0.
+        // Check if the userid is valid.
+        if (!\core_user::is_real_user($userfromid)) {
+            $userfromid = 0;
+        }
+
+        // Get the context for the user who received the notification.
+        $context = \context_user::instance($usertoid, IGNORE_MISSING);
+        // If the user no longer exists the context value will be false, in this case use the system context.
+        if ($context === false) {
+            $context = \context_system::instance();
+        }
+
+        $event = self::create(
+            [
+                'objectid' => $notificationid,
+                'userid' => $usertoid, // Using the user who read the notification as they are the ones performing the action.
+                'context' => $context,
+                'relateduserid' => $userfromid
+            ]
+        );
+
+        return $event;
+    }
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'notifications';
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventnotificationviewed', 'message');
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/message/output/popup/notifications.php', array('notificationid' => $this->objectid));
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' read a notification from the user with id '$this->relateduserid'.";
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+    }
+
+    public static function get_objectid_mapping() {
+        return array('db' => 'notifications', 'restore' => base::NOT_MAPPED);
+    }
+}
index d08489d..c05e14d 100644 (file)
@@ -54,7 +54,7 @@ class manager {
      * @param \core\message\message $eventdata fully prepared event data for processors
      * @param \stdClass $savemessage the message saved in 'message' table
      * @param array $processorlist list of processors for target user
-     * @return int $messageid the id from 'message' or 'message_read' table (false is not returned)
+     * @return int $messageid the id from 'messages' (false is not returned)
      */
     public static function send_message($eventdata, \stdClass $savemessage, array $processorlist) {
         global $CFG;
@@ -77,27 +77,35 @@ class manager {
         require_once($CFG->dirroot.'/message/lib.php'); // This is most probably already included from messagelib.php file.
 
         if (empty($processorlist)) {
-            // Trigger event for sending a message - we need to do this before marking as read!
-            \core\event\message_sent::create_from_ids(
-                $eventdata->userfrom->id,
-                $eventdata->userto->id,
-                $savemessage->id,
-                $eventdata->courseid
+            // Trigger event for sending a message or notification - we need to do this before marking as read!
+            if ($eventdata->notification) {
+                \core\event\notification_sent::create_from_ids(
+                    $eventdata->userfrom->id,
+                    $eventdata->userto->id,
+                    $savemessage->id,
+                    $eventdata->courseid
+                )->trigger();
+            } else { // Must be a message.
+                \core\event\message_sent::create_from_ids(
+                    $eventdata->userfrom->id,
+                    $eventdata->userto->id,
+                    $savemessage->id,
+                    $eventdata->courseid
                 )->trigger();
+            }
 
-            if ($savemessage->notification or empty($CFG->messaging)) {
+            if ($eventdata->notification or empty($CFG->messaging)) {
                 // If they have deselected all processors and its a notification mark it read. The user doesn't want to be bothered.
                 // The same goes if the messaging is completely disabled.
-                // We cannot insert directly to the message_read table because we want to get all events in proper order!
-                $messageid = message_mark_message_read($savemessage, time(), true);
-
-            } else {
-                // Just add it to the list of unread messages, there is no way it could be delivered to them,
-                // but they can read it via the messaging UI later.
-                $messageid = $savemessage->id;
+                if ($eventdata->notification) {
+                    $savemessage->timeread = null;
+                    \core_message\api::mark_notification_as_read($savemessage);
+                } else {
+                    \core_message\api::mark_message_as_read($eventdata->userto->id, $savemessage);
+                }
             }
 
-            return $messageid;
+            return $savemessage->id;
         }
 
         // Let the manager do the sending or buffering when db transaction in progress.
@@ -133,7 +141,6 @@ class manager {
             return $savemessage->id;
         }
 
-        $failed = false;
         foreach ($processorlist as $procname) {
             // Let new messaging class add custom content based on the processor.
             $proceventdata = ($eventdata instanceof message) ? $eventdata->get_eventobject_for_processor($procname) : $eventdata;
@@ -142,40 +149,38 @@ class manager {
             $processor = \core_message\api::get_processed_processor_object($stdproc);
             if (!$processor->object->send_message($proceventdata)) {
                 debugging('Error calling message processor ' . $procname);
-                $failed = true;
-                // Previously the $messageid = false here was overridden
-                // by other processors and message_mark_message_read() below.
             }
         }
 
-        // Trigger event for sending a message - must be done before marking as read.
-        \core\event\message_sent::create_from_ids(
-            $eventdata->userfrom->id,
-            $eventdata->userto->id,
-            $savemessage->id,
-            $eventdata->courseid
+        // Trigger event for sending a message or notification - we need to do this before marking as read!
+        if ($eventdata->notification) {
+            \core\event\notification_sent::create_from_ids(
+                $eventdata->userfrom->id,
+                $eventdata->userto->id,
+                $savemessage->id,
+                $eventdata->courseid
             )->trigger();
+        } else { // Must be a message.
+            \core\event\message_sent::create_from_ids(
+                $eventdata->userfrom->id,
+                $eventdata->userto->id,
+                $savemessage->id,
+                $eventdata->courseid
+            )->trigger();
+        }
 
         if (empty($CFG->messaging)) {
-            // If messaging is disabled and they previously had forum notifications handled by the popup processor
-            // or any processor that puts a row in message_working then the notification will remain forever
-            // unread. To prevent this mark the message read if messaging is disabled.
-            $messageid = message_mark_message_read($savemessage, time());
-
-        } else if ($failed) {
-            // Something failed, better keep it as unread then.
-            $messageid = $savemessage->id;
-
-        } else if ($DB->count_records('message_working', array('unreadmessageid' => $savemessage->id)) == 0) {
-            // If there is no more processors that want to process this we can move message to message_read.
-            $messageid = message_mark_message_read($savemessage, time(), true);
-
-        } else {
-            // Some processor is still working on the data, let's keep it unread.
-            $messageid = $savemessage->id;
+            // If they have deselected all processors and its a notification mark it read. The user doesn't want to be bothered.
+            // The same goes if the messaging is completely disabled.
+            if ($eventdata->notification) {
+                $savemessage->timeread = null;
+                \core_message\api::mark_notification_as_read($savemessage);
+            } else {
+                \core_message\api::mark_message_as_read($eventdata->userto->id, $savemessage);
+            }
         }
 
-        return $messageid;
+        return $savemessage->id;
     }
 
     /**
index 7af9689..302e536 100644 (file)
@@ -1884,7 +1884,7 @@ class core_plugin_manager {
             ),
 
             'search' => array(
-                'solr'
+                'simpledb', 'solr'
             ),
 
             'scormreport' => array(
@@ -1908,7 +1908,7 @@ class core_plugin_manager {
                 'dbtransfer', 'filetypes', 'generator', 'health', 'httpsreplace', 'innodb', 'installaddon',
                 'langimport', 'log', 'lp', 'lpimportcsv', 'lpmigrate', 'messageinbound', 'mobile', 'multilangupgrade',
                 'monitor', 'oauth2', 'phpunit', 'profiling', 'recyclebin', 'replace', 'spamcleaner', 'task', 'templatelibrary',
-                'unittest', 'uploadcourse', 'uploaduser', 'unsuproles', 'usertours', 'xmldb'
+                'uploadcourse', 'uploaduser', 'unsuproles', 'usertours', 'xmldb'
             ),
 
             'webservice' => array(
index 4debce3..6d1a2c4 100644 (file)
@@ -50,7 +50,7 @@ class messaging_cleanup_task extends scheduled_task {
         if (!empty($CFG->messagingdeletereadnotificationsdelay)) {
             $notificationdeletetime = $timenow - $CFG->messagingdeletereadnotificationsdelay;
             $params = array('notificationdeletetime' => $notificationdeletetime);
-            $DB->delete_records_select('message_read', 'notification=1 AND timeread<:notificationdeletetime', $params);
+            $DB->delete_records_select('notifications', 'timeread < :notificationdeletetime', $params);
         }
 
     }
index 02123a1..5ad20b7 100644 (file)
@@ -722,6 +722,9 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
             return;
         }
 
+        // Trigger a purge for all caches listening for changes to category enrolment.
+        cache_helper::purge_by_event('changesincategoryenrolment');
+
         if (!$CFG->coursecontact || !in_array($roleid, explode(',', $CFG->coursecontact))) {
             // The role is not one of course contact roles.
             return;
@@ -1171,6 +1174,27 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
         return $rv;
     }
 
+    /**
+     * Returns an array of ids of categories that are (direct and indirect) children
+     * of this category.
+     *
+     * @return int[]
+     */
+    public function get_all_children_ids() {
+        $children = [];
+        $walk = [$this->id];
+        while (count($walk) > 0) {
+            $catid = array_pop($walk);
+            $directchildren = self::get_tree($catid);
+            if ($directchildren !== false && count($directchildren) > 0) {
+                $walk = array_merge($walk, $directchildren);
+                $children = array_merge($children, $directchildren);
+            }
+        }
+
+        return $children;
+    }
+
     /**
      * Returns true if the user has the manage capability on any category.
      *
index fb43616..84ccd70 100644 (file)
@@ -127,6 +127,18 @@ $definitions = array(
         'staticacceleration' => true,
     ),
 
+    // Cache the course categories where the user has any enrolment and all categories that this user can manage.
+    'calendar_categories' => array(
+        'mode' => cache_store::MODE_SESSION,
+        'simplekeys' => true,
+        'simpledata' => true,
+        'invalidationevents' => array(
+            'changesincoursecat',
+            'changesincategoryenrolment',
+        ),
+        'ttl' => 900,
+    ),
+
     // Cache the capabilities list DB table. See get_all_capabilities in accesslib.
     'capabilities' => array(
         'mode' => cache_store::MODE_APPLICATION,
index 97df485..abd5c93 100644 (file)
         <INDEX NAME="useridto_timeusertodeleted_notification" UNIQUE="false" FIELDS="useridto, timeusertodeleted, notification"/>
       </INDEXES>
     </TABLE>
+    <TABLE NAME="messages" COMMENT="Stores all messages">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="useridfrom" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="conversationid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="subject" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="fullmessage" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="fullmessageformat" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="fullmessagehtml" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="smallmessage" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="useridfrom" TYPE="foreign" FIELDS="useridfrom" REFTABLE="user" REFFIELDS="id"/>
+        <KEY NAME="conversationid" TYPE="foreign" FIELDS="conversationid" REFTABLE="message_conversations" REFFIELDS="id"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="conversationid_timecreated" UNIQUE="false" FIELDS="conversationid, timecreated"/>
+      </INDEXES>
+    </TABLE>
+    <TABLE NAME="message_conversations" COMMENT="Stores all message conversations">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="convhash" TYPE="char" LENGTH="40" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="convhash" UNIQUE="true" FIELDS="convhash"/>
+      </INDEXES>
+    </TABLE>
+    <TABLE NAME="message_conversation_members" COMMENT="Stores all members in a conversations">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="conversationid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="conversationid" TYPE="foreign" FIELDS="conversationid" REFTABLE="message_conversations" REFFIELDS="id"/>
+        <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
+      </KEYS>
+    </TABLE>
+    <TABLE NAME="message_user_actions" COMMENT="Stores all per-user actions on individual messages">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="messageid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="action" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
+        <KEY NAME="messageid" TYPE="foreign" FIELDS="messageid" REFTABLE="messages" REFFIELDS="id"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="userid_messageid_action" UNIQUE="true" FIELDS="userid, messageid, action"/>
+      </INDEXES>
+    </TABLE>
+    <TABLE NAME="notifications" COMMENT="Stores all notifications">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="useridfrom" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="useridto" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="subject" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="The message subject"/>
+        <FIELD NAME="fullmessage" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="fullmessageformat" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="fullmessagehtml" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="smallmessage" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="eventtype" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="contexturl" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="contexturlname" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="timeread" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="useridto" TYPE="foreign" FIELDS="useridto" REFTABLE="user" REFFIELDS="id"/>
+      </KEYS>
+    </TABLE>
     <TABLE NAME="message_contacts" COMMENT="Maintains lists of relationships between users">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
         <KEY NAME="primary" TYPE="primary" FIELDS="id" COMMENT="primary key of the table, please edit me"/>
       </KEYS>
     </TABLE>
-    <TABLE NAME="message_working" COMMENT="Lists all the messages and processors that need to be processed">
-      <FIELDS>
-        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" COMMENT="id of the table, please edit me"/>
-        <FIELD NAME="unreadmessageid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="message that still needs some processing (on message table)"/>
-        <FIELD NAME="processorid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The processor with processes the message"/>
-      </FIELDS>
-      <KEYS>
-        <KEY NAME="primary" TYPE="primary" FIELDS="id" COMMENT="primary key of the table, please edit me"/>
-      </KEYS>
-      <INDEXES>
-        <INDEX NAME="unreadmessageid_idx" UNIQUE="false" FIELDS="unreadmessageid" COMMENT="Index on unreadmessage id"/>
-      </INDEXES>
-    </TABLE>
     <TABLE NAME="files" COMMENT="description of files, content is stored in sha1 file pool">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
index 35ba7f9..55943d6 100644 (file)
@@ -990,6 +990,15 @@ $functions = array(
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
         'ajax' => true,
     ),
+    'core_message_mark_notification_read' => array(
+        'classname' => 'core_message_external',
+        'methodname' => 'mark_notification_read',
+        'classpath' => 'message/externallib.php',
+        'description' => 'Mark a single notification as read, trigger notification_viewed event.',
+        'type' => 'write',
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+        'ajax' => true,
+    ),
     'core_message_message_processor_config_form' => array(
         'classname' => 'core_message_external',
         'methodname' => 'message_processor_config_form',
index 78ebbee..08f2f75 100644 (file)
@@ -2020,5 +2020,185 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2018022800.03);
     }
 
+    if ($oldversion < 2018032200.01) {
+        // Define table 'messages' to be created.
+        $table = new xmldb_table('messages');
+
+        // Adding fields to table 'messages'.
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('useridfrom', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('conversationid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('subject', XMLDB_TYPE_TEXT, null, null, null, null, null);
+        $table->add_field('fullmessage', XMLDB_TYPE_TEXT, null, null, null, null, null);
+        $table->add_field('fullmessageformat', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, 0);
+        $table->add_field('fullmessagehtml', XMLDB_TYPE_TEXT, null, null, null, null, null);
+        $table->add_field('smallmessage', XMLDB_TYPE_TEXT, null, null, null, null, null);
+        $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+
+        // Adding keys to table 'messages'.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $table->add_key('useridfrom', XMLDB_KEY_FOREIGN, array('useridfrom'), 'user', array('id'));
+        $table->add_key('conversationid', XMLDB_KEY_FOREIGN, array('conversationid'), 'message_conversations', array('id'));
+
+        // Conditionally launch create table for 'messages'.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Define table 'message_conversations' to be created.
+        $table = new xmldb_table('message_conversations');
+
+        // Adding fields to table 'message_conversations'.
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+
+        // Adding keys to table 'message_conversations'.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+
+        // Conditionally launch create table for 'message_conversations'.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Define table 'message_conversation_members' to be created.
+        $table = new xmldb_table('message_conversation_members');
+
+        // Adding fields to table 'message_conversation_members'.
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('conversationid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+
+        // Adding keys to table 'message_conversation_members'.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $table->add_key('conversationid', XMLDB_KEY_FOREIGN, array('conversationid'), 'message_conversations', array('id'));
+        $table->add_key('userid', XMLDB_KEY_FOREIGN, array('userid'), 'user', array('id'));
+
+        // Conditionally launch create table for 'message_conversation_members'.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Define table 'message_user_actions' to be created.
+        $table = new xmldb_table('message_user_actions');
+
+        // Adding fields to table 'message_user_actions'.
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('messageid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('action', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+
+        // Adding keys to table 'message_user_actions'.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $table->add_key('userid', XMLDB_KEY_FOREIGN, array('userid'), 'user', array('id'));
+        $table->add_key('messageid', XMLDB_KEY_FOREIGN, array('messageid'), 'messages', array('id'));
+
+        // Conditionally launch create table for 'message_user_actions'.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Define table 'notifications' to be created.
+        $table = new xmldb_table('notifications');
+
+        // Adding fields to table 'notifications'.
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('useridfrom', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('useridto', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('subject', XMLDB_TYPE_TEXT, null, null, null, null, null);
+        $table->add_field('fullmessage', XMLDB_TYPE_TEXT, null, null, null, null, null);
+        $table->add_field('fullmessageformat', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, 0);
+        $table->add_field('fullmessagehtml', XMLDB_TYPE_TEXT, null, null, null, null, null);
+        $table->add_field('smallmessage', XMLDB_TYPE_TEXT, null, null, null, null, null);
+        $table->add_field('component', XMLDB_TYPE_CHAR, '100', null, null, null, null);
+        $table->add_field('eventtype', XMLDB_TYPE_CHAR, '100', null, null, null, null);
+        $table->add_field('contexturl', XMLDB_TYPE_TEXT, null, null, null, null, null);
+        $table->add_field('contexturlname', XMLDB_TYPE_TEXT, null, null, null, null, null);
+        $table->add_field('timeread', XMLDB_TYPE_INTEGER, '10', null, false, null, null);
+        $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+
+        // Adding keys to table 'notifications'.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $table->add_key('useridto', XMLDB_KEY_FOREIGN, array('useridto'), 'user', array('id'));
+
+        // Conditionally launch create table for 'notifications'.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2018032200.01);
+    }
+
+    if ($oldversion < 2018032200.04) {
+        // Define table 'message_conversations' to be updated.
+        $table = new xmldb_table('message_conversations');
+        $field = new xmldb_field('convhash', XMLDB_TYPE_CHAR, '40', null, XMLDB_NOTNULL, null, null, 'id');
+
+        // Conditionally launch add field 'convhash'.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Conditionally launch add index.
+        $index = new xmldb_index('convhash', XMLDB_INDEX_UNIQUE, array('convhash'));
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2018032200.04);
+    }
+
+    if ($oldversion < 2018032200.05) {
+        // Drop table that is no longer needed.
+        $table = new xmldb_table('message_working');
+        if ($dbman->table_exists($table)) {
+            $dbman->drop_table($table);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2018032200.05);
+    }
+
+    if ($oldversion < 2018032200.06) {
+        // Define table 'message_user_actions' to add an index to.
+        $table = new xmldb_table('message_user_actions');
+
+        // Conditionally launch add index.
+        $index = new xmldb_index('userid_messageid_action', XMLDB_INDEX_UNIQUE, array('userid, messageid, action'));
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2018032200.06);
+    }
+
+    if ($oldversion < 2018032200.07) {
+        // Define table 'messages' to add an index to.
+        $table = new xmldb_table('messages');
+
+        // Conditionally launch add index.
+        $index = new xmldb_index('conversationid_timecreated', XMLDB_INDEX_NOTUNIQUE, array('conversationid, timecreated'));
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2018032200.07);
+    }
+
+    if ($oldversion < 2018032700.00) {
+        // Update default search engine to search_simpledb if global search is disabled and there is no solr index defined.
+        if (empty($CFG->enableglobalsearch) && empty(get_config('search_solr', 'indexname'))) {
+            set_config('searchengine', 'simpledb');
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2018032700.00);
+    }
+
     return true;
 }
index 9528c10..d479b71 100644 (file)
@@ -4709,29 +4709,10 @@ function message_contact_link($userid, $linktype='add', $return=false, $script=n
 }
 
 /**
- * Get the users recent event notifications
- *
  * @deprecated since Moodle 3.2
- * @param object $user the current user
- * @param int $limitfrom can be used for paging
- * @param int $limitto can be used for paging
- * @return array
  */
 function message_get_recent_notifications($user, $limitfrom=0, $limitto=100) {
-    debugging('message_get_recent_notifications() is deprecated and is no longer used.', DEBUG_DEVELOPER);
-
-    global $DB;
-
-    $userfields = user_picture::fields('u', array('lastaccess'));
-    $sql = "SELECT mr.id AS message_read_id, $userfields, mr.notification, mr.smallmessage, mr.fullmessage, mr.fullmessagehtml, mr.fullmessageformat, mr.timecreated as timecreated, mr.contexturl, mr.contexturlname
-              FROM {message_read} mr
-                   JOIN {user} u ON u.id=mr.useridfrom
-             WHERE mr.useridto = :userid1 AND u.deleted = '0' AND mr.notification = :notification
-             ORDER BY mr.timecreated DESC";
-    $params = array('userid1' => $user->id, 'notification' => 1);
-
-    $notifications =  $DB->get_records_sql($sql, $params, $limitfrom, $limitto);
-    return $notifications;
+    throw new coding_exception('message_get_recent_notifications() can not be used any more.', DEBUG_DEVELOPER);
 }
 
 /**
@@ -4801,162 +4782,10 @@ function message_history_link($userid1, $userid2, $return=false, $keywords='', $
 }
 
 /**
- * Search a user's messages
- *
- * Returns a list of posts found using an array of search terms
- * eg   word  +word -word
- *
  * @deprecated since Moodle 3.2
- * @param array $searchterms an array of search terms (strings)
- * @param bool $fromme include messages from the user?
- * @param bool $tome include messages to the user?
- * @param mixed $courseid SITEID for admins searching all messages. Other behaviour not yet implemented
- * @param int $userid the user ID of the current user
- * @return mixed An array of messages or false if no matching messages were found
  */
 function message_search($searchterms, $fromme=true, $tome=true, $courseid='none', $userid=0) {
-    debugging('message_search() is deprecated and is no longer used.', DEBUG_DEVELOPER);
-
-    global $CFG, $USER, $DB;
-
-    // If user is searching all messages check they are allowed to before doing anything else.
-    if ($courseid == SITEID && !has_capability('moodle/site:readallmessages', context_system::instance())) {
-        print_error('accessdenied','admin');
-    }
-
-    // If no userid sent then assume current user.
-    if ($userid == 0) $userid = $USER->id;
-
-    // Some differences in SQL syntax.
-    if ($DB->sql_regex_supported()) {
-        $REGEXP    = $DB->sql_regex(true);
-        $NOTREGEXP = $DB->sql_regex(false);
-    }
-
-    $searchcond = array();
-    $params = array();
-    $i = 0;
-
-    // Preprocess search terms to check whether we have at least 1 eligible search term.
-    // If we do we can drop words around it like 'a'.
-    $dropshortwords = false;
-    foreach ($searchterms as $searchterm) {
-        if (strlen($searchterm) >= 2) {
-            $dropshortwords = true;
-        }
-    }
-
-    foreach ($searchterms as $searchterm) {
-        $i++;
-
-        $NOT = false; // Initially we aren't going to perform NOT LIKE searches, only MSSQL and Oracle.
-
-        if ($dropshortwords && strlen($searchterm) < 2) {
-            continue;
-        }
-        // Under Oracle and MSSQL, trim the + and - operators and perform simpler LIKE search.
-        if (!$DB->sql_regex_supported()) {
-            if (substr($searchterm, 0, 1) == '-') {
-                $NOT = true;
-            }
-            $searchterm = trim($searchterm, '+-');
-        }
-
-        if (substr($searchterm,0,1) == "+") {
-            $searchterm = substr($searchterm,1);
-            $searchterm = preg_quote($searchterm, '|');
-            $searchcond[] = "m.fullmessage $REGEXP :ss$i";
-            $params['ss'.$i] = "(^|[^a-zA-Z0-9])$searchterm([^a-zA-Z0-9]|$)";
-
-        } else if (substr($searchterm,0,1) == "-") {
-            $searchterm = substr($searchterm,1);
-            $searchterm = preg_quote($searchterm, '|');
-            $searchcond[] = "m.fullmessage $NOTREGEXP :ss$i";
-            $params['ss'.$i] = "(^|[^a-zA-Z0-9])$searchterm([^a-zA-Z0-9]|$)";
-
-        } else {
-            $searchcond[] = $DB->sql_like("m.fullmessage", ":ss$i", false, true, $NOT);
-            $params['ss'.$i] = "%$searchterm%";
-        }
-    }
-
-    if (empty($searchcond)) {
-        $searchcond = " ".$DB->sql_like('m.fullmessage', ':ss1', false);
-        $params['ss1'] = "%";
-    } else {
-        $searchcond = implode(" AND ", $searchcond);
-    }
-
-    // There are several possibilities
-    // 1. courseid = SITEID : The admin is searching messages by all users
-    // 2. courseid = ??     : A teacher is searching messages by users in
-    //                        one of their courses - currently disabled
-    // 3. courseid = none   : User is searching their own messages;
-    //    a.  Messages from user
-    //    b.  Messages to user
-    //    c.  Messages to and from user
-
-    if ($fromme && $tome) {
-        $searchcond .= " AND ((useridto = :useridto AND timeusertodeleted = 0) OR
-            (useridfrom = :useridfrom AND timeuserfromdeleted = 0))";
-        $params['useridto'] = $userid;
-        $params['useridfrom'] = $userid;
-    } else if ($fromme) {
-        $searchcond .= " AND (useridfrom = :useridfrom AND timeuserfromdeleted = 0)";
-        $params['useridfrom'] = $userid;
-    } else if ($tome) {
-        $searchcond .= " AND (useridto = :useridto AND timeusertodeleted = 0)";
-        $params['useridto'] = $userid;
-    }
-    if ($courseid == SITEID) { // Admin is searching all messages.
-        $m_read   = $DB->get_records_sql("SELECT m.id, m.useridto, m.useridfrom, m.smallmessage, m.fullmessage, m.timecreated
-                                            FROM {message_read} m
-                                           WHERE $searchcond", $params, 0, MESSAGE_SEARCH_MAX_RESULTS);
-        $m_unread = $DB->get_records_sql("SELECT m.id, m.useridto, m.useridfrom, m.smallmessage, m.fullmessage, m.timecreated
-                                            FROM {message} m
-                                           WHERE $searchcond", $params, 0, MESSAGE_SEARCH_MAX_RESULTS);
-
-    } else if ($courseid !== 'none') {
-        // This has not been implemented due to security concerns.
-        $m_read   = array();
-        $m_unread = array();
-
-    } else {
-
-        if ($fromme and $tome) {
-            $searchcond .= " AND (m.useridfrom=:userid1 OR m.useridto=:userid2)";
-            $params['userid1'] = $userid;
-            $params['userid2'] = $userid;
-
-        } else if ($fromme) {
-            $searchcond .= " AND m.useridfrom=:userid";
-            $params['userid'] = $userid;
-
-        } else if ($tome) {
-            $searchcond .= " AND m.useridto=:userid";
-            $params['userid'] = $userid;
-        }
-
-        $m_read   = $DB->get_records_sql("SELECT m.id, m.useridto, m.useridfrom, m.smallmessage, m.fullmessage, m.timecreated
-                                            FROM {message_read} m
-                                           WHERE $searchcond", $params, 0, MESSAGE_SEARCH_MAX_RESULTS);
-        $m_unread = $DB->get_records_sql("SELECT m.id, m.useridto, m.useridfrom, m.smallmessage, m.fullmessage, m.timecreated
-                                            FROM {message} m
-                                           WHERE $searchcond", $params, 0, MESSAGE_SEARCH_MAX_RESULTS);
-
-    }
-
-    /// The keys may be duplicated in $m_read and $m_unread so we can't
-    /// do a simple concatenation
-    $messages = array();
-    foreach ($m_read as $m) {
-        $messages[] = $m;
-    }
-    foreach ($m_unread as $m) {
-        $messages[] = $m;
-    }
-
-    return (empty($messages)) ? false : $messages;
+    throw new coding_exception('message_search() can not be used any more.', DEBUG_DEVELOPER);
 }
 
 /**
@@ -5070,63 +4899,10 @@ function message_get_fragment($message, $keywords) {
 }
 
 /**
- * Retrieve the messages between two users
- *
  * @deprecated since Moodle 3.2
- * @param object $user1 the current user
- * @param object $user2 the other user
- * @param int $limitnum the maximum number of messages to retrieve
- * @param bool $viewingnewmessages are we currently viewing new messages?
  */
 function message_get_history($user1, $user2, $limitnum=0, $viewingnewmessages=false) {
-    debugging('message_get_history() is deprecated and is no longer used.', DEBUG_DEVELOPER);
-
-    global $DB, $CFG;
-
-    $messages = array();
-
-    //we want messages sorted oldest to newest but if getting a subset of messages we need to sort
-    //desc to get the last $limitnum messages then flip the order in php
-    $sort = 'asc';
-    if ($limitnum>0) {
-        $sort = 'desc';
-    }
-
-    $notificationswhere = null;
-    //we have just moved new messages to read. If theyre here to see new messages dont hide notifications
-    if (!$viewingnewmessages && $CFG->messaginghidereadnotifications) {
-        $notificationswhere = 'AND notification=0';
-    }
-
-    //prevent notifications of your own actions appearing in your own message history
-    $ownnotificationwhere = ' AND NOT (useridfrom=? AND notification=1)';
-
-    $sql = "((useridto = ? AND useridfrom = ? AND timeusertodeleted = 0) OR
-        (useridto = ? AND useridfrom = ? AND timeuserfromdeleted = 0))";
-    if ($messages_read = $DB->get_records_select('message_read', $sql . $notificationswhere . $ownnotificationwhere,
-        array($user1->id, $user2->id, $user2->id, $user1->id, $user1->id),
-        "timecreated $sort", '*', 0, $limitnum)) {
-        foreach ($messages_read as $message) {
-            $messages[] = $message;
-        }
-    }
-    if ($messages_new = $DB->get_records_select('message', $sql . $ownnotificationwhere,
-        array($user1->id, $user2->id, $user2->id, $user1->id, $user1->id),
-        "timecreated $sort", '*', 0, $limitnum)) {
-        foreach ($messages_new as $message) {
-            $messages[] = $message;
-        }
-    }
-
-    $result = core_collator::asort_objects_by_property($messages, 'timecreated', core_collator::SORT_NUMERIC);
-
-    //if we only want the last $limitnum messages
-    $messagecount = count($messages);
-    if ($limitnum > 0 && $messagecount > $limitnum) {
-        $messages = array_slice($messages, $messagecount - $limitnum, $limitnum, true);
-    }
-
-    return $messages;
+    throw new coding_exception('message_get_history() can not be used any more.', DEBUG_DEVELOPER);
 }
 
 /**
@@ -5197,9 +4973,9 @@ function message_get_contact_block_link($incontactlist, $isblocked, $contact, $s
  */
 function message_mark_messages_read($touserid, $fromuserid) {
     debugging('message_mark_messages_read() is deprecated and is no longer used, please use
-        \core_message\api::mark_all_read_for_user() instead.', DEBUG_DEVELOPER);
+        \core_message\api::mark_all_messages_as_read() instead.', DEBUG_DEVELOPER);
 
-    \core_message\api::mark_all_read_for_user($touserid, $fromuserid);
+    \core_message\api::mark_all_messages_as_read($touserid, $fromuserid);
 }
 
 /**
@@ -6035,144 +5811,11 @@ function prevent_form_autofill_password() {
 }
 
 /**
- * Get the users recent conversations meaning all the people they've recently
- * sent or received a message from plus the most recent message sent to or received from each other user
- *
  * @deprecated since Moodle 3.3 MDL-57370
- * @param object|int $userorid the current user or user id
- * @param int $limitfrom can be used for paging
- * @param int $limitto can be used for paging
- * @return array
  */
 function message_get_recent_conversations($userorid, $limitfrom = 0, $limitto = 100) {
-    global $DB;
-
-    debugging('message_get_recent_conversations() is deprecated. Please use \core_message\api::get_conversations() instead.', DEBUG_DEVELOPER);
-
-    if (is_object($userorid)) {
-        $user = $userorid;
-    } else {
-        $userid = $userorid;
-        $user = new stdClass();
-        $user->id = $userid;
-    }
-
-    $userfields = user_picture::fields('otheruser', array('lastaccess'));
-
-    // This query retrieves the most recent message received from or sent to
-    // seach other user.
-    //
-    // If two messages have the same timecreated, we take the one with the
-    // larger id.
-    //
-    // There is a separate query for read and unread messages as they are stored
-    // in different tables. They were originally retrieved in one query but it
-    // was so large that it was difficult to be confident in its correctness.
-    $uniquefield = $DB->sql_concat('message.useridfrom', "'-'", 'message.useridto');
-    $sql = "SELECT $uniquefield, $userfields,
-                   message.id as mid, message.notification, message.useridfrom, message.useridto,
-                   message.smallmessage, message.fullmessage, message.fullmessagehtml,
-                   message.fullmessageformat, message.timecreated,
-                   contact.id as contactlistid, contact.blocked
-              FROM {message_read} message
-              JOIN (
-                        SELECT MAX(id) AS messageid,
-                               matchedmessage.useridto,
-                               matchedmessage.useridfrom
-                         FROM {message_read} matchedmessage
-                   INNER JOIN (
-                               SELECT MAX(recentmessages.timecreated) timecreated,
-                                      recentmessages.useridfrom,
-                                      recentmessages.useridto
-                                 FROM {message_read} recentmessages
-                                WHERE (
-                                      (recentmessages.useridfrom = :userid1 AND recentmessages.timeuserfromdeleted = 0) OR
-                                      (recentmessages.useridto = :userid2   AND recentmessages.timeusertodeleted = 0)
-                                      )
-                             GROUP BY recentmessages.useridfrom, recentmessages.useridto
-                              ) recent ON matchedmessage.useridto     = recent.useridto
-                           AND matchedmessage.useridfrom   = recent.useridfrom
-                           AND matchedmessage.timecreated  = recent.timecreated
-                           WHERE (
-                                 (matchedmessage.useridfrom = :userid6 AND matchedmessage.timeuserfromdeleted = 0) OR
-                                 (matchedmessage.useridto = :userid7   AND matchedmessage.timeusertodeleted = 0)
-                                 )
-                      GROUP BY matchedmessage.useridto, matchedmessage.useridfrom
-                   ) messagesubset ON messagesubset.messageid = message.id
-              JOIN {user} otheruser ON (message.useridfrom = :userid4 AND message.useridto = otheruser.id)
-                OR (message.useridto   = :userid5 AND message.useridfrom   = otheruser.id)
-         LEFT JOIN {message_contacts} contact ON contact.userid  = :userid3 AND contact.contactid = otheruser.id
-             WHERE otheruser.deleted = 0 AND message.notification = 0
-          ORDER BY message.timecreated DESC";
-    $params = array(
-        'userid1' => $user->id,
-        'userid2' => $user->id,
-        'userid3' => $user->id,
-        'userid4' => $user->id,
-        'userid5' => $user->id,
-        'userid6' => $user->id,
-        'userid7' => $user->id
-    );
-    $read = $DB->get_records_sql($sql, $params, $limitfrom, $limitto);
-
-    // We want to get the messages that have not been read. These are stored in the 'message' table. It is the
-    // exact same query as the one above, except for the table we are querying. So, simply replace references to
-    // the 'message_read' table with the 'message' table.
-    $sql = str_replace('{message_read}', '{message}', $sql);
-    $unread = $DB->get_records_sql($sql, $params, $limitfrom, $limitto);
-
-    $unreadcountssql = 'SELECT useridfrom, count(*) as count
-                          FROM {message}
-                         WHERE useridto = :userid
-                           AND timeusertodeleted = 0
-                           AND notification = 0
-                      GROUP BY useridfrom';
-    $unreadcounts = $DB->get_records_sql($unreadcountssql, array('userid' => $user->id));
-
-    // Union the 2 result sets together looking for the message with the most
-    // recent timecreated for each other user.
-    // $conversation->id (the array key) is the other user's ID.
-    $conversations = array();
-    $conversation_arrays = array($unread, $read);
-    foreach ($conversation_arrays as $conversation_array) {
-        foreach ($conversation_array as $conversation) {
-            // Only consider it unread if $user has unread messages.
-            if (isset($unreadcounts[$conversation->useridfrom])) {
-                $conversation->isread = 0;
-                $conversation->unreadcount = $unreadcounts[$conversation->useridfrom]->count;
-            } else {
-                $conversation->isread = 1;
-            }
-
-            if (!isset($conversations[$conversation->id])) {
-                $conversations[$conversation->id] = $conversation;
-            } else {
-                $current = $conversations[$conversation->id];
-                // We need to maintain the isread and unreadcount values from existing
-                // parts of the conversation if we're replacing it.
-                $conversation->isread = ($conversation->isread && $current->isread);
-                if (isset($current->unreadcount) && !isset($conversation->unreadcount)) {
-                    $conversation->unreadcount = $current->unreadcount;
-                }
-
-                if ($current->timecreated < $conversation->timecreated) {
-                    $conversations[$conversation->id] = $conversation;
-                } else if ($current->timecreated == $conversation->timecreated) {
-                    if ($current->mid < $conversation->mid) {
-                        $conversations[$conversation->id] = $conversation;
-                    }
-                }
-            }
-        }
-    }
-
-    // Sort the conversations by $conversation->timecreated, newest to oldest
-    // There may be multiple conversations with the same timecreated
-    // The conversations array contains both read and unread messages (different tables) so sorting by ID won't work
-    $result = core_collator::asort_objects_by_property($conversations, 'timecreated', core_collator::SORT_NUMERIC);
-    $conversations = array_reverse($conversations);
-
-    return $conversations;
+    throw new coding_exception('message_get_recent_conversations() can not be used any more. ' .
+        'Please use \core_message\api::get_conversations() instead.', DEBUG_DEVELOPER);
 }
 
 /**
@@ -6548,15 +6191,16 @@ function question_add_tops($categories, $pcontexts) {
             DEBUG_DEVELOPER);
 
     $topcats = array();
-    foreach ($pcontexts as $context) {
-        $topcat = question_get_top_category($context, true);
+    foreach ($pcontexts as $contextid) {
+        $topcat = question_get_top_category($contextid, true);
+        $context = context::instance_by_id($contextid);
 
         $newcat = new stdClass();
-        $newcat->id = "{$topcat->id},$context";
-        $newcat->name = get_string('top');
+        $newcat->id = "{$topcat->id},$contextid";
+        $newcat->name = get_string('topfor', 'question', $context->get_context_name(false));
         $newcat->parent = 0;
-        $newcat->contextid = $context;
-        $topcats["{$topcat->id},$context"] = $newcat;
+        $newcat->contextid = $contextid;
+        $topcats["{$topcat->id},$contextid"] = $newcat;
     }
     // Put topcats in at beginning of array - they'll be sorted into different contexts later.
     return array_merge($topcats, $categories);
@@ -6576,3 +6220,185 @@ function question_is_only_toplevel_category_in_context($categoryid) {
 
     return question_is_only_child_of_top_category_in_context($categoryid);
 }
+
+/**
+ * Moves messages from a particular user from the message table (unread messages) to message_read
+ * This is typically only used when a user is deleted
+ *
+ * @param object $userid User id
+ * @return boolean success
+ * @deprecated since Moodle 3.5
+ */
+function message_move_userfrom_unread2read($userid) {
+    debugging('message_move_userfrom_unread2read() is deprecated and is no longer used.', DEBUG_DEVELOPER);
+
+    global $DB;
+
+    // Move all unread messages from message table to message_read.
+    if ($messages = $DB->get_records_select('message', 'useridfrom = ?', array($userid), 'timecreated')) {
+        foreach ($messages as $message) {
+            message_mark_message_read($message, 0); // Set timeread to 0 as the message was never read.
+        }
+    }
+    return true;
+}
+
+/**
+ * Retrieve users blocked by $user1
+ *
+ * @param object $user1 the user whose messages are being viewed
+ * @param object $user2 the user $user1 is talking to. If they are being blocked
+ *                      they will have a variable called 'isblocked' added to their user object
+ * @return array the users blocked by $user1
+ * @deprecated since Moodle 3.5
+ */
+function message_get_blocked_users($user1=null, $user2=null) {
+    debugging('message_get_blocked_users() is deprecated, please use \core_message\api::get_blocked_users() instead.',
+        DEBUG_DEVELOPER);
+
+    global $USER;
+
+    if (empty($user1)) {
+        $user1 = new stdClass();
+        $user1->id = $USER->id;
+    }
+
+    return \core_message\api::get_blocked_users($user1->id);
+}
+
+/**
+ * Retrieve $user1's contacts (online, offline and strangers)
+ *
+ * @param object $user1 the user whose messages are being viewed
+ * @param object $user2 the user $user1 is talking to. If they are a contact
+ *                      they will have a variable called 'iscontact' added to their user object
+ * @return array containing 3 arrays. array($onlinecontacts, $offlinecontacts, $strangers)
+ * @deprecated since Moodle 3.5
+ */
+function message_get_contacts($user1=null, $user2=null) {
+    debugging('message_get_contacts() is deprecated and is no longer used.', DEBUG_DEVELOPER);
+
+    global $DB, $CFG, $USER;
+
+    if (empty($user1)) {
+        $user1 = $USER;
+    }
+
+    if (!empty($user2)) {
+        $user2->iscontact = false;
+    }
+
+    $timetoshowusers = 300; // Seconds default.
+    if (isset($CFG->block_online_users_timetosee)) {
+        $timetoshowusers = $CFG->block_online_users_timetosee * 60;
+    }
+
+    // Rime which a user is counting as being active since.
+    $timefrom = time() - $timetoshowusers;
+
+    // People in our contactlist who are online.
+    $onlinecontacts  = array();
+    // People in our contactlist who are offline.
+    $offlinecontacts = array();
+    // People who are not in our contactlist but have sent us a message.
+    $strangers       = array();
+
+    // Get all in our contact list who are not blocked in our and count messages we have waiting from each of them.
+    $rs = \core_message\api::get_contacts_with_unread_message_count($user1->id);
+    foreach ($rs as $rd) {
+        if ($rd->lastaccess >= $timefrom) {
+            // They have been active recently, so are counted online.
+            $onlinecontacts[] = $rd;
+
+        } else {
+            $offlinecontacts[] = $rd;
+        }
+
+        if (!empty($user2) && $user2->id == $rd->id) {
+            $user2->iscontact = true;
+        }
+    }
+
+    // Get messages from anyone who isn't in our contact list and count the number of messages we have from each of them.
+    $rs = \core_message\api::get_non_contacts_with_unread_message_count($user1->id);
+    // Add user id as array index, so supportuser and noreply user don't get duplicated (if they are real users).
+    foreach ($rs as $rd) {
+        $strangers[$rd->id] = $rd;
+    }
+
+    // Add noreply user and support user to the list, if they don't exist.
+    $supportuser = core_user::get_support_user();
+    if (!isset($strangers[$supportuser->id]) && !$supportuser->deleted) {
+        $supportuser->messagecount = message_count_unread_messages($USER, $supportuser);
+        if ($supportuser->messagecount > 0) {
+            $strangers[$supportuser->id] = $supportuser;
+        }
+    }
+
+    $noreplyuser = core_user::get_noreply_user();
+    if (!isset($strangers[$noreplyuser->id]) && !$noreplyuser->deleted) {
+        $noreplyuser->messagecount = message_count_unread_messages($USER, $noreplyuser);
+        if ($noreplyuser->messagecount > 0) {
+            $strangers[$noreplyuser->id] = $noreplyuser;
+        }
+    }
+
+    return array($onlinecontacts, $offlinecontacts, $strangers);
+}
+
+/**
+ * Mark a single message as read
+ *
+ * @param stdClass $message An object with an object property ie $message->id which is an id in the message table
+ * @param int $timeread the timestamp for when the message should be marked read. Usually time().
+ * @param bool $messageworkingempty Is the message_working table already confirmed empty for this message?
+ * @return int the ID of the message in the messags table
+ * @deprecated since Moodle 3.5
+ */
+function message_mark_message_read($message, $timeread, $messageworkingempty=false) {
+    debugging('message_mark_message_read() is deprecated, please use \core_message\api::mark_message_as_read()
+        or \core_message\api::mark_notification_as_read().', DEBUG_DEVELOPER);
+
+    if (!empty($message->notification)) {
+        \core_message\api::mark_notification_as_read($message, $timeread);
+    } else {
+        \core_message\api::mark_message_as_read($message->useridto, $message, $timeread);
+    }
+
+    return $message->id;
+}
+
+
+/**
+ * Checks if a user can delete a message.
+ *
+ * @param stdClass $message the message to delete
+ * @param string $userid the user id of who we want to delete the message for (this may be done by the admin
+ *  but will still seem as if it was by the user)
+ * @return bool Returns true if a user can delete the message, false otherwise.
+ * @deprecated since Moodle 3.5
+ */
+function message_can_delete_message($message, $userid) {
+    debugging('message_can_delete_message() is deprecated, please use \core_message\api::can_delete_message() instead.',
+        DEBUG_DEVELOPER);
+
+    return \core_message\api::can_delete_message($userid, $message->id);
+}
+
+/**
+ * Deletes a message.
+ *
+ * This function does not verify any permissions.
+ *
+ * @param stdClass $message the message to delete
+ * @param string $userid the user id of who we want to delete the message for (this may be done by the admin
+ *  but will still seem as if it was by the user)
+ * @return bool
+ * @deprecated since Moodle 3.5
+ */
+function message_delete_message($message, $userid) {
+    debugging('message_delete_message() is deprecated, please use \core_message\api::delete_message() instead.',
+        DEBUG_DEVELOPER);
+
+    return \core_message\api::delete_message($userid, $message->id);
+}
index 48debee..c333222 100644 (file)
@@ -107,4 +107,18 @@ class mariadb_native_moodle_database extends mysqli_native_moodle_database {
         }
         return true;
     }
+
+    /**
+     * Does this mariadb instance support fulltext indexes?
+     *
+     * @return bool
+     */
+    public function is_fulltext_search_supported() {
+        $info = $this->get_server_info();
+
+        if (version_compare($info['version'], '10.0.5', '>=')) {
+            return true;
+        }
+        return false;
+    }
 }
index a16b933..77d0b98 100644 (file)
@@ -2687,4 +2687,14 @@ abstract class moodle_database {
     public function perf_get_queries_time() {
         return $this->queriestime;
     }
+
+    /**
+     * Whether the database is able to support full-text search or not.
+     *
+     * @return bool
+     */
+    public function is_fulltext_search_supported() {
+        // No support unless specified.
+        return false;
+    }
 }
index 05c6c43..015d3a6 100644 (file)
@@ -1989,4 +1989,18 @@ class mysqli_native_moodle_database extends moodle_database {
             $this->change_database_structure("ALTER TABLE {$prefix}$tablename $rowformat");
         }
     }
+
+    /**
+     * Does this mysql instance support fulltext indexes?
+     *
+     * @return bool
+     */
+    public function is_fulltext_search_supported() {
+        $info = $this->get_server_info();
+
+        if (version_compare($info['version'], '5.6.4', '>=')) {
+            return true;
+        }
+        return false;
+    }
 }
index ae75501..97d9490 100644 (file)
@@ -1497,4 +1497,13 @@ class pgsql_native_moodle_database extends moodle_database {
     private function trim_quotes($str) {
         return trim(trim($str), "'\"");
     }
+
+    /**
+     * Postgresql supports full-text search indexes.
+     *
+     * @return bool
+     */
+    public function is_fulltext_search_supported() {
+        return true;
+    }
 }
index d1d27f1..9174f70 100644 (file)
@@ -1586,4 +1586,26 @@ class sqlsrv_native_moodle_database extends moodle_database {
         $result = sqlsrv_rollback($this->sqlsrv);
         $this->query_end($result);
     }
+
+    /**
+     * Is fulltext search enabled?.
+     *
+     * @return bool
+     */
+    public function is_fulltext_search_supported() {
+        global $CFG;
+
+        $sql = "SELECT FULLTEXTSERVICEPROPERTY('IsFullTextInstalled')";
+        $this->query_start($sql, null, SQL_QUERY_AUX);
+        $result = sqlsrv_query($this->sqlsrv, $sql);
+        $this->query_end($result);
+        if ($result) {
+            if ($row = sqlsrv_fetch_array($result)) {
+                $property = (bool)reset($row);
+            }
+        }
+        $this->free_result($result);
+
+        return !empty($property);
+    }
 }
index a7d073c..c8db3c5 100644 (file)
Binary files a/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-debug.js and b/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-debug.js differ
index 015f2f1..5b9e915 100644 (file)
Binary files a/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-min.js and b/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-min.js differ
index a7d073c..c8db3c5 100644 (file)
Binary files a/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button.js and b/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button.js differ
index 8d3705b..f33bc87 100644 (file)
@@ -315,7 +315,7 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
      * @return {boolean} whether or not the parameter node exists within the editor.
      */
     _stopAtContentEditableFilter: function(node) {
-        this.editor.contains(node);
+        return this.editor.contains(node);
     },
 
     /**
@@ -409,12 +409,13 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
     _getSuitableTableCell: function() {
         var targetcell = null,
             host = this.get('host');
+        var stopAtContentEditableFilter = Y.bind(this._stopAtContentEditableFilter, this);
 
         host.getSelectedNodes().some(function(node) {
-            if (node.ancestor('td, th, caption', true, this._stopAtContentEditableFilter)) {
+            if (node.ancestor('td, th, caption', true, stopAtContentEditableFilter)) {
                 targetcell = node;
 
-                var caption = node.ancestor('caption', true, this._stopAtContentEditableFilter);
+                var caption = node.ancestor('caption', true, stopAtContentEditableFilter);
                 if (caption) {
                     var table = caption.get('parentNode');
                     if (table) {
diff --git a/lib/editor/tinymce/plugins/ctrlhelp/classes/privacy/provider.php b/lib/editor/tinymce/plugins/ctrlhelp/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..7402ad1
--- /dev/null
@@ -0,0 +1,46 @@
+<?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/>.
+
+/**
+ * Privacy Subsystem implementation for tinymce_ctrlhelp.
+ *
+ * @package    tinymce_ctrlhelp
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tinymce_ctrlhelp\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for tinymce_ctrlhelp implementing null_provider.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
index 8c34644..4e718d8 100644 (file)
@@ -26,3 +26,4 @@ $string['pluginname'] = 'CTRL+right click helper';
 
 /* All lang strings used from TinyMCE JavaScript code must be named 'pluginname:stringname', no need to create langs/en_dlg.js */
 $string['ctrlhelp:desc'] = 'CTRL + right click for more options';
+$string['privacy:metadata'] = 'The Ctrl+right TinyMCE plugin does not store any personal data.';
diff --git a/lib/editor/tinymce/plugins/managefiles/classes/privacy/provider.php b/lib/editor/tinymce/plugins/managefiles/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..b65c101
--- /dev/null
@@ -0,0 +1,46 @@
+<?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/>.
+
+/**
+ * Privacy Subsystem implementation for tinymce_managefiles.
+ *
+ * @package    tinymce_managefiles
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tinymce_managefiles\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for tinymce_managefiles implementing null_provider.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
index e1b7d06..c42f77f 100644 (file)
@@ -30,4 +30,5 @@ $string['hasmissingfiles'] = 'Warning! The following files that are referenced i
 $string['refreshfiles'] = 'Refresh the lists of missing and unused files';
 $string['unusedfilesheader'] = 'Unused files';
 $string['unusedfilesdesc'] = 'The following embedded files are not used in the text area:';
-$string['deleteselected'] = 'Delete selected files';
\ No newline at end of file
+$string['deleteselected'] = 'Delete selected files';
+$string['privacy:metadata'] = 'The Manage embedded files TinyMCE plugin does not store any personal data.';
diff --git a/lib/editor/tinymce/plugins/moodleemoticon/classes/privacy/provider.php b/lib/editor/tinymce/plugins/moodleemoticon/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..0327c13
--- /dev/null
@@ -0,0 +1,46 @@
+<?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/>.
+
+/**
+ * Privacy Subsystem implementation for tinymce_moodleemoticon.
+ *
+ * @package    tinymce_moodleemoticon
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tinymce_moodleemoticon\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for tinymce_moodleemoticon implementing null_provider.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
index c8fc2d0..e6e3b2a 100644 (file)
@@ -28,3 +28,4 @@ $string['requireemoticon_desc'] = 'If enabled the emoticon button is visible onl
 
 /* All lang strings used from TinyMCE JavaScript code must be named 'pluginname:stringname', no need to create langs/en_dlg.js */
 $string['moodleemoticon:desc'] = 'Insert emoticon';
+$string['privacy:metadata'] = 'The Moodle Emoticon TinyMCE plugin does not store any personal data.';
diff --git a/lib/editor/tinymce/plugins/moodleimage/classes/privacy/provider.php b/lib/editor/tinymce/plugins/moodleimage/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..f613a7d
--- /dev/null
@@ -0,0 +1,46 @@
+<?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/>.
+
+/**
+ * Privacy Subsystem implementation for tinymce_moodleimage.
+ *
+ * @package    tinymce_moodleimage
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tinymce_moodleimage\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for tinymce_moodleimage implementing null_provider.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
index b312b04..3cd8cc6 100644 (file)
@@ -26,3 +26,4 @@ $string['pluginname'] = 'Insert image';
 
 /* This plugin abuses strings from the standard TinyMCE advimage plugin, there is no need to duplicate them here. */
 $string['moodleimage:browseimage'] = 'Find or upload an image...';
+$string['privacy:metadata'] = 'The TinyMCE Moodle image plugin does not store any personal data.';
diff --git a/lib/editor/tinymce/plugins/moodlemedia/classes/privacy/provider.php b/lib/editor/tinymce/plugins/moodlemedia/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..66be93d
--- /dev/null
@@ -0,0 +1,46 @@
+<?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/>.
+
+/**
+ * Privacy Subsystem implementation for tinymce_moodlemedia.
+ *
+ * @package    tinymce_moodlemedia
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tinymce_moodlemedia\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for tinymce_moodlemedia implementing null_provider.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
index c70193e..6404191 100644 (file)
@@ -28,3 +28,4 @@ $string['pluginname'] = 'Insert media';
 /* All lang strings used from TinyMCE JavaScript code must be named 'pluginname:stringname', no need to create langs/en_dlg.js */
 $string['moodlemedia:browsemedia'] = 'Find or upload a sound, video or applet...';
 $string['moodlemedia:desc'] = 'Insert Moodle media';
+$string['privacy:metadata'] = 'The TinyMCE Moodle Media plugin does not store any personal data.';
diff --git a/lib/editor/tinymce/plugins/moodlenolink/classes/privacy/provider.php b/lib/editor/tinymce/plugins/moodlenolink/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..955852a
--- /dev/null
@@ -0,0 +1,46 @@
+<?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/>.
+
+/**
+ * Privacy Subsystem implementation for tinymce_moodlenolink.
+ *
+ * @package    tinymce_moodlenolink
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tinymce_moodlenolink\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for tinymce_moodlenolink implementing null_provider.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
index 49df0a9..8eb325a 100644 (file)
@@ -26,3 +26,4 @@ $string['pluginname'] = 'Prevent automatic linking';
 
 /* All lang strings used from TinyMCE JavaScript code must be named 'pluginname:stringname', no need to create langs/en_dlg.js */
 $string['moodlenolink:desc'] = 'Prevent automatic linking';
+$string['privacy:metadata'] = 'The TinyMCE Automatic link prevention plugin does not store any personal data.';
diff --git a/lib/editor/tinymce/plugins/pdw/classes/privacy/provider.php b/lib/editor/tinymce/plugins/pdw/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..5f3f50e
--- /dev/null
@@ -0,0 +1,46 @@
+<?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/>.
+
+/**
+ * Privacy Subsystem implementation for tinymce_pdw.
+ *
+ * @package    tinymce_pdw
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tinymce_pdw\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for tinymce_pdw implementing null_provider.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
index d057f79..1a6ceff 100644 (file)
@@ -26,3 +26,4 @@ $string['pluginname'] = 'Toolbar Toggle';
 
 /* All lang strings used from TinyMCE JavaScript code must be named 'pluginname:stringname', no need to create langs/en_dlg.js */
 $string['pdw:desc'] = 'Toolbar Toggle';
+$string['privacy:metadata'] = 'The TinyMCE Toolebar toggle plugin does not store any personal data.';
diff --git a/lib/editor/tinymce/plugins/spellchecker/classes/privacy/provider.php b/lib/editor/tinymce/plugins/spellchecker/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..01909cd
--- /dev/null
@@ -0,0 +1,46 @@
+<?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/>.
+
+/**
+ * Privacy Subsystem implementation for tinymce_spellchecker.
+ *
+ * @package    tinymce_spellchecker
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tinymce_spellchecker\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for tinymce_spellchecker implementing null_provider.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
index ca651ea..f66926c 100644 (file)
@@ -25,3 +25,4 @@
 $string['pluginname'] = 'Legacy spell checker';
 
 /* This plugin abuses strings from the standard TinyMCE spellchecker plugin, there is no need to duplicate them here. */
+$string['privacy:metadata'] = 'The TinyMCE Legacy spell checker plugin does not store any personal data.';
diff --git a/lib/editor/tinymce/plugins/wrap/classes/privacy/provider.php b/lib/editor/tinymce/plugins/wrap/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..da99906
--- /dev/null
@@ -0,0 +1,46 @@
+<?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/>.
+
+/**
+ * Privacy Subsystem implementation for tinymce_wrap.
+ *
+ * @package    tinymce_wrap
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tinymce_wrap\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for tinymce_wrap implementing null_provider.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
index eedf6d0..293623b 100644 (file)
@@ -26,3 +26,4 @@ $string['pluginname'] = 'Wrap';
 
 /* All lang strings used from TinyMCE JavaScript code must be named 'pluginname:stringname', no need to create langs/en_dlg.js */
 $string['moodlewrap:desc'] = 'Wrap';
+$string['privacy:metadata'] = 'The TinyMCE Wrap plugin does not store any personal data.';
index ec42975..cc96a9a 100644 (file)
@@ -1175,6 +1175,9 @@ class external_settings {
     /** @var string In which file should the urls be rewritten */
     private $file = 'webservice/pluginfile.php';
 
+    /** @var string The session lang */
+    private $lang = '';
+
     /**
      * Constructor - protected - can not be instanciated
      */
@@ -1277,6 +1280,24 @@ class external_settings {
     public function get_file() {
         return $this->file;
     }
+
+    /**
+     * Set lang
+     *
+     * @param string $lang
+     */
+    public function set_lang($lang) {
+        $this->lang = $lang;
+    }
+
+    /**
+     * Get lang
+     *
+     * @return string
+     */
+    public function get_lang() {
+        return $this->lang;
+    }
 }
 
 /**
index d575cbc..0b5d300 100644 (file)
@@ -127,33 +127,53 @@ function message_send($eventdata) {
         $userstate = 'loggedoff';
     }
 
-    // Create the message object
-    $savemessage = new stdClass();
-    $savemessage->courseid          = $eventdata->courseid;
-    $savemessage->useridfrom        = $eventdata->userfrom->id;
-    $savemessage->useridto          = $eventdata->userto->id;
-    $savemessage->subject           = $eventdata->subject;
-    $savemessage->fullmessage       = $eventdata->fullmessage;
-    $savemessage->fullmessageformat = $eventdata->fullmessageformat;
-    $savemessage->fullmessagehtml   = $eventdata->fullmessagehtml;
-    $savemessage->smallmessage      = $eventdata->smallmessage;
-    $savemessage->notification      = $eventdata->notification;
-    $savemessage->eventtype         = $eventdata->name;
-    $savemessage->component         = $eventdata->component;
-
-    if (!empty($eventdata->contexturl)) {
-        $savemessage->contexturl = (string)$eventdata->contexturl;
-    } else {
-        $savemessage->contexturl = null;
-    }
+    // Check if we are creating a notification or message.
+    if ($eventdata->notification) {
+        $table = 'notifications';
+
+        $tabledata = new stdClass();
+        $tabledata->useridfrom = $eventdata->userfrom->id;
+        $tabledata->useridto = $eventdata->userto->id;
+        $tabledata->subject = $eventdata->subject;
+        $tabledata->fullmessage = $eventdata->fullmessage;
+        $tabledata->fullmessageformat = $eventdata->fullmessageformat;
+        $tabledata->fullmessagehtml = $eventdata->fullmessagehtml;
+        $tabledata->smallmessage = $eventdata->smallmessage;
+        $tabledata->eventtype = $eventdata->name;
+        $tabledata->component = $eventdata->component;
+
+        if (!empty($eventdata->contexturl)) {
+            $tabledata->contexturl = (string)$eventdata->contexturl;
+        } else {
+            $tabledata->contexturl = null;
+        }
 
-    if (!empty($eventdata->contexturlname)) {
-        $savemessage->contexturlname = (string)$eventdata->contexturlname;
+        if (!empty($eventdata->contexturlname)) {
+            $tabledata->contexturlname = (string)$eventdata->contexturlname;
+        } else {
+            $tabledata->contexturlname = null;
+        }
     } else {
-        $savemessage->contexturlname = null;
+        $table = 'messages';
+
+        if (!$conversationid = \core_message\api::get_conversation_between_users([$eventdata->userfrom->id,
+                $eventdata->userto->id])) {
+            $conversationid = \core_message\api::create_conversation_between_users([$eventdata->userfrom->id,
+                $eventdata->userto->id]);
+        }
+
+        $tabledata = new stdClass();
+        $tabledata->courseid = $eventdata->courseid;
+        $tabledata->useridfrom = $eventdata->userfrom->id;
+        $tabledata->conversationid = $conversationid;
+        $tabledata->subject = $eventdata->subject;
+        $tabledata->fullmessage = $eventdata->fullmessage;
+        $tabledata->fullmessageformat = $eventdata->fullmessageformat;
+        $tabledata->fullmessagehtml = $eventdata->fullmessagehtml;
+        $tabledata->smallmessage = $eventdata->smallmessage;
     }
 
-    $savemessage->timecreated = time();
+    $tabledata->timecreated = time();
 
     if (PHPUNIT_TEST and class_exists('phpunit_util')) {
         // Add some more tests to make sure the normal code can actually work.
@@ -173,9 +193,21 @@ function message_send($eventdata) {
         unset($messageproviders);
         // Now ask phpunit if it wants to catch this message.
         if (phpunit_util::is_redirecting_messages()) {
-            $savemessage->timeread = time();
-            $messageid = $DB->insert_record('message_read', $savemessage);
-            $message = $DB->get_record('message_read', array('id'=>$messageid));
+            $messageid = $DB->insert_record($table, $tabledata);
+            $message = $DB->get_record($table, array('id' => $messageid));
+
+            // Add the useridto attribute for BC.
+            $message->useridto = $eventdata->userto->id;
+
+            // Mark the message/notification as read.
+            if ($eventdata->notification) {
+                \core_message\api::mark_notification_as_read($message);
+            } else {
+                \core_message\api::mark_message_as_read($eventdata->userto->id, $message);
+            }
+
+            // Unit tests need this detail.
+            $message->notification = $eventdata->notification;
             phpunit_util::message_sent($message);
             return $messageid;
         }
@@ -183,7 +215,7 @@ function message_send($eventdata) {
 
     // Fetch enabled processors.
     // If we are dealing with a message some processors may want to handle it regardless of user and site settings.
-    if (empty($savemessage->notification)) {
+    if (!$eventdata->notification) {
         $processors = array_filter(get_message_processors(false), function($processor) {
             if ($processor->object->force_process_messages()) {
                 return true;
@@ -226,7 +258,7 @@ function message_send($eventdata) {
         }
 
         // Populate the list of processors we will be using
-        if (empty($savemessage->notification) && $processor->object->force_process_messages()) {
+        if (!$eventdata->notification && $processor->object->force_process_messages()) {
             $processorlist[] = $processor->name;
         } else if ($permitted == 'forced' && $userisconfigured) {
             // An admin is forcing users to use this message processor. Use this processor unconditionally.
@@ -248,20 +280,20 @@ function message_send($eventdata) {
     }
 
     // Only cache messages, not notifications.
-    if (empty($savemessage->notification)) {
+    if (!$eventdata->notification) {
         // Cache the timecreated value of the last message between these two users.
         $cache = cache::make('core', 'message_time_last_message_between_users');
-        $key = \core_message\helper::get_last_message_time_created_cache_key($savemessage->useridfrom,
-            $savemessage->useridto);
-        $cache->set($key, $savemessage->timecreated);
+        $key = \core_message\helper::get_last_message_time_created_cache_key($eventdata->userfrom->id,
+            $eventdata->userto->id);
+        $cache->set($key, $tabledata->timecreated);
     }
 
     // Store unread message just in case we get a fatal error any time later.
-    $savemessage->id = $DB->insert_record('message', $savemessage);
-    $eventdata->savedmessageid = $savemessage->id;
+    $tabledata->id = $DB->insert_record($table, $tabledata);
+    $eventdata->savedmessageid = $tabledata->id;
 
     // Let the manager do the sending or buffering when db transaction in progress.
-    return \core\message\manager::send_message($eventdata, $savemessage, $processorlist);
+    return \core\message\manager::send_message($eventdata, $tabledata, $processorlist);
 }
 
 
index a8259ae..c011a13 100644 (file)
@@ -442,7 +442,7 @@ define('FEATURE_USES_QUESTIONS', 'usesquestions');
 /**
  * Maximum filename char size
  */
-define('MAX_FILENAME_SIZE', 90);
+define('MAX_FILENAME_SIZE', 100);
 
 /** Unspecified module archetype */
 define('MOD_ARCHETYPE_OTHER', 0);
@@ -991,21 +991,6 @@ function clean_param($param, $type) {
             if ($param === '.' || $param === '..') {
                 $param = '';
             }
-            // Extract a part of the filename if it's char size exceeds MAX_FILENAME_SIZE.
-            // If the filename is too long, the file cannot be created on the filesystem due to exceeding max byte size.
-            // Limiting the filename to a certain size (considering multibyte characters) will prevent this.
-            if (core_text::strlen($param) > MAX_FILENAME_SIZE) {
-                // Exclude extension if present in filename.
-                $mimetypes = get_mimetypes_array();
-                $extension = pathinfo($param, PATHINFO_EXTENSION);
-                if ($extension && !empty($mimetypes[$extension])) {
-                    $basename = pathinfo($param, PATHINFO_FILENAME);
-                    $param = core_text::substr($basename, 0, MAX_FILENAME_SIZE);
-                    $param .= '.' . $extension;
-                } else {
-                    $param = core_text::substr($param, 0, MAX_FILENAME_SIZE);
-                }
-            }
             return $param;
 
         case PARAM_PATH:
@@ -4071,9 +4056,6 @@ function delete_user(stdClass $user) {
     // Delete all grades - backup is kept in grade_grades_history table.
     grade_user_delete($user->id);
 
-    // Move unread messages from this user to read.
-    message_move_userfrom_unread2read($user->id);
-
     // TODO: remove from cohorts using standard API here.
 
     // Remove user tags.
@@ -6816,7 +6798,6 @@ function clean_filename($string) {
     return clean_param($string, PARAM_FILE);
 }
 
-
 // STRING TRANSLATION.
 
 /**
@@ -8261,6 +8242,32 @@ function shorten_text($text, $ideal=30, $exact = false, $ending='...') {
     return $truncate;
 }
 
+/**
+ * Shortens a given filename by removing characters positioned after the ideal string length.
+ * When the filename is too long, the file cannot be created on the filesystem due to exceeding max byte size.
+ * Limiting the filename to a certain size (considering multibyte characters) will prevent this.
+ *
+ * @param string $filename file name
+ * @param int $length ideal string length
+ * @return string $shortened shortened file name
+ */
+function shorten_filename($filename, $length = MAX_FILENAME_SIZE) {
+    $shortened = $filename;
+    // Extract a part of the filename if it's char size exceeds the ideal string length.
+    if (core_text::strlen($filename) > $length) {
+        // Exclude extension if present in filename.
+        $mimetypes = get_mimetypes_array();
+        $extension = pathinfo($filename, PATHINFO_EXTENSION);
+        if ($extension && !empty($mimetypes[$extension])) {
+            $basename = pathinfo($filename, PATHINFO_FILENAME);
+            $shortened = core_text::substr($basename, 0, $length);
+            $shortened .= '.' . $extension;