Merge branch 'wip-MDL-61746-master' of https://github.com/RohithSrk/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 4 Apr 2018 22:42:11 +0000 (00:42 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 4 Apr 2018 22:42:11 +0000 (00:42 +0200)
218 files changed:
.travis.yml
admin/settings/appearance.php
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/tests/course_test.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
calendar/templates/month_mini.mustache
calendar/view.php
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/self/bulkchangeforms.php [new file with mode: 0644]
enrol/self/classes/deleteselectedusers_form.php [new file with mode: 0644]
enrol/self/classes/deleteselectedusers_operation.php [new file with mode: 0644]
enrol/self/classes/editselectedusers_form.php [new file with mode: 0644]
enrol/self/classes/editselectedusers_operation.php [new file with mode: 0644]
enrol/self/lang/en/enrol_self.php
enrol/self/lib.php
enrol/self/locallib.php
files/renderer.php
grade/export/lib.php
group/index.php
install.php
install/lang/sk/install.php
iplookup/tests/geoip_test.php
lang/en/message.php
lang/en/moodle.php
lang/en/search.php
lib/adminlib.php
lib/amd/build/auto_rows.min.js
lib/amd/src/auto_rows.js
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/classes/task/tag_cron_task.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/testing/generator/data_generator.php
lib/tests/message_test.php
lib/tests/messagelib_test.php
lib/tests/moodlelib_test.php
lib/tests/questionlib_test.php
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/view.php
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/locallib.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/collection.php
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/collection_test.php
privacy/tests/legacy_polyfill_test.php
privacy/tests/manager_test.php
privacy/tests/moodle_content_writer_test.php
privacy/tests/types_plugintype_link_test.php
privacy/tests/types_subsystem_link_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/lib.php
question/question.php
question/tests/externallib_test.php
question/tests/generator/lib.php
question/type/calculatedsimple/tests/helper.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/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/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/lang/en/search_solr.php
search/engine/solr/tests/engine_test.php
theme/boost/templates/core/filemanager_modal_generallayout.mustache
user/index.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 5e13a2f..7486db8 100644 (file)
@@ -199,7 +199,7 @@ preferences,moodle|/user/preferences.php|preferences',
 
     // "documentation" settingpage
     $temp = new admin_settingpage('documentation', new lang_string('moodledocs'));
-    $temp->add(new admin_setting_configtext('docroot', new lang_string('docroot', 'admin'), new lang_string('configdocroot', 'admin'), 'http://docs.moodle.org', PARAM_URL));
+    $temp->add(new admin_setting_configtext('docroot', new lang_string('docroot', 'admin'), new lang_string('configdocroot', 'admin'), 'https://docs.moodle.org', PARAM_URL));
     $ltemp = array('' => get_string('forceno'));
     $ltemp += get_string_manager()->get_list_of_translations(true);
     $temp->add(new admin_setting_configselect('doclang', get_string('doclang', 'admin'), get_string('configdoclang', 'admin'), '', $ltemp));
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 80e462d..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);
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 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 d4a98a1..2a34335 100644 (file)
@@ -61,7 +61,7 @@
                 <a{{!
                     }} href="#"{{!
                     }} class="arrow_link next"{{!
-                    }} title="{{#str}}monthprev, calendar{{/str}}"{{!
+                    }} title="{{#str}}monthnext, calendar{{/str}}"{{!
                     }} data-year="{{nextperiod.year}}"{{!
                     }} data-month="{{nextperiod.mon}}"{{!
                 }}>
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();
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')));
+    }
 }
diff --git a/enrol/self/bulkchangeforms.php b/enrol/self/bulkchangeforms.php
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/enrol/self/classes/deleteselectedusers_form.php b/enrol/self/classes/deleteselectedusers_form.php
new file mode 100644 (file)
index 0000000..4ebc7ed
--- /dev/null
@@ -0,0 +1,37 @@
+<?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/>.
+
+/**
+ * The form to confirm the intention to bulk delete users enrolments.
+ *
+ * @package enrol_self
+ * @copyright 2018 Farhan Karmali
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once("$CFG->dirroot/enrol/bulkchange_forms.php");
+
+/**
+ * The form to confirm the intention to bulk delete users enrolments.
+ *
+ * @package enrol_self
+ * @copyright 2018 Farhan Karmali
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class enrol_self_deleteselectedusers_form extends enrol_bulk_enrolment_confirm_form {
+}
\ No newline at end of file
diff --git a/enrol/self/classes/deleteselectedusers_operation.php b/enrol/self/classes/deleteselectedusers_operation.php
new file mode 100644 (file)
index 0000000..81f0248
--- /dev/null
@@ -0,0 +1,98 @@
+<?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/>.
+
+/**
+ * A bulk operation for the self enrolment plugin to delete selected users enrolments.
+ *
+ * @package enrol_self
+ * @copyright 2018 Farhan Karmali
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A bulk operation for the self enrolment plugin to delete selected users enrolments.
+ *
+ * @package enrol_self
+ * @copyright 2018 Farhan Karmali
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class enrol_self_deleteselectedusers_operation extends enrol_bulk_enrolment_operation {
+
+    /**
+     * Returns the title to display for this bulk operation.
+     *
+     * @return string
+     */
+    public function get_identifier() {
+        return 'deleteselectedusers';
+    }
+
+    /**
+     * Returns the identifier for this bulk operation. This is the key used when the plugin
+     * returns an array containing all of the bulk operations it supports.
+     *
+     * @return string
+     */
+    public function get_title() {
+        return get_string('deleteselectedusers', 'enrol_manual');
+    }
+
+    /**
+     * Returns a enrol_bulk_enrolment_operation extension form to be used
+     * in collecting required information for this operation to be processed.
+     *
+     * @param string|moodle_url|null $defaultaction
+     * @param mixed $defaultcustomdata
+     * @return enrol_manual_editselectedusers_form
+     */
+    public function get_form($defaultaction = null, $defaultcustomdata = null) {
+        if (!array($defaultcustomdata)) {
+            $defaultcustomdata = array();
+        }
+        $defaultcustomdata['title'] = $this->get_title();
+        $defaultcustomdata['message'] = get_string('confirmbulkdeleteenrolment', 'enrol_self');
+        $defaultcustomdata['button'] = get_string('unenrolusers', 'enrol_self');
+
+        return new enrol_self_deleteselectedusers_form($defaultaction, $defaultcustomdata);
+    }
+
+    /**
+     * Processes the bulk operation request for the given userids with the provided properties.
+     *
+     * @param course_enrolment_manager $manager
+     * @param array $users
+     * @param stdClass $properties The data returned by the form.
+     */
+    public function process(course_enrolment_manager $manager, array $users, stdClass $properties) {
+        if (!has_capability("enrol/self:unenrol", $manager->get_context())) {
+            return false;
+        }
+
+        foreach ($users as $user) {
+            foreach ($user->enrolments as $enrolment) {
+                $plugin = $enrolment->enrolmentplugin;
+                $instance = $enrolment->enrolmentinstance;
+                if ($plugin->allow_unenrol_user($instance, $enrolment)) {
+                    $plugin->unenrol_user($instance, $user->id);
+                }
+            }
+        }
+
+        return true;
+    }
+}
diff --git a/enrol/self/classes/editselectedusers_form.php b/enrol/self/classes/editselectedusers_form.php
new file mode 100644 (file)
index 0000000..7bc82f4
--- /dev/null
@@ -0,0 +1,37 @@
+<?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/>.
+
+/**
+ * The form to collect required information when bulk editing users enrolments.
+ *
+ * @package enrol_self
+ * @copyright 2018 Farhan Karmali
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once("$CFG->dirroot/enrol/bulkchange_forms.php");
+
+/**
+ * The form to collect required information when bulk editing users enrolments.
+ *
+ * @package enrol_self
+ * @copyright 2018 Farhan Karmali
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class enrol_self_editselectedusers_form extends enrol_bulk_enrolment_change_form {
+}
diff --git a/enrol/self/classes/editselectedusers_operation.php b/enrol/self/classes/editselectedusers_operation.php
new file mode 100644 (file)
index 0000000..800a0b8
--- /dev/null
@@ -0,0 +1,161 @@
+<?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/>.
+
+/**
+ * A bulk operation for the manual enrolment plugin to edit selected users.
+ *
+ * @package enrol_self
+ * @copyright 2018 Farhan Karmali
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A bulk operation for the manual enrolment plugin to edit selected users.
+ *
+ * @package enrol_self
+ * @copyright 2018 Farhan Karmali
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class enrol_self_editselectedusers_operation extends enrol_bulk_enrolment_operation {
+
+    /**
+     * Returns the title to display for this bulk operation.
+     *
+     * @return string
+     */
+    public function get_title() {
+        return get_string('editselectedusers', 'enrol_self');
+    }
+
+    /**
+     * Returns the identifier for this bulk operation. This is the key used when the plugin
+     * returns an array containing all of the bulk operations it supports.
+     */
+    public function get_identifier() {
+        return 'editselectedusers';
+    }
+
+    /**
+     * Processes the bulk operation request for the given userids with the provided properties.
+     *
+     * @param course_enrolment_manager $manager
+     * @param array $users
+     * @param stdClass $properties The data returned by the form.
+     */
+    public function process(course_enrolment_manager $manager, array $users, stdClass $properties) {
+        global $DB, $USER;
+
+        if (!has_capability("enrol/self:manage", $manager->get_context())) {
+            return false;
+        }
+
+        // Get all of the user enrolment id's.
+        $ueids = array();
+        $instances = array();
+        foreach ($users as $user) {
+            foreach ($user->enrolments as $enrolment) {
+                $ueids[] = $enrolment->id;
+                if (!array_key_exists($enrolment->id, $instances)) {
+                    $instances[$enrolment->id] = $enrolment;
+                }
+            }
+        }
+
+        // Check that each instance is manageable by the current user.
+        foreach ($instances as $instance) {
+            if (!$this->plugin->allow_manage($instance)) {
+                return false;
+            }
+        }
+
+        // Collect the known properties.
+        $status = $properties->status;
+        $timestart = $properties->timestart;
+        $timeend = $properties->timeend;
+
+        list($ueidsql, $params) = $DB->get_in_or_equal($ueids, SQL_PARAMS_NAMED);
+
+        $updatesql = array();
+        if ($status == ENROL_USER_ACTIVE || $status == ENROL_USER_SUSPENDED) {
+            $updatesql[] = 'status = :status';
+            $params['status'] = (int)$status;
+        }
+        if (!empty($timestart)) {
+            $updatesql[] = 'timestart = :timestart';
+            $params['timestart'] = (int)$timestart;
+        }
+        if (!empty($timeend)) {
+            $updatesql[] = 'timeend = :timeend';
+            $params['timeend'] = (int)$timeend;
+        }
+        if (empty($updatesql)) {
+            return true;
+        }
+
+        // Update the modifierid.
+        $updatesql[] = 'modifierid = :modifierid';
+        $params['modifierid'] = (int)$USER->id;
+
+        // Update the time modified.
+        $updatesql[] = 'timemodified = :timemodified';
+        $params['timemodified'] = time();
+
+        // Build the SQL statement.
+        $updatesql = join(', ', $updatesql);
+        $sql = "UPDATE {user_enrolments}
+                   SET $updatesql
+                 WHERE id $ueidsql";
+
+        if ($DB->execute($sql, $params)) {
+            foreach ($users as $user) {
+                foreach ($user->enrolments as $enrolment) {
+                    $enrolment->courseid  = $enrolment->enrolmentinstance->courseid;
+                    $enrolment->enrol     = 'self';
+                    // Trigger event.
+                    $event = \core\event\user_enrolment_updated::create(
+                        array(
+                            'objectid' => $enrolment->id,
+                            'courseid' => $enrolment->courseid,
+                            'context' => context_course::instance($enrolment->courseid),
+                            'relateduserid' => $user->id,
+                            'other' => array('enrol' => 'self')
+                        )
+                    );
+                    $event->trigger();
+                }
+            }
+            // Delete cached course contacts for this course because they may be affected.
+            cache::make('core', 'coursecontacts')->delete($manager->get_context()->instanceid);
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns a enrol_bulk_enrolment_operation extension form to be used
+     * in collecting required information for this operation to be processed.
+     *
+     * @param string|moodle_url|null $defaultaction
+     * @param mixed $defaultcustomdata
+     * @return enrol_self_editselectedusers_form
+     */
+    public function get_form($defaultaction = null, $defaultcustomdata = null) {
+        return new enrol_self_editselectedusers_form($defaultaction, $defaultcustomdata);
+    }
+}
index 5f4e5e1..9cfb17a 100644 (file)
@@ -28,6 +28,7 @@ $string['canntenrollate'] = 'You cannot enrol any more, since enrolment ended on
 $string['cohortnonmemberinfo'] = 'Only members of cohort \'{$a}\' can self-enrol.';
 $string['cohortonly'] = 'Only cohort members';
 $string['cohortonly_help'] = 'Self enrolment may be restricted to members of a specified cohort only. Note that changing this setting has no effect on existing enrolments.';
+$string['confirmbulkdeleteenrolment'] = 'Are you sure you want to delete these users enrolments?';
 $string['customwelcomemessage'] = 'Custom welcome message';
 $string['customwelcomemessage_help'] = 'A custom welcome message may be added as plain text or Moodle-auto format, including HTML tags and multi-lang tags.
 
@@ -39,6 +40,8 @@ The following placeholders may be included in the message:
 * User fullname {$a->fullname}';
 $string['defaultrole'] = 'Default role assignment';
 $string['defaultrole_desc'] = 'Select role which should be assigned to users during self enrolment';
+$string['deleteselectedusers'] = 'Delete selected user enrolments';
+$string['editselectedusers'] = 'Edit selected user enrolments';
 $string['enrolenddate'] = 'End date';
 $string['enrolenddate_help'] = 'If enabled, users can enrol themselves until this date only.';
 $string['enrolenddaterror'] = 'Enrolment end date cannot be earlier than start date';
@@ -107,6 +110,7 @@ $string['status_help'] = 'If enabled together with \'Allow new enrolments\' disa
 $string['unenrol'] = 'Unenrol user';
 $string['unenrolselfconfirm'] = 'Do you really want to unenrol yourself from course "{$a}"?';
 $string['unenroluser'] = 'Do you really want to unenrol "{$a->user}" from course "{$a->course}"?';
+$string['unenrolusers'] = 'Unenrol users';
 $string['usepasswordpolicy'] = 'Use password policy';
 $string['usepasswordpolicy_desc'] = 'Use standard password policy for enrolment keys.';
 $string['welcometocourse'] = 'Welcome to {$a}';
index 98e9583..e521c2e 100644 (file)
@@ -694,6 +694,25 @@ class enrol_self_plugin extends enrol_plugin {
         return $options;
     }
 
+    /**
+     * The self enrollment plugin has several bulk operations that can be performed.
+     * @param course_enrolment_manager $manager
+     * @return array
+     */
+    public function get_bulk_operations(course_enrolment_manager $manager) {
+        global $CFG;
+        require_once($CFG->dirroot.'/enrol/self/locallib.php');
+        $context = $manager->get_context();
+        $bulkoperations = array();
+        if (has_capability("enrol/self:manage", $context)) {
+            $bulkoperations['editselectedusers'] = new enrol_self_editselectedusers_operation($manager, $this);
+        }
+        if (has_capability("enrol/self:unenrol", $context)) {
+            $bulkoperations['deleteselectedusers'] = new enrol_self_deleteselectedusers_operation($manager, $this);
+        }
+        return $bulkoperations;
+    }
+
     /**
      * Add elements to the edit instance form.
      *
index dc17af1..850555e 100644 (file)
@@ -25,6 +25,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 require_once("$CFG->libdir/formslib.php");
+require_once($CFG->dirroot . '/enrol/locallib.php');
 
 /**
  * Check if the given password match a group enrolment key in the specified course.
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 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 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 21b3c60..d9faa52 100644 (file)
@@ -1478,6 +1478,7 @@ $string['parentcategory'] = 'Parent category';
 $string['parentcoursenotfound'] = 'Parent course not found!';
 $string['parentfolder'] = 'Parent folder';
 $string['participants'] = 'Participants';
+$string['participantscount'] = 'Number of participants: {$a}';
 $string['participantslist'] = 'Participants list';
 $string['participationratio'] = 'Participation ratio';
 $string['participationreport'] = 'Participation report';
index 2129142..6afcbd4 100644 (file)
@@ -100,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 5666dc3..edc87d9 100644 (file)
Binary files a/lib/amd/build/auto_rows.min.js and b/lib/amd/build/auto_rows.min.js differ
index ab7a876..644d3a9 100644 (file)
@@ -49,9 +49,6 @@ define(['jquery'], function($) {
         var innerHeight = element.innerHeight();
         var padding = innerHeight - height;
 
-        // Set height to 1ox to force scroll height to calculate correctly.
-        element.height('1px');
-
         var scrollHeight = element[0].scrollHeight;
         var rows = (scrollHeight - padding) / (height / currentRows);
 
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 721edfb..5be4bb1 100644 (file)
@@ -184,7 +184,7 @@ class tag_cron_task extends scheduled_task {
         $sql = "SELECT DISTINCT component, itemtype
                   FROM {tag_instance}
                  WHERE itemtype <> 'user' or component <> 'core'";
-        $tagareas = $DB->get_records_sql($sql);
+        $tagareas = $DB->get_recordset_sql($sql);
         foreach ($tagareas as $tagarea) {
             $sql = 'SELECT ti.id
                       FROM {tag_instance} ti
@@ -196,6 +196,7 @@ class tag_cron_task extends scheduled_task {
                 $tagarray[] = $tagid->id;
             }
         }
+        $tagareas->close();
 
         // Get instances for each of the ids to be deleted.
         if (count($tagarray) > 0) {
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 132eb55..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);
 }
 
 /**
@@ -6577,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 9e50c1e..d6a5dab 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;
+        } else {
+            $shortened = core_text::substr($filename, 0, $length);
+        }
+    }
+    return $shortened;
+}
 
 /**
  * Given dates in seconds, how many weeks is the date from startdate
index ffbf83e..383780c 100644 (file)
@@ -33,7 +33,7 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class phpunit_message_sink {
-    /** @var array of records from message_read table */
+    /** @var array of records from messages table */
     protected $messages = array();
 
     /**
@@ -48,7 +48,7 @@ class phpunit_message_sink {
     /**
      * To be called from phpunit_util only!
      *
-     * @param stdClass $message record from message_read table
+     * @param stdClass $message record from messages table
      */
     public function add_message($message) {
         /* Number messages from 0. */
@@ -58,7 +58,7 @@ class phpunit_message_sink {
     /**
      * Returns all redirected messages.
      *
-     * The instances are records form the message_read table.
+     * The instances are records from the messages table.
      * The array indexes are numbered from 0 and the order is matching
      * the creation of events.
      *
index bb00478..df0eb01 100644 (file)
@@ -50,7 +50,7 @@ class phpunit_phpmailer_sink {
     /**
      * To be called from phpunit_util only!
      *
-     * @param stdClass $message record from message_read table
+     * @param stdClass $message record from messages table
      */
     public function add_message($message) {
         /* Number messages from 0. */
@@ -60,7 +60,7 @@ class phpunit_phpmailer_sink {
     /**
      * Returns all redirected messages.
      *
-     * The instances are records form the message_read table.
+     * The instances are records from the messages table.
      * The array indexes are numbered from 0 and the order is matching
      * the creation of events.
      *
index d5cfd94..3252bf6 100644 (file)
@@ -714,7 +714,7 @@ class phpunit_util extends testing_util {
     /**
      * To be called from messagelib.php only!
      *
-     * @param stdClass $message record from message_read table
+     * @param stdClass $message record from messages table
      * @return bool true means send message, false means message "sent" to sink.
      */
     public static function message_sent($message) {
@@ -765,7 +765,7 @@ class phpunit_util extends testing_util {
     /**
      * To be called from messagelib.php only!
      *
-     * @param stdClass $message record from message_read table
+     * @param stdClass $message record from messages table
      * @return bool true means send message, false means message "sent" to sink.
      */
     public static function phpmailer_sent($message) {
index 4f0124c..57e3776 100644 (file)
@@ -1606,61 +1606,40 @@ class context_to_string_translator{
 /**
  * Check capability on category
  *
- * @param mixed $question object or id
- * @param string $cap 'add', 'edit', 'view', 'use', 'move'
- * @param integer $cachecat useful to cache all question records in a category
+ * @param mixed $questionorid object or id. If an object is passed, it should include ->contextid and ->createdby.
+ * @param string $cap 'add', 'edit', 'view', 'use', 'move' or 'tag'.
+ * @param integer $notused no longer used.
  * @return boolean this user has the capability $cap for this question $question?
  */
-function question_has_capability_on($question, $cap, $cachecat = -1) {
-    global $USER, $DB;
-
-    // these are capabilities on existing questions capabilties are
-    //set per category. Each of these has a mine and all version. Append 'mine' and 'all'
-    $question_questioncaps = array('edit', 'view', 'use', 'move', 'tag');
-    static $questions = array();
-    static $categories = array();
-    static $cachedcat = array();
-    if ($cachecat != -1 && array_search($cachecat, $cachedcat) === false) {
-        $questions += $DB->get_records('question', array('category' => $cachecat), '', 'id,category,createdby');
-        $cachedcat[] = $cachecat;
-    }
-    if (!is_object($question)) {
-        if (!isset($questions[$question])) {
-            if (!$questions[$question] = $DB->get_record('question',
-                    array('id' => $question), 'id,category,createdby')) {
-                print_error('questiondoesnotexist', 'question');
-            }
-        }
-        $question = $questions[$question];
-    }
-    if (empty($question->category)) {
-        // This can happen when we have created a fake 'missingtype' question to
-        // take the place of a deleted question.
-        return false;
-    }
-    if (!isset($categories[$question->category])) {
-        if (!$categories[$question->category] = $DB->get_record('question_categories',
-                array('id'=>$question->category))) {
-            print_error('invalidcategory', 'question');
+function question_has_capability_on($questionorid, $cap, $notused = -1) {
+    global $USER;
+
+    if (is_numeric($questionorid)) {
+        $question = question_bank::load_question_data((int)$questionorid);
+    } else if (is_object($questionorid)) {
+        if (isset($questionorid->contextid) && isset($questionorid->createdby)) {
+            $question = $questionorid;
         }
-    }
-    $category = $categories[$question->category];
-    $context = context::instance_by_id($category->contextid);
 
-    if (array_search($cap, $question_questioncaps)!== false) {
-        if (!has_capability('moodle/question:' . $cap . 'all', $context)) {
-            if ($question->createdby == $USER->id) {
-                return has_capability('moodle/question:' . $cap . 'mine', $context);
-            } else {
-                return false;
-            }
-        } else {
-            return true;
+        if (!isset($question) && isset($questionorid->id) && $questionorid->id != 0) {
+            $question = question_bank::load_question_data($questionorid->id);
         }
     } else {
-        return has_capability('moodle/question:' . $cap, $context);
+        throw new coding_exception('$questionorid parameter needs to be an integer or an object.');
     }
 
+    $context = context::instance_by_id($question->contextid);
+
+    // These are existing questions capabilities that are set per category.
+    // Each of these has a 'mine' and 'all' version that is appended to the capability name.
+    $capabilitieswithallandmine = ['edit' => 1, 'view' => 1, 'use' => 1, 'move' => 1, 'tag' => 1];
+
+    if (!isset($capabilitieswithallandmine[$cap])) {
+        return has_capability('moodle/question:' . $cap, $context);
+    } else {
+        return has_capability('moodle/question:' . $cap . 'all', $context) ||
+            ($question->createdby == $USER->id && has_capability('moodle/question:' . $cap . 'mine', $context));
+    }
 }
 
 /**
index b3499c7..5dc5e4c 100644 (file)
@@ -415,6 +415,10 @@ EOD;
             $record['category'] = $DB->get_field_select('course_categories', "MIN(id)", "parent=0");
         }
 
+        if (!isset($record['startdate'])) {
+            $record['startdate'] = usergetmidnight(time());
+        }
+
         if (isset($record['tags']) && !is_array($record['tags'])) {
             $record['tags'] = preg_split('/\s*,\s*/', trim($record['tags']), -1, PREG_SPLIT_NO_EMPTY);
         }
index 42c4cad..3f1d3de 100644 (file)
@@ -167,7 +167,7 @@ class core_message_testcase extends advanced_testcase {
         $emails = $sink->get_messages();
         $this->assertCount(1, $emails);
         $email = reset($emails);
-        $recordexists = $DB->record_exists('message', array('id' => $messageid));
+        $recordexists = $DB->record_exists('messages', array('id' => $messageid));
         $this->assertSame(true, $recordexists);
         $this->assertSame($user1->email, $email->from);
         $this->assertSame($user2->email, $email->to);
@@ -207,7 +207,7 @@ class core_message_testcase extends advanced_testcase {
         $emails = $sink->get_messages();
         $this->assertCount(1, $emails);
         $email = reset($emails);
-        $recordexists = $DB->record_exists('message', array('id' => $messageid));
+        $recordexists = $DB->record_exists('messages', array('id' => $messageid));
         $this->assertSame(true, $recordexists);
         $this->assertSame($user1->email, $email->from);
         $this->assertSame($user2->email, $email->to);
index 051f61c..ed6e8ab 100644 (file)
@@ -218,14 +218,15 @@ class core_messagelib_testcase extends advanced_testcase {
         $this->assertEquals($message->smallmessage, $savedmessage->smallmessage);
         $this->assertEquals($message->smallmessage, $savedmessage->smallmessage);
         $this->assertEquals($message->notification, $savedmessage->notification);
-        $this->assertNull($savedmessage->contexturl);
-        $this->assertNull($savedmessage->contexturlname);
         $this->assertTimeCurrent($savedmessage->timecreated);
-        $record = $DB->get_record('message_read', array('id' => $savedmessage->id), '*', MUST_EXIST);
+        $record = $DB->get_record('messages', array('id' => $savedmessage->id), '*', MUST_EXIST);
+        unset($savedmessage->useridto);
+        unset($savedmessage->notification);
         $this->assertEquals($record, $savedmessage);
         $sink->clear();
-        $this->assertFalse($DB->record_exists('message', array()));
-        $DB->delete_records('message_read', array());
+        $this->assertTrue($DB->record_exists('message_user_actions', array('userid' => $user2->id, 'messageid' => $messageid,
+            'action' => \core_message\api::MESSAGE_ACTION_READ)));
+        $DB->delete_records('messages', array());
 
         $message = new \core\message\message();
         $message->courseid = 1;
@@ -239,8 +240,7 @@ class core_messagelib_testcase extends advanced_testcase {
         $message->fullmessagehtml = '<p>message body</p>';
         $message->smallmessage = 'small message';
         $message->notification = '0';
-        $message->contexturl = new moodle_url('/');
-        $message->contexturlname = 'front';
+
         $sink = $this->redirectMessages();
         $messageid = message_send($message);
         $savedmessages = $sink->get_messages();
@@ -255,14 +255,15 @@ class core_messagelib_testcase extends advanced_testcase {
         $this->assertEquals($message->smallmessage, $savedmessage->smallmessage);
         $this->assertEquals($message->smallmessage, $savedmessage->smallmessage);
         $this->assertEquals($message->notification, $savedmessage->notification);
-        $this->assertEquals($message->contexturl->out(), $savedmessage->contexturl);
-        $this->assertEquals($message->contexturlname, $savedmessage->contexturlname);
         $this->assertTimeCurrent($savedmessage->timecreated);
-        $record = $DB->get_record('message_read', array('id' => $savedmessage->id), '*', MUST_EXIST);
+        $record = $DB->get_record('messages', array('id' => $savedmessage->id), '*', MUST_EXIST);
+        unset($savedmessage->useridto);
+        unset($savedmessage->notification);
         $this->assertEquals($record, $savedmessage);
         $sink->clear();
-        $this->assertFalse($DB->record_exists('message', array()));
-        $DB->delete_records('message_read', array());
+        $this->assertTrue($DB->record_exists('message_user_actions', array('userid' => $user2->id, 'messageid' => $messageid,
+            'action' => \core_message\api::MESSAGE_ACTION_READ)));
+        $DB->delete_records('messages', array());
 
         // Test phpunit problem detection.
 
@@ -297,8 +298,7 @@ class core_messagelib_testcase extends advanced_testcase {
         }
         $this->assertCount(0, $sink->get_messages());
         $sink->close();
-        $this->assertFalse($DB->record_exists('message', array()));
-        $this->assertFalse($DB->record_exists('message_read', array()));
+        $this->assertFalse($DB->record_exists('messages', array()));
 
         // Invalid users.
 
@@ -420,10 +420,11 @@ class core_messagelib_testcase extends advanced_testcase {
         $messageid = message_send($message);
         $emails = $sink->get_messages();
         $this->assertCount(0, $emails);
-        $savedmessage = $DB->get_record('message', array('id' => $messageid), '*', MUST_EXIST);
+        $savedmessage = $DB->get_record('messages', array('id' => $messageid), '*', MUST_EXIST);
         $sink->clear();
-        $this->assertFalse($DB->record_exists('message_read', array()));
-        $DB->delete_records('message', array());
+        $this->assertFalse($DB->record_exists('message_user_actions', array()));
+        $DB->delete_records('messages', array());
+        $DB->delete_records('message_user_actions', array());
         $events = $eventsink->get_events();
         $this->assertCount(1, $events);
         $this->assertInstanceOf('\core\event\message_sent', $events[0]);
@@ -447,10 +448,12 @@ class core_messagelib_testcase extends advanced_testcase {
         $messageid = message_send($message);
         $emails = $sink->get_messages();
         $this->assertCount(0, $emails);
-        $savedmessage = $DB->get_record('message_read', array('id' => $messageid), '*', MUST_EXIST);
+        $savedmessage = $DB->get_record('messages', array('id' => $messageid), '*', MUST_EXIST);
         $sink->clear();
-        $this->assertFalse($DB->record_exists('message', array()));
-        $DB->delete_records('message_read', array());
+        $this->assertTrue($DB->record_exists('message_user_actions', array('userid' => $user2->id, 'messageid' => $messageid,
+            'action' => \core_message\api::MESSAGE_ACTION_READ)));
+        $DB->delete_records('messages', array());
+        $DB->delete_records('message_user_actions', array());
         $events = $eventsink->get_events();
         $this->assertCount(2, $events);
         $this->assertInstanceOf('\core\event\message_sent', $events[0]);
@@ -475,14 +478,14 @@ class core_messagelib_testcase extends advanced_testcase {
         $messageid = message_send($message);
         $emails = $sink->get_messages();
         $this->assertCount(0, $emails);
-        $savedmessage = $DB->get_record('message_read', array('id' => $messageid), '*', MUST_EXIST);
+        $savedmessage = $DB->get_record('notifications', array('id' => $messageid), '*', MUST_EXIST);
         $sink->clear();
-        $this->assertFalse($DB->record_exists('message', array()));
-        $DB->delete_records('message_read', array());
+        $this->assertFalse($DB->record_exists('messages', array()));
+        $DB->delete_records('notifications', array());
         $events = $eventsink->get_events();
         $this->assertCount(2, $events);
-        $this->assertInstanceOf('\core\event\message_sent', $events[0]);
-        $this->assertInstanceOf('\core\event\message_viewed', $events[1]);
+        $this->assertInstanceOf('\core\event\notification_sent', $events[0]);
+        $this->assertInstanceOf('\core\event\notification_viewed', $events[1]);
         $eventsink->clear();
 
         // Will always use the pop-up processor.
@@ -507,10 +510,11 @@ class core_messagelib_testcase extends advanced_testcase {
         $messageid = message_send($message);
         $emails = $sink->get_messages();
         $this->assertCount(0, $emails);
-        $savedmessage = $DB->get_record('message', array('id' => $messageid), '*', MUST_EXIST);
+        $savedmessage = $DB->get_record('messages', array('id' => $messageid), '*', MUST_EXIST);
         $sink->clear();
-        $this->assertFalse($DB->record_exists('message_read', array()));
-        $DB->delete_records('message', array());
+        $this->assertFalse($DB->record_exists('message_user_actions', array()));
+        $DB->delete_records('messages', array());
+        $DB->delete_records('message_user_actions', array());
         $events = $eventsink->get_events();
         $this->assertCount(1, $events);
         $this->assertInstanceOf('\core\event\message_sent', $events[0]);
@@ -537,15 +541,15 @@ class core_messagelib_testcase extends advanced_testcase {
         $emails = $sink->get_messages();
         $this->assertCount(1, $emails);
         $email = reset($emails);
-        $savedmessage = $DB->get_record('message', array('id' => $messageid), '*', MUST_EXIST);
+        $savedmessage = $DB->get_record('messages', array('id' => $messageid), '*', MUST_EXIST);
         $this->assertSame($user1->email, $email->from);
         $this->assertSame($user2->email, $email->to);
         $this->assertSame($message->subject, $email->subject);
         $this->assertNotEmpty($email->header);
         $this->assertNotEmpty($email->body);
         $sink->clear();
-        $this->assertFalse($DB->record_exists('message_read', array()));
-        $DB->delete_records('message_read', array());
+        $this->assertFalse($DB->record_exists('message_user_actions', array()));
+        $DB->delete_records('message_user_actions', array());
         $events = $eventsink->get_events();
         $this->assertCount(1, $events);
         $this->assertInstanceOf('\core\event\message_sent', $events[0]);
@@ -570,16 +574,16 @@ class core_messagelib_testcase extends advanced_testcase {
         $emails = $sink->get_messages();
         $this->assertCount(1, $emails);
         $email = reset($emails);
-        $savedmessage = $DB->get_record('message', array('id' => $messageid), '*', MUST_EXIST);
-        $working = $DB->get_record('message_working', array('unreadmessageid' => $messageid), '*', MUST_EXIST);
+        $savedmessage = $DB->get_record('messages', array('id' => $messageid), '*', MUST_EXIST);
         $this->assertSame($user1->email, $email->from);
         $this->assertSame($user2->email, $email->to);
         $this->assertSame($message->subject, $email->subject);
         $this->assertNotEmpty($email->header);
         $this->assertNotEmpty($email->body);
         $sink->clear();
-        $this->assertFalse($DB->record_exists('message_read', array()));
-        $DB->delete_records('message', array());
+        $this->assertFalse($DB->record_exists('message_user_actions', array()));
+        $DB->delete_records('messages', array());
+        $DB->delete_records('message_user_actions', array());
         $events = $eventsink->get_events();
         $this->assertCount(1, $events);
         $this->assertInstanceOf('\core\event\message_sent', $events[0]);
@@ -603,11 +607,10 @@ class core_messagelib_testcase extends advanced_testcase {
         $messageid = message_send($message);
         $emails = $sink->get_messages();
         $this->assertCount(0, $emails);
-        $savedmessage = $DB->get_record('message', array('id' => $messageid), '*', MUST_EXIST);
-        $working = $DB->get_record('message_working', array('unreadmessageid' => $messageid), '*', MUST_EXIST);
+        $savedmessage = $DB->get_record('messages', array('id' => $messageid), '*', MUST_EXIST);
         $sink->clear();
-        $this->assertFalse($DB->record_exists('message_read', array()));
-        $DB->delete_records('message', array());
+        $this->assertFalse($DB->record_exists('message_user_actions', array()));
+        $DB->delete_records('messages', array());
         $events = $eventsink->get_events();
         $this->assertCount(1, $events);
         $this->assertInstanceOf('\core\event\message_sent', $events[0]);
@@ -641,10 +644,10 @@ class core_messagelib_testcase extends advanced_testcase {
         $messageid = message_send($message);
         $emails = $sink->get_messages();
         $this->assertCount(0, $emails);
-        $savedmessage = $DB->get_record('message', array('id' => $messageid), '*', MUST_EXIST);
+        $savedmessage = $DB->get_record('messages', array('id' => $messageid), '*', MUST_EXIST);
         $sink->clear();
-        $this->assertFalse($DB->record_exists('message_read', array()));
-        $DB->delete_records('message', array());
+        $this->assertFalse($DB->record_exists('message_user_actions', array()));
+        $DB->delete_records('messages', array());
         $events = $eventsink->get_events();
         $this->assertCount(0, $events);
         $eventsink->clear();
@@ -674,9 +677,9 @@ class core_messagelib_testcase extends advanced_testcase {
         $messageid = message_send($message);
         $emails = $sink->get_messages();
         $this->assertCount(0, $emails);
-        $savedmessage = $DB->get_record('message', array('id' => $messageid), '*', MUST_EXIST);
+        $savedmessage = $DB->get_record('messages', array('id' => $messageid), '*', MUST_EXIST);
         $sink->clear();
-        $this->assertFalse($DB->record_exists('message_read', array()));
+        $this->assertFalse($DB->record_exists('message_user_actions', array()));
         $events = $eventsink->get_events();
         $this->assertCount(1, $events);
         $this->assertInstanceOf('\core\event\message_sent', $events[0]);
@@ -689,8 +692,8 @@ class core_messagelib_testcase extends advanced_testcase {
         $transaction = $DB->start_delegated_transaction();
         message_send($message);
         message_send($message);
-        $this->assertCount(3, $DB->get_records('message'));
-        $this->assertFalse($DB->record_exists('message_read', array()));
+        $this->assertCount(3, $DB->get_records('messages'));
+        $this->assertFalse($DB->record_exists('message_user_actions', array()));
         $events = $eventsink->get_events();
         $this->assertCount(0, $events);
         $transaction->allow_commit();
@@ -699,14 +702,13 @@ class core_messagelib_testcase extends advanced_testcase {
         $this->assertInstanceOf('\core\event\message_sent', $events[0]);
         $this->assertInstanceOf('\core\event\message_sent', $events[1]);
         $eventsink->clear();
-        $DB->delete_records('message', array());
-        $DB->delete_records('message_read', array());
+        $DB->delete_records('messages', array());
 
         $transaction = $DB->start_delegated_transaction();
         message_send($message);
         message_send($message);
-        $this->assertCount(2, $DB->get_records('message'));
-        $this->assertCount(0, $DB->get_records('message_read'));
+        $this->assertCount(2, $DB->get_records('messages'));
+        $this->assertCount(0, $DB->get_records('message_user_actions'));
         $events = $eventsink->get_events();
         $this->assertCount(0, $events);
         try {
@@ -716,16 +718,14 @@ class core_messagelib_testcase extends advanced_testcase {
         }
         $events = $eventsink->get_events();
         $this->assertCount(0, $events);
-        $this->assertCount(0, $DB->get_records('message'));
-        $this->assertCount(0, $DB->get_records('message_read'));
+        $this->assertCount(0, $DB->get_records('messages'));
         message_send($message);
-        $this->assertCount(1, $DB->get_records('message'));
-        $this->assertCount(0, $DB->get_records('message_read'));
+        $this->assertCount(1, $DB->get_records('messages'));
+        $this->assertCount(0, $DB->get_records('message_user_actions'));
         $events = $eventsink->get_events();
         $this->assertCount(1, $events);
         $this->assertInstanceOf('\core\event\message_sent', $events[0]);
         $sink->clear();
-        $DB->delete_records('message_read', array());
     }
 
     public function test_rollback() {
index 454d9cc..87f59e8 100644 (file)
@@ -682,13 +682,6 @@ class core_moodlelib_testcase extends advanced_testcase {
         $this->assertSame(' . .dontltrim.me', clean_param(' . .dontltrim.me', PARAM_FILE));
         $this->assertSame('here is a tab.txt', clean_param("here is a tab\t.txt", PARAM_FILE));
         $this->assertSame('here is a linebreak.txt', clean_param("here is a line\r\nbreak.txt", PARAM_FILE));
-        // Test filename that contains more than 90 characters.
-        $filename = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium totam rem';
-        $this->assertSame('sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laud',
-            clean_param($filename, PARAM_FILE));
-        // Filename contains extension.
-        $this->assertSame('sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laud.zip',
-            clean_param($filename . '.zip', PARAM_FILE));
 
         // The following behaviours have been maintained although they seem a little odd.
         $this->assertSame('funnything', clean_param('funny:thing', PARAM_FILE));
@@ -1014,6 +1007,29 @@ class core_moodlelib_testcase extends advanced_testcase {
                 shorten_text($text, 1));
     }
 
+    public function test_shorten_filename() {
+        // Test filename that contains more than 100 characters.
+        $filename = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium totam rem';
+        $this->assertSame('sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium tot',
+            shorten_filename($filename));
+        // Filename contains extension.
+        $this->assertSame('sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium tot.zip',
+            shorten_filename($filename . '.zip'));
+        // Limit filename to 50 chars.
+        $this->assertSame('sed ut perspiciatis unde omnis iste natus error si',
+            shorten_filename($filename, 50));
+        $this->assertSame('sed ut perspiciatis unde omnis iste natus error si.zip',
+            shorten_filename($filename . '.zip', 50));
+
+        // Test filename that contains less than 100 characters.
+        $filename = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque';
+        $this->assertSame('sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque',
+            shorten_filename($filename));
+        // Filename contains extension.
+        $this->assertSame('sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque.zip',
+            shorten_filename($filename . '.zip'));
+    }
+
     public function test_usergetdate() {
         global $USER, $CFG, $DB;
         $this->resetAfterTest();
index 1d79dcd..dde35cf 100644 (file)
@@ -1498,4 +1498,469 @@ class core_questionlib_testcase extends advanced_testcase {
         }
     }
 
+    /**
+     * Data provider for tests of question_has_capability_on_context and question_require_capability_on_context.
+     *
+     * @return  array
+     */
+    public function question_capability_on_question_provider() {
+        return [
+            'Unrelated capability which is present' => [
+                'capabilities' => [
+                    'moodle/question:config' => CAP_ALLOW,
+                ],
+                'testcapability' => 'config',
+                'isowner' => true,
+                'expect' => true,
+            ],
+            'Unrelated capability which is present (not owner)' => [
+                'capabilities' => [
+                    'moodle/question:config' => CAP_ALLOW,
+                ],
+                'testcapability' => 'config',
+                'isowner' => false,
+                'expect' => true,
+            ],
+            'Unrelated capability which is not set' => [
+                'capabilities' => [
+                ],
+                'testcapability' => 'config',
+                'isowner' => true,
+                'expect' => false,
+            ],
+            'Unrelated capability which is not set (not owner)' => [
+                'capabilities' => [
+                ],
+                'testcapability' => 'config',
+                'isowner' => false,
+                'expect' => false,
+            ],
+            'Unrelated capability which is prevented' => [
+                'capabilities' => [
+                    'moodle/question:config' => CAP_PREVENT,
+                ],
+                'testcapability' => 'config',
+                'isowner' => true,
+                'expect' => false,
+            ],
+            'Unrelated capability which is prevented (not owner)' => [
+                'capabilities' => [
+                    'moodle/question:config' => CAP_PREVENT,
+                ],
+                'testcapability' => 'config',
+                'isowner' => false,
+                'expect' => false,
+            ],
+            'Related capability which is not set' => [
+                'capabilities' => [
+                ],
+                'testcapability' => 'edit',
+                'isowner' => true,
+                'expect' => false,
+            ],
+            'Related capability which is not set (not owner)' => [
+                'capabilities' => [
+                ],
+                'testcapability' => 'edit',
+                'isowner' => false,
+                'expect' => false,
+            ],
+            'Related capability which is allowed at all, unset at mine' => [
+                'capabilities' => [
+                    'moodle/question:editall' => CAP_ALLOW,
+                ],
+                'testcapability' => 'edit',
+                'isowner' => true,
+                'expect' => true,
+            ],
+            'Related capability which is allowed at all, unset at mine (not owner)' => [
+                'capabilities' => [
+                    'moodle/question:editall' => CAP_ALLOW,
+                ],
+                'testcapability' => 'edit',
+                'isowner' => false,
+                'expect' => true,
+            ],
+            'Related capability which is allowed at all, prevented at mine' => [
+                'capabilities' => [
+                    'moodle/question:editall' => CAP_ALLOW,
+                    'moodle/question:editmine' => CAP_PREVENT,
+                ],
+                'testcapability' => 'edit',
+                'isowner' => true,
+                'expect' => true,
+            ],
+            'Related capability which is allowed at all, prevented at mine (not owner)' => [
+                'capabilities' => [
+                    'moodle/question:editall' => CAP_ALLOW,
+                    'moodle/question:editmine' => CAP_PREVENT,
+                ],
+                'testcapability' => 'edit',
+                'isowner' => false,
+                'expect' => true,
+            ],
+            'Related capability which is unset all, allowed at mine' => [
+                'capabilities' => [
+                    'moodle/question:editall' => CAP_PREVENT,
+                    'moodle/question:editmine' => CAP_ALLOW,
+                ],
+                'testcapability' => 'edit',
+                'isowner' => true,
+                'expect' => true,
+            ],
+            'Related capability which is unset all, allowed at mine (not owner)' => [
+                'capabilities' => [
+                    'moodle/question:editall' => CAP_PREVENT,
+                    'moodle/question:editmine' => CAP_ALLOW,
+                ],
+                'testcapability' => 'edit',
+                'isowner' => false,
+                'expect' => false,
+            ],
+        ];
+    }
+
+    /**
+     * Tests for the deprecated question_has_capability_on function when passing a stdClass as parameter.
+     *
+     * @dataProvider question_capability_on_question_provider
+     * @param   array   $capabilities The capability assignments to set.
+     * @param   string  $capability The capability to test
+     * @param   bool    $isowner Whether the user to create the question should be the owner or not.
+     * @param   bool    $expect The expected result.
+     */
+    public function test_question_has_capability_on_using_stdclass($capabilities, $capability, $isowner, $expect) {
+        $this->resetAfterTest();
+
+        // Create the test data.
+        $user = $this->getDataGenerator()->create_user();
+        $otheruser = $this->getDataGenerator()->create_user();
+        $roleid = $this->getDataGenerator()->create_role();
+        $category = $this->getDataGenerator()->create_category();
+        $context = context_coursecat::instance($category->id);
+
+        // Assign the user to the role.
+        role_assign($roleid, $user->id, $context->id);
+
+        // Assign the capabilities to the role.
+        foreach ($capabilities as $capname => $capvalue) {
+            assign_capability($capname, $capvalue, $roleid, $context->id);
+        }
+        $context->mark_dirty();
+
+        $this->setUser($user);
+
+        // The current fake question we make use of is always a stdClass and typically has no ID.
+        $fakequestion = (object) [
+            'contextid' => $context->id,
+        ];
+
+        if ($isowner) {
+            $fakequestion->createdby = $user->id;
+        } else {
+            $fakequestion->createdby = $otheruser->id;
+        }
+
+        $result = question_has_capability_on($fakequestion, $capability);
+        $this->assertEquals($expect, $result);
+    }
+
+    /**
+     * Tests for the deprecated question_has_capability_on function when using question definition.
+     *
+     * @dataProvider question_capability_on_question_provider
+     * @param   array   $capabilities The capability assignments to set.
+     * @param   string  $capability The capability to test
+     * @param   bool    $isowner Whether the user to create the question should be the owner or not.
+     * @param   bool    $expect The expected result.
+     */
+    public function test_question_has_capability_on_using_question_definition($capabilities, $capability, $isowner, $expect) {
+        $this->resetAfterTest();
+
+        // Create the test data.
+        $generator = $this->getDataGenerator();
+        $questiongenerator = $generator->get_plugin_generator('core_question');
+        $user = $generator->create_user();
+        $otheruser = $generator->create_user();
+        $roleid = $generator->create_role();
+        $category = $generator->create_category();
+        $context = context_coursecat::instance($category->id);
+        $questioncat = $questiongenerator->create_question_category([
+            'contextid' => $context->id,
+        ]);
+
+        // Assign the user to the role.
+        role_assign($roleid, $user->id, $context->id);
+
+        // Assign the capabilities to the role.
+        foreach ($capabilities as $capname => $capvalue) {
+            assign_capability($capname, $capvalue, $roleid, $context->id);
+        }
+        $context->mark_dirty();
+
+        // Create the question.
+        $qtype = 'truefalse';
+        $overrides = [
+            'category' => $questioncat->id,
+        ];
+
+        $question = $questiongenerator->create_question($qtype, null, $overrides);
+
+        // The question generator does not support setting of the createdby for some reason.
+        $question->createdby = ($isowner) ? $user->id : $otheruser->id;
+        $fromform = test_question_maker::get_question_form_data($qtype, null);
+        $fromform = (object) $generator->combine_defaults_and_record((array) $fromform, $overrides);
+        question_bank::get_qtype($qtype)->save_question($question, $fromform);
+
+        $this->setUser($user);
+        $result = question_has_capability_on($question, $capability);
+        $this->assertEquals($expect, $result);
+    }
+
+    /**
+     * Tests for the deprecated question_has_capability_on function when using a real question id.
+     *
+     * @dataProvider question_capability_on_question_provider
+     * @param   array   $capabilities The capability assignments to set.
+     * @param   string  $capability The capability to test
+     * @param   bool    $isowner Whether the user to create the question should be the owner or not.
+     * @param   bool    $expect The expected result.
+     */
+    public function test_question_has_capability_on_using_question_id($capabilities, $capability, $isowner, $expect) {
+        $this->resetAfterTest();
+
+        // Create the test data.
+        $generator = $this->getDataGenerator();
+        $questiongenerator = $generator->get_plugin_generator('core_question');
+        $user = $generator->create_user();
+        $otheruser = $generator->create_user();
+        $roleid = $generator->create_role();
+        $category = $generator->create_category();
+        $context = context_coursecat::instance($category->id);
+        $questioncat = $questiongenerator->create_question_category([
+            'contextid' => $context->id,
+        ]);
+
+        // Assign the user to the role.
+        role_assign($roleid, $user->id, $context->id);
+
+        // Assign the capabilities to the role.
+        foreach ($capabilities as $capname => $capvalue) {
+            assign_capability($capname, $capvalue, $roleid, $context->id);
+        }
+        $context->mark_dirty();
+
+        // Create the question.
+        $qtype = 'truefalse';
+        $overrides = [
+            'category' => $questioncat->id,
+        ];
+
+        $question = $questiongenerator->create_question($qtype, null, $overrides);
+
+        // The question generator does not support setting of the createdby for some reason.
+        $question->createdby = ($isowner) ? $user->id : $otheruser->id;
+        $fromform = test_question_maker::get_question_form_data($qtype, null);
+        $fromform = (object) $generator->combine_defaults_and_record((array) $fromform, $overrides);
+        question_bank::get_qtype($qtype)->save_question($question, $fromform);
+
+        $this->setUser($user);
+        $result = question_has_capability_on($question->id, $capability);
+        $this->assertEquals($expect, $result);
+    }
+
+    /**
+     * Tests for the deprecated question_has_capability_on function when using a string as question id.
+     *
+     * @dataProvider question_capability_on_question_provider
+     * @param   array   $capabilities The capability assignments to set.
+     * @param   string  $capability The capability to test
+     * @param   bool    $isowner Whether the user to create the question should be the owner or not.
+     * @param   bool    $expect The expected result.
+     */
+    public function test_question_has_capability_on_using_question_string_id($capabilities, $capability, $isowner, $expect) {
+        $this->resetAfterTest();
+
+        // Create the test data.
+        $generator = $this->getDataGenerator();
+        $questiongenerator = $generator->get_plugin_generator('core_question');
+        $user = $generator->create_user();
+        $otheruser = $generator->create_user();
+        $roleid = $generator->create_role();
+        $category = $generator->create_category();
+        $context = context_coursecat::instance($category->id);
+        $questioncat = $questiongenerator->create_question_category([
+            'contextid' => $context->id,
+        ]);
+
+        // Assign the user to the role.
+        role_assign($roleid, $user->id, $context->id);
+
+        // Assign the capabilities to the role.
+        foreach ($capabilities as $capname => $capvalue) {
+            assign_capability($capname, $capvalue, $roleid, $context->id);
+        }
+        $context->mark_dirty();
+
+        // Create the question.
+        $qtype = 'truefalse';
+        $overrides = [
+            'category' => $questioncat->id,
+        ];
+
+        $question = $questiongenerator->create_question($qtype, null, $overrides);
+
+        // The question generator does not support setting of the createdby for some reason.
+        $question->createdby = ($isowner) ? $user->id : $otheruser->id;
+        $fromform = test_question_maker::get_question_form_data($qtype, null);
+        $fromform = (object) $generator->combine_defaults_and_record((array) $fromform, $overrides);
+        question_bank::get_qtype($qtype)->save_question($question, $fromform);
+
+        $this->setUser($user);
+        $result = question_has_capability_on((string) $question->id, $capability);
+        $this->assertEquals($expect, $result);
+    }
+
+    /**
+     * Tests for the question_has_capability_on function when using a moved question.
+     *
+     * @dataProvider question_capability_on_question_provider
+     * @param   array   $capabilities The capability assignments to set.
+     * @param   string  $capability The capability to test
+     * @param   bool    $isowner Whether the user to create the question should be the owner or not.
+     * @param   bool    $expect The expected result.
+     */
+    public function test_question_has_capability_on_using_moved_question($capabilities, $capability, $isowner, $expect) {
+        $this->resetAfterTest();
+
+        // Create the test data.
+        $generator = $this->getDataGenerator();
+        $questiongenerator = $generator->get_plugin_generator('core_question');
+        $user = $generator->create_user();
+        $otheruser = $generator->create_user();
+        $roleid = $generator->create_role();
+        $category = $generator->create_category();
+        $context = context_coursecat::instance($category->id);
+        $questioncat = $questiongenerator->create_question_category([
+            'contextid' => $context->id,
+        ]);
+
+        $newcategory = $generator->create_category();
+        $newcontext = context_coursecat::instance($newcategory->id);
+        $newquestioncat = $questiongenerator->create_question_category([
+            'contextid' => $newcontext->id,
+        ]);
+
+        // Assign the user to the role in the _new_ context..
+        role_assign($roleid, $user->id, $newcontext->id);
+
+        // Assign the capabilities to the role in the _new_ context.
+        foreach ($capabilities as $capname => $capvalue) {
+            assign_capability($capname, $capvalue, $roleid, $newcontext->id);
+        }
+        $context->mark_dirty();
+        $newcontext->mark_dirty();
+
+        // Create the question.
+        $qtype = 'truefalse';
+        $overrides = [
+            'category' => $questioncat->id,
+        ];
+
+        $question = $questiongenerator->create_question($qtype, null, $overrides);
+
+        // The question generator does not support setting of the createdby for some reason.
+        $question->createdby = ($isowner) ? $user->id : $otheruser->id;
+        $fromform = test_question_maker::get_question_form_data($qtype, null);
+        $fromform = (object) $generator->combine_defaults_and_record((array) $fromform, $overrides);
+        question_bank::get_qtype($qtype)->save_question($question, $fromform);
+
+        // Move the question.
+        question_move_questions_to_category([$question->id], $newquestioncat->id);
+
+        // Test that the capability is correct after the question has been moved.
+        $this->setUser($user);
+        $result = question_has_capability_on($question->id, $capability);
+        $this->assertEquals($expect, $result);
+    }
+
+    /**
+     * Tests for the question_has_capability_on function when using a real question.
+     *
+     * @dataProvider question_capability_on_question_provider
+     * @param   array   $capabilities The capability assignments to set.
+     * @param   string  $capability The capability to test
+     * @param   bool    $isowner Whether the user to create the question should be the owner or not.
+     * @param   bool    $expect The expected result.
+     */
+    public function test_question_has_capability_on_using_question($capabilities, $capability, $isowner, $expect) {
+        $this->resetAfterTest();
+
+        // Create the test data.
+        $generator = $this->getDataGenerator();
+        $questiongenerator = $generator->get_plugin_generator('core_question');
+        $user = $generator->create_user();
+        $otheruser = $generator->create_user();
+        $roleid = $generator->create_role();
+        $category = $generator->create_category();
+        $context = context_coursecat::instance($category->id);
+        $questioncat = $questiongenerator->create_question_category([
+            'contextid' => $context->id,
+        ]);
+
+        // Assign the user to the role.
+        role_assign($roleid, $user->id, $context->id);
+
+        // Assign the capabilities to the role.
+        foreach ($capabilities as $capname => $capvalue) {
+            assign_capability($capname, $capvalue, $roleid, $context->id);
+        }
+        $context->mark_dirty();
+
+        // Create the question.
+        $question = $questiongenerator->create_question('truefalse', null, [
+            'category' => $questioncat->id,
+        ]);
+        $question = question_bank::load_question_data($question->id);
+
+        // The question generator does not support setting of the createdby for some reason.
+        $question->createdby = ($isowner) ? $user->id : $otheruser->id;
+
+        $this->setUser($user);
+        $result = question_has_capability_on($question, $capability);
+        $this->assertEquals($expect, $result);
+    }
+
+    /**
+     * Tests that question_has_capability_on throws an exception for wrong parameter types.
+     */
+    public function test_question_has_capability_on_wrong_param_type() {
+        // Create the test data.
+        $generator = $this->getDataGenerator();
+        $questiongenerator = $generator->get_plugin_generator('core_question');
+        $user = $generator->create_user();
+
+        $category = $generator->create_category();
+        $context = context_coursecat::instance($category->id);
+        $questioncat = $questiongenerator->create_question_category([
+            'contextid' => $context->id,
+        ]);
+
+        // Create the question.
+        $question = $questiongenerator->create_question('truefalse', null, [
+            'category' => $questioncat->id,
+        ]);
+        $question = question_bank::load_question_data($question->id);
+
+        // The question generator does not support setting of the createdby for some reason.
+        $question->createdby = $user->id;
+
+        $this->setUser($user);
+        $result = question_has_capability_on((string)$question->id, 'tag');
+        $this->assertFalse($result);
+
+        $this->expectException('coding_exception');
+        $this->expectExceptionMessage('$questionorid parameter needs to be an integer or an object.');
+        question_has_capability_on('one', 'tag');
+    }
 }
index 174efd9..c569cf6 100644 (file)
@@ -22,6 +22,8 @@ information provided here is intended especially for developers.
 * Scripts can define a constant NO_SITEPOLICY_CHECK and set it to true before requiring the main config.php file. It
   will make the require_login() skipping the test for the user's policyagreed status. This is useful for plugins that
   act as a site policy handler.
+* There is a new is_fulltext_search_supported() DML function. The default implementation returns false. This function
+  is used by 'Simple search' global search engine to determine if the database full-text search capabilities can be used.
 
 === 3.4 ===
 
index 0f500a6..ad681ef 100644 (file)
@@ -234,7 +234,14 @@ class xmldb_structure extends xmldb_object {
         // Normalize paths to compare them.
         $filepath = realpath($this->name); // File path comes in name.
         $filename = basename($filepath);
-        $structurepath = realpath($CFG->dirroot . DIRECTORY_SEPARATOR . $this->path . DIRECTORY_SEPARATOR . $filename);
+        $normalisedpath = $this->path;
+        if ($CFG->admin !== 'admin') {
+            $needle = 'admin/';
+            if (strpos($this->path, $needle) === 0) {
+                $normalisedpath = substr_replace($this->path, "$CFG->admin/", 0, strlen($needle));
+            }
+        }
+        $structurepath = realpath($CFG->dirroot . DIRECTORY_SEPARATOR . $normalisedpath . DIRECTORY_SEPARATOR . $filename);
         if ($filepath !== $structurepath) {
             $relativepath = dirname(str_replace(realpath($CFG->dirroot) . DIRECTORY_SEPARATOR, '', $filepath));
             $this->errormsg = 'PATH attribute does not match file directory: ' . $relativepath;
index c960552..060bf8d 100644 (file)
@@ -1428,3 +1428,32 @@ video::-webkit-media-text-track-display {
 .mediaplugin_videojs video {
   min-height: 32px;
 }
+
+/* MDL-61020: VideoJS timeline progress bar should not be flipped in RTL mode. */
+
+/* Prevent the progress bar from being flipped in RTL. */
+/*rtl:ignore*/
+.video-js .vjs-progress-holder .vjs-play-progress,
+.video-js .vjs-progress-holder .vjs-load-progress,
+.video-js .vjs-progress-holder .vjs-load-progress div {
+  left: 0;
+  right: auto;
+}
+/* Keep the video scrubber button at the end of the progress bar in RTL. */
+/*rtl:ignore*/
+.video-js .vjs-play-progress:before {
+  left: auto;
+  right: -0.5em;
+}
+/* Prevent the volume slider from being flipped in RTL. */
+/*rtl:ignore*/
+.video-js .vjs-volume-level {
+  left: 0;
+  right: auto;
+}
+/* Keep the volume slider handle at the end of the volume slider in RTL. */
+/*rtl:ignore*/
+.vjs-slider-horizontal .vjs-volume-level:before {
+  left: auto;
+  right: -0.5em;
+}
index 2065529..25246a0 100644 (file)
@@ -36,6 +36,16 @@ require_once($CFG->dirroot . '/lib/messagelib.php');
  */
 class api {
 
+    /**
+     * The action for reading a message.
+     */
+    const MESSAGE_ACTION_READ = 1;
+
+    /**
+     * The action for deleting a message.
+     */
+    const MESSAGE_ACTION_DELETED = 2;
+
     /**
      * Handles searching for messages in the message area.
      *
@@ -52,47 +62,33 @@ class api {
         $ufields = \user_picture::fields('u', array('lastaccess'), 'userfrom_id', 'userfrom_');
         $ufields2 = \user_picture::fields('u2', array('lastaccess'), 'userto_id', 'userto_');
 
-        // Get all the messages for the user.
-        $sql = "SELECT m.id, m.useridfrom, m.useridto, m.subject, m.fullmessage, m.fullmessagehtml, m.fullmessageformat,
-                       m.smallmessage, m.notification, m.timecreated, 0 as isread, $ufields, mc.blocked as userfrom_blocked,
-                       $ufields2, mc2.blocked as userto_blocked
-                  FROM {message} m
-                  JOIN {user} u
-                    ON m.useridfrom = u.id
-             LEFT JOIN {message_contacts} mc
-                    ON (mc.contactid = u.id AND mc.userid = ?)
-                  JOIN {user} u2
-                    ON m.useridto = u2.id
-             LEFT JOIN {message_contacts} mc2
-                    ON (mc2.contactid = u2.id AND mc2.userid = ?)
-                 WHERE ((useridto = ? AND timeusertodeleted = 0)
-                    OR (useridfrom = ? AND timeuserfromdeleted = 0))
-                   AND notification = 0
-                   AND u.deleted = 0
-                   AND u2.deleted = 0
-                   AND " . $DB->sql_like('smallmessage', '?', false) . "
-             UNION ALL
-                SELECT mr.id, mr.useridfrom, mr.useridto, mr.subject, mr.fullmessage, mr.fullmessagehtml, mr.fullmessageformat,
-                       mr.smallmessage, mr.notification, mr.timecreated, 1 as isread, $ufields, mc.blocked as userfrom_blocked,
-                       $ufields2, mc2.blocked as userto_blocked
-                  FROM {message_read} mr
-                  JOIN {user} u
-                    ON mr.useridfrom = u.id
-             LEFT JOIN {message_contacts} mc
-                    ON (mc.contactid = u.id AND mc.userid = ?)
-                  JOIN {user} u2
-                    ON mr.useridto = u2.id
-             LEFT JOIN {message_contacts} mc2
-                    ON (mc2.contactid = u2.id AND mc2.userid = ?)
-                 WHERE ((useridto = ? AND timeusertodeleted = 0)
-                    OR (useridfrom = ? AND timeuserfromdeleted = 0))
-                   AND notification = 0
+        $sql = "SELECT m.id, m.useridfrom, mcm.userid as useridto, m.subject, m.fullmessage, m.fullmessagehtml, m.fullmessageformat,
+                       m.smallmessage, m.timecreated, 0 as isread, $ufields, mcont.blocked as userfrom_blocked, $ufields2,
+                       mcont2.blocked as userto_blocked
+                  FROM {messages} m
+            INNER JOIN {user} u
+                    ON u.id = m.useridfrom
+            INNER JOIN {message_conversations} mc
+                    ON mc.id = m.conversationid
+            INNER JOIN {message_conversation_members} mcm
+                    ON mcm.conversationid = m.conversationid
+            INNER JOIN {user} u2
+                    ON u2.id = mcm.userid
+             LEFT JOIN {message_contacts} mcont
+                    ON (mcont.contactid = u.id AND mcont.userid = ?)
+             LEFT JOIN {message_contacts} mcont2
+                    ON (mcont2.contactid = u2.id AND mcont2.userid = ?)
+             LEFT JOIN {message_user_actions} mua
+                    ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
+                 WHERE (m.useridfrom = ? OR mcm.userid = ?)
+                   AND m.useridfrom != mcm.userid
                    AND u.deleted = 0
                    AND u2.deleted = 0
+                   AND mua.id is NULL
                    AND " . $DB->sql_like('smallmessage', '?', false) . "
               ORDER BY timecreated DESC";
-        $params = array($userid, $userid, $userid, $userid, '%' . $search . '%',
-                        $userid, $userid, $userid, $userid, '%' . $search . '%');
+
+        $params = array($userid, $userid, $userid, self::MESSAGE_ACTION_DELETED, $userid, $userid, '%' . $search . '%');
 
         // Convert the messages into searchable contacts with their last message being the message that was searched.
         $conversations = array();
@@ -266,173 +262,48 @@ class api {
     public static function get_conversations($userid, $limitfrom = 0, $limitnum = 20) {
         global $DB;
 
-        // The case statement is used to make sure the same key is generated
-        // whether a user sent or received a message (it's the same conversation).
-        // E.g. If there is a message from user 1 to user 2 and then from user 2 to user 1 the result set
-        // will group those into a single record, since 1 -> 2 and 2 -> 1 is the same conversation.
-        $case1 = $DB->sql_concat('useridfrom', "'-'", 'useridto');
-        $case2 = $DB->sql_concat('useridto', "'-'", 'useridfrom');
-        $convocase = "CASE WHEN useridfrom > useridto
-                        THEN $case1
-                        ELSE $case2 END";
-        $convosig = "$convocase AS convo_signature";
-
-        // This is a snippet to join the message tables and filter out any messages the user has deleted
-        // and ignore notifications. The fields are specified by name so that the union works on MySQL.
-        $allmessages = "SELECT
-                            id, useridfrom, useridto, subject, fullmessage, fullmessageformat,
-                            fullmessagehtml, smallmessage, notification, contexturl,
-                            contexturlname, timecreated, timeuserfromdeleted, timeusertodeleted,
-                            component, eventtype, 0 as timeread
-                        FROM {message}
-                        WHERE
-                            (useridto = ? AND timeusertodeleted = 0 AND notification = 0)
-                        UNION ALL
-                        SELECT
-                            id, useridfrom, useridto, subject, fullmessage, fullmessageformat,
-                            fullmessagehtml, smallmessage, notification, contexturl,
-                            contexturlname, timecreated, timeuserfromdeleted, timeusertodeleted,
-                            component, eventtype, 0 as timeread
-                        FROM {message}
-                        WHERE
-                            (useridfrom = ? AND timeuserfromdeleted = 0 AND notification = 0)
-                        UNION ALL
-                        SELECT
-                            id, useridfrom, useridto, subject, fullmessage, fullmessageformat,
-                            fullmessagehtml, smallmessage, notification, contexturl,
-                            contexturlname, timecreated, timeuserfromdeleted, timeusertodeleted,
-                            component, eventtype, timeread
-                        FROM {message_read}
-                        WHERE
-                            (useridto = ? AND timeusertodeleted = 0 AND notification = 0)
-                        UNION ALL
-                        SELECT
-                            id, useridfrom, useridto, subject, fullmessage, fullmessageformat,
-                            fullmessagehtml, smallmessage, notification, contexturl,
-                            contexturlname, timecreated, timeuserfromdeleted, timeusertodeleted,
-                            component, eventtype, timeread
-                        FROM {message_read}
-                        WHERE
-                            (useridfrom = ? AND timeuserfromdeleted = 0 AND notification = 0)";
-        $allmessagesparams = [$userid, $userid, $userid, $userid];
-
-        // Create a transaction to protect against concurrency issues.
-        $transaction = $DB->start_delegated_transaction();
-
-        // First we need to get the list of conversations from the database ordered by the conversation
-        // with the most recent message first.
-        //
-        // This query will join the two message tables and then group the results by the combination
-        // of useridfrom and useridto (the 'convo_signature').
-        $conversationssql = "SELECT $convosig, max(timecreated) as timecreated
-                             FROM ($allmessages) x
-                             GROUP BY $convocase
-                             ORDER BY timecreated DESC, max(id) DESC";
-        $conversationrecords = $DB->get_records_sql($conversationssql, $allmessagesparams, $limitfrom, $limitnum);
-
-        // This user has no conversations so we can return early here.
-        if (empty($conversationrecords)) {
-            $transaction->allow_commit();
-            return [];
-        }
+        // Get the last message from each conversation that the user belongs to.
+        $sql = "SELECT m.id, m.conversationid, m.useridfrom, mcm2.userid as useridto, m.smallmessage, m.timecreated
+                  FROM {messages} m
+            INNER JOIN (
+                          SELECT MAX(m.id) AS messageid
+                            FROM {messages} m
+                      INNER JOIN (
+                                      SELECT m.conversationid, MAX(m.timecreated) as maxtime
+                                        FROM {messages} m
+                                  INNER JOIN {message_conversation_members} mcm
+                                          ON mcm.conversationid = m.conversationid
+                                   LEFT JOIN {message_user_actions} mua
+                                          ON (mua.messageid = m.id AND mua.userid = :userid AND mua.action = :action)
+                                       WHERE mua.id is NULL
+                                         AND mcm.userid = :userid2
+                                    GROUP BY m.conversationid
+                                 ) maxmessage
+                               ON maxmessage.maxtime = m.timecreated AND maxmessage.conversationid = m.conversationid
+                         GROUP BY m.conversationid
+                       ) lastmessage
+                    ON lastmessage.messageid = m.id
+            INNER JOIN {message_conversation_members} mcm
+                    ON mcm.conversationid = m.conversationid
+            INNER JOIN {message_conversation_members} mcm2
+                    ON mcm2.conversationid = m.conversationid
+                 WHERE mcm.userid = m.useridfrom
+                   AND mcm.id != mcm2.id
+              ORDER BY m.timecreated DESC";
+        $messageset = $DB->get_recordset_sql($sql, ['userid' => $userid, 'action' => self::MESSAGE_ACTION_DELETED,
+            'userid2' => $userid], $limitfrom, $limitnum);
 
-        // Next we need to get the max id of the messages sent at the latest time for each conversation.
-        // This needs to be a separate query to above because there is no guarantee that the message with
-        // the highest id will also have the highest timecreated value (in fact that is fairly likely due
-        // to the split between the message tables).
-        //
-        // E.g. if we just added max(id) to the conversation query above and ran it on data like:
-        // id, userfrom, userto, timecreated
-        //  1,        1,      2,           2
-        //  2,        2,      1,           1
-        //
-        // Then the result of the query wo