Merge branch 'MDL-61020-master' of git://github.com/rezaies/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Tue, 3 Apr 2018 05:58:03 +0000 (13:58 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Tue, 3 Apr 2018 05:58:03 +0000 (13:58 +0800)
137 files changed:
.travis.yml
admin/settings/plugins.php
admin/tool/mobile/classes/api.php
admin/tool/mobile/classes/external.php
admin/tool/mobile/tests/externallib_test.php
admin/tool/usertours/classes/manager.php
admin/tool/usertours/classes/step.php
admin/user.php
analytics/tests/course_test.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
course/tests/behat/course_controls.feature
course/tests/behat/paged_course_navigation.feature
grade/export/lib.php
install/lang/sk/install.php
iplookup/tests/geoip_test.php
lang/en/message.php
lib/adminlib.php
lib/classes/event/course_module_completion_updated.php
lib/classes/event/message_deleted.php
lib/classes/event/message_sent.php
lib/classes/event/message_viewed.php
lib/classes/event/notification_sent.php [new file with mode: 0644]
lib/classes/event/notification_viewed.php [new file with mode: 0644]
lib/classes/message/manager.php
lib/classes/plugin_manager.php
lib/classes/task/messaging_cleanup_task.php
lib/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/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/questionlib_test.php
lib/upgrade.txt
lib/xmldb/xmldb_structure.php
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/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/feedback/classes/responses_table.php
mod/feedback/item/multichoicerated/lib.php
mod/feedback/lang/en/feedback.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/plagiarism_provider.php
privacy/classes/tests/request/content_writer.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/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/gapselect/tests/edit_form_test.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
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
version.php
webservice/lib.php

index 9e436f5..a1217e8 100644 (file)
@@ -248,7 +248,7 @@ script:
     - >
       if [ "$TASK" = 'PHPUNIT' ];
       then
-        vendor/bin/phpunit;
+        vendor/bin/phpunit --fail-on-risky --disallow-test-output --verbose;
       fi
 
     - >
index c55b1f1..9a2202f 100644 (file)
@@ -556,7 +556,7 @@ if ($hassiteconfig) {
     // Search engine selection.
     $temp->add(new admin_setting_heading('searchengineheading', new lang_string('searchengine', 'admin'), ''));
     $temp->add(new admin_setting_configselect('searchengine',
-                                new lang_string('selectsearchengine', 'admin'), '', 'solr', $engines));
+                                new lang_string('selectsearchengine', 'admin'), '', 'simpledb', $engines));
     $temp->add(new admin_setting_heading('searchoptionsheading', new lang_string('searchoptions', 'admin'), ''));
     $temp->add(new admin_setting_configcheckbox('searchindexwhendisabled',
             new lang_string('searchindexwhendisabled', 'admin'), new lang_string('searchindexwhendisabled_desc', 'admin'),
index 4841569..160c4f0 100644 (file)
@@ -151,6 +151,11 @@ class api {
             'tool_mobile_disabledfeatures' => get_config('tool_mobile', 'disabledfeatures'),
             'country' => clean_param($CFG->country, PARAM_NOTAGS),
             'agedigitalconsentverification' => \core_auth\digital_consent::is_age_digital_consent_verification_enabled(),
+            'autolang' => $CFG->autolang,
+            'lang' => clean_param($CFG->lang, PARAM_LANG),  // Avoid breaking WS because of incorrect package langs.
+            'langmenu' => $CFG->langmenu,
+            'langlist' => $CFG->langlist,
+            'locale' => $CFG->locale,
         );
 
         $typeoflogin = get_config('tool_mobile', 'typeoflogin');
index 65a5ccf..7df769b 100644 (file)
@@ -168,6 +168,12 @@ class external extends external_api {
                     (only if age verification is enabled).', VALUE_OPTIONAL),
                 'supportemail' => new external_value(PARAM_EMAIL, 'Site support contact email
                     (only if age verification is enabled).', VALUE_OPTIONAL),
+                'autolang' => new external_value(PARAM_INT, 'Whether to detect default language
+                    from browser setting.', VALUE_OPTIONAL),
+                'lang' => new external_value(PARAM_LANG, 'Default language for the site.', VALUE_OPTIONAL),
+                'langmenu' => new external_value(PARAM_INT, 'Whether the language menu should be displayed.', VALUE_OPTIONAL),
+                'langlist' => new external_value(PARAM_RAW, 'Languages on language menu.', VALUE_OPTIONAL),
+                'locale' => new external_value(PARAM_RAW, 'Sitewide locale.', VALUE_OPTIONAL),
                 'warnings' => new external_warnings(),
             )
         );
index 19483ee..e61657c 100644 (file)
@@ -88,6 +88,11 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
             'launchurl' => "$CFG->wwwroot/$CFG->admin/tool/mobile/launch.php",
             'country' => $CFG->country,
             'agedigitalconsentverification' => \core_auth\digital_consent::is_age_digital_consent_verification_enabled(),
+            'autolang' => $CFG->autolang,
+            'lang' => $CFG->lang,
+            'langmenu' => $CFG->langmenu,
+            'langlist' => $CFG->langlist,
+            'locale' => $CFG->locale,
             'warnings' => array()
         );
         $this->assertEquals($expected, $result);
@@ -101,6 +106,8 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
         set_config('logocompact', 'mock.png', 'core_admin');
         set_config('forgottenpasswordurl', 'mailto:fake@email.zy'); // Test old hack.
         set_config('agedigitalconsentverification', 1);
+        set_config('autolang', 1);
+        set_config('lang', 'a_b');  // Set invalid lang.
 
         list($authinstructions, $notusedformat) = external_format_text($authinstructions, FORMAT_MOODLE, $context->id);
         $expected['registerauth'] = 'email';
@@ -110,6 +117,8 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
         $expected['agedigitalconsentverification'] = true;
         $expected['supportname'] = $CFG->supportname;
         $expected['supportemail'] = $CFG->supportemail;
+        $expected['autolang'] = '1';
+        $expected['lang'] = ''; // Expect empty because it was set to an invalid lang.
 
         if ($logourl = $OUTPUT->get_logo_url()) {
             $expected['logourl'] = $logourl->out(false);
index b481d2b..15f142c 100644 (file)
@@ -819,7 +819,7 @@ class manager {
         $existingtourrecords->close();
 
         foreach ($shippedtours as $filename => $version) {
-            $filepath = $CFG->dirroot . '/admin/tool/usertours/tours/' . $filename;
+            $filepath = $CFG->dirroot . "/{$CFG->admin}/tool/usertours/tours/" . $filename;
             $tourjson = file_get_contents($filepath);
             $tour = self::import_tour_from_json($tourjson);
 
index 6049e8f..4819ac0 100644 (file)
@@ -483,10 +483,9 @@ class step {
             $record = $this->to_record();
             unset($record->id);
             $this->id = $DB->insert_record('tool_usertours_steps', $record);
+            $this->get_tour()->reset_step_sortorder();
         }
 
-        $this->get_tour()->reset_step_sortorder();
-
         $this->reload();
 
         // Notify of a change to the step configuration.
index 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 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 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 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 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 30f756a..b14741e 100644 (file)
@@ -2960,6 +2960,7 @@ class admin_setting_configselect extends admin_setting {
     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
         // Look for optgroup and single options.
         if (is_array($choices)) {
+            $this->choices = [];
             foreach ($choices as $key => $val) {
                 if (is_array($val)) {
                     $this->optgroups[$key] = $val;
index ab03bb9..f3f4697 100644 (file)
@@ -81,7 +81,7 @@ class course_module_completion_updated extends base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/report/completion/index.php', array('course' => $this->courseid));
+        return new \moodle_url('/report/progress/index.php', array('course' => $this->courseid));
     }
 
     /**
index 7d5087e..b991853 100644 (file)
@@ -32,7 +32,6 @@ defined('MOODLE_INTERNAL') || die();
  * @property-read array $other {
  *      Extra information about event.
  *
- *      - string $messagetable: the table we marked the message as deleted from (message/message_read).
  *      - int messageid: the id of the message.
  *      - int useridfrom: the id of the user who received the message.
  *      - int useridto: the id of the user who sent the message.
@@ -51,11 +50,11 @@ class message_deleted extends base {
      * @param int $userfromid the user who the message was from.
      * @param int $usertoid the user who the message was sent to.
      * @param int $userdeleted the user who deleted it.
-     * @param string $messagetable the table we are marking the message as deleted in.
      * @param int $messageid the id of the message that was deleted.
+     * @param int $muaid The id in the message_user_actions table
      * @return message_deleted
      */
-    public static function create_from_ids($userfromid, $usertoid, $userdeleted, $messagetable, $messageid) {
+    public static function create_from_ids($userfromid, $usertoid, $userdeleted, $messageid, $muaid) {
         // Check who was deleting the message.
         if ($userdeleted == $userfromid) {
             $relateduserid = $usertoid;
@@ -66,11 +65,11 @@ class message_deleted extends base {
         // We set the userid to the user who deleted the message, nothing to do
         // with whether or not they sent or received the message.
         $event = self::create(array(
+            'objectid' => $muaid,
             'userid' => $userdeleted,
             'context' => \context_system::instance(),
             'relateduserid' => $relateduserid,
             'other' => array(
-                'messagetable' => $messagetable,
                 'messageid' => $messageid,
                 'useridfrom' => $userfromid,
                 'useridto' => $usertoid
@@ -84,7 +83,8 @@ class message_deleted extends base {
      * Init method.
      */
     protected function init() {
-        $this->data['crud'] = 'u';
+        $this->data['objecttable'] = 'message_user_actions';
+        $this->data['crud'] = 'c';
         $this->data['edulevel'] = self::LEVEL_OTHER;
     }
 
@@ -126,10 +126,6 @@ class message_deleted extends base {
             throw new \coding_exception('The \'relateduserid\' must be set.');
         }
 
-        if (!isset($this->other['messagetable'])) {
-            throw new \coding_exception('The \'messagetable\' value must be set in other.');
-        }
-
         if (!isset($this->other['messageid'])) {
             throw new \coding_exception('The \'messageid\' value must be set in other.');
         }
@@ -143,11 +139,13 @@ class message_deleted extends base {
         }
     }
 
+    public static function get_objectid_mapping() {
+        return array('db' => 'message_user_actions', 'restore' => base::NOT_MAPPED);
+    }
+
     public static function get_other_mapping() {
         // Messages are not backed up, so no need to map them on restore.
         $othermapped = array();
-        // The messageid table varies so it cannot be mapped.
-        $othermapped['messageid'] = array('db' => base::NOT_MAPPED, 'restore' => base::NOT_MAPPED);
         $othermapped['useridfrom'] = array('db' => 'user', 'restore' => base::NOT_MAPPED);
         $othermapped['useridto'] = array('db' => 'user', 'restore' => base::NOT_MAPPED);
         return $othermapped;
index e330919..9b3a8c5 100644 (file)
@@ -32,7 +32,6 @@ defined('MOODLE_INTERNAL') || die();
  * @property-read array $other {
  *      Extra information about event.
  *
- *      - int messageid: the id of the message.
  *      - int courseid: the id of the related course.
  * }
  *
@@ -69,14 +68,11 @@ class message_sent extends base {
         }
 
         $event = self::create(array(
+            'objectid' => $messageid,
             'userid' => $userfromid,
             'context' => \context_system::instance(),
             'relateduserid' => $usertoid,
             'other' => array(
-                // In earlier versions it can either be the id in the 'message_read' or 'message' table.
-                // Now it is always the id from 'message' table. Please note that the record is still moved
-                // to the 'message_read' table later when message marked as read.
-                'messageid' => $messageid,
                 'courseid' => $courseid
             )
         ));
@@ -88,6 +84,7 @@ class message_sent extends base {
      * Init method.
      */
     protected function init() {
+        $this->data['objecttable'] = 'messages';
         $this->data['crud'] = 'c';
         $this->data['edulevel'] = self::LEVEL_OTHER;
     }
@@ -133,8 +130,9 @@ class message_sent extends base {
         // The add_to_log function was only ever called when we sent a message from one user to another. We do not want
         // to return the legacy log data if we are sending a system message, so check that the userid is valid.
         if (\core_user::is_real_user($this->userid)) {
+            $messageid = $this->other['messageid'] ?? $this->objectid; // For BC we may have 'messageid' in other.
             return array(SITEID, 'message', 'write', 'index.php?user=' . $this->userid . '&id=' . $this->relateduserid .
-                '&history=1#m' . $this->other['messageid'], $this->userid);
+                '&history=1#m' . $messageid, $this->userid);
         }
 
         return null;
@@ -153,26 +151,18 @@ class message_sent extends base {
             throw new \coding_exception('The \'relateduserid\' must be set.');
         }
 
-        if (!isset($this->other['messageid'])) {
-            throw new \coding_exception('The \'messageid\' value must be set in other.');
-        }
-
         if (!isset($this->other['courseid'])) {
             throw new \coding_exception('The \'courseid\' value must be set in other.');
         }
     }
 
     public static function get_objectid_mapping() {
-        // Messages are not backed up, so no need to map them.
-        return false;
+        return array('db' => 'messages', 'restore' => base::NOT_MAPPED);
     }
 
     public static function get_other_mapping() {
-        // Messages are not backed up, so no need to map them on restore.
         $othermapped = array();
-        // The messages table could vary for older events - so cannot be mapped.
-        $othermapped['messageid'] = array('db' => base::NOT_MAPPED, 'restore' => base::NOT_MAPPED);
-        $othermapped['courseid'] = array('db' => base::NOT_MAPPED, 'restore' => base::NOT_MAPPED);
+        $othermapped['courseid'] = array('db' => 'course', 'restore' => base::NOT_MAPPED);
         return $othermapped;
     }
 }
index 8e12947..35ae1ff 100644 (file)
@@ -46,7 +46,7 @@ class message_viewed extends base {
      * Init method.
      */
     protected function init() {
-        $this->data['objecttable'] = 'message_read';
+        $this->data['objecttable'] = 'message_user_actions';
         $this->data['crud'] = 'c';
         $this->data['edulevel'] = self::LEVEL_OTHER;
     }
@@ -97,15 +97,13 @@ class message_viewed extends base {
     }
 
     public static function get_objectid_mapping() {
-        // Messages are not backed up, so no need to map them.
-        return array('db' => 'message_read', 'restore' => base::NOT_MAPPED);
+        return array('db' => 'message_user_actions', 'restore' => base::NOT_MAPPED);
     }
 
     public static function get_other_mapping() {
         // Messages are not backed up, so no need to map them on restore.
         $othermapped = array();
-        // The messages table could vary for older events - so cannot be mapped.
-        $othermapped['messageid'] = array('db' => base::NOT_MAPPED, 'restore' => base::NOT_MAPPED);
+        $othermapped['messageid'] = array('db' => 'messages', 'restore' => base::NOT_MAPPED);
         return $othermapped;
     }
 }
diff --git a/lib/classes/event/notification_sent.php b/lib/classes/event/notification_sent.php
new file mode 100644 (file)
index 0000000..1c2080c
--- /dev/null
@@ -0,0 +1,144 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Notification sent event.
+ *
+ * @package    core
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Notification sent event class.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      - int courseid: the id of the related course.
+ * }
+ *
+ * @package    core
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class notification_sent extends base {
+
+    /**
+     * Create event using ids.
+     *
+     * @param int $userfromid
+     * @param int $usertoid
+     * @param int $notificationid
+     * @param int $courseid course id the event is related with - SITEID if no relation exists.
+     * @return notification_sent
+     */
+    public static function create_from_ids($userfromid, $usertoid, $notificationid, $courseid) {
+        // We may be sending a notification from the 'noreply' address, which means we are not actually sending a
+        // notification from a valid user. In this case, we will set the userid to 0.
+        // Check if the userid is valid.
+        if (!\core_user::is_real_user($userfromid)) {
+            $userfromid = 0;
+        }
+
+        $event = self::create(
+            [
+                'objectid' => $notificationid,
+                'userid' => $userfromid,
+                'context' => \context_system::instance(),
+                'relateduserid' => $usertoid,
+                'other' => [
+                    'courseid' => $courseid
+                ]
+            ]
+        );
+
+        return $event;
+    }
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'notifications';
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventnotificationsent', 'message');
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/message/output/popup/notifications.php', array('notificationid' => $this->objectid));
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        // Check if we are sending from a valid user.
+        if (\core_user::is_real_user($this->userid)) {
+            return "The user with id '$this->userid' sent a notification to the user with id '$this->relateduserid'.";
+        }
+
+        return "A notification was sent by the system to the user with id '$this->relateduserid'.";
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+
+        if (!isset($this->other['courseid'])) {
+            throw new \coding_exception('The \'courseid\' value must be set in other.');
+        }
+    }
+
+    public static function get_objectid_mapping() {
+        return array('db' => 'notifications', 'restore' => base::NOT_MAPPED);
+    }
+
+    public static function get_other_mapping() {
+        $othermapped = array();
+        $othermapped['courseid'] = array('db' => 'course', 'restore' => base::NOT_MAPPED);
+        return $othermapped;
+    }
+}
diff --git a/lib/classes/event/notification_viewed.php b/lib/classes/event/notification_viewed.php
new file mode 100644 (file)
index 0000000..f33c333
--- /dev/null
@@ -0,0 +1,126 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Notification viewed event.
+ *
+ * @package    core
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Notification viewed event class.
+ *
+ * @package    core
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class notification_viewed extends base {
+
+    /**
+     * Create event using ids.
+     *
+     * @param int $userfromid
+     * @param int $usertoid
+     * @param int $notificationid
+     * @return notification_viewed
+     */
+    public static function create_from_ids($userfromid, $usertoid, $notificationid) {
+        // We may be sending a notification from the 'noreply' address, which means we are not actually sending a
+        // notification from a valid user. In this case, we will set the userid to 0.
+        // Check if the userid is valid.
+        if (!\core_user::is_real_user($userfromid)) {
+            $userfromid = 0;
+        }
+
+        // Get the context for the user who received the notification.
+        $context = \context_user::instance($usertoid, IGNORE_MISSING);
+        // If the user no longer exists the context value will be false, in this case use the system context.
+        if ($context === false) {
+            $context = \context_system::instance();
+        }
+
+        $event = self::create(
+            [
+                'objectid' => $notificationid,
+                'userid' => $usertoid, // Using the user who read the notification as they are the ones performing the action.
+                'context' => $context,
+                'relateduserid' => $userfromid
+            ]
+        );
+
+        return $event;
+    }
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'notifications';
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventnotificationviewed', 'message');
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/message/output/popup/notifications.php', array('notificationid' => $this->objectid));
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' read a notification from the user with id '$this->relateduserid'.";
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+    }
+
+    public static function get_objectid_mapping() {
+        return array('db' => 'notifications', 'restore' => base::NOT_MAPPED);
+    }
+}
index d08489d..c05e14d 100644 (file)
@@ -54,7 +54,7 @@ class manager {
      * @param \core\message\message $eventdata fully prepared event data for processors
      * @param \stdClass $savemessage the message saved in 'message' table
      * @param array $processorlist list of processors for target user
-     * @return int $messageid the id from 'message' or 'message_read' table (false is not returned)
+     * @return int $messageid the id from 'messages' (false is not returned)
      */
     public static function send_message($eventdata, \stdClass $savemessage, array $processorlist) {
         global $CFG;
@@ -77,27 +77,35 @@ class manager {
         require_once($CFG->dirroot.'/message/lib.php'); // This is most probably already included from messagelib.php file.
 
         if (empty($processorlist)) {
-            // Trigger event for sending a message - we need to do this before marking as read!
-            \core\event\message_sent::create_from_ids(
-                $eventdata->userfrom->id,
-                $eventdata->userto->id,
-                $savemessage->id,
-                $eventdata->courseid
+            // Trigger event for sending a message or notification - we need to do this before marking as read!
+            if ($eventdata->notification) {
+                \core\event\notification_sent::create_from_ids(
+                    $eventdata->userfrom->id,
+                    $eventdata->userto->id,
+                    $savemessage->id,
+                    $eventdata->courseid
+                )->trigger();
+            } else { // Must be a message.
+                \core\event\message_sent::create_from_ids(
+                    $eventdata->userfrom->id,
+                    $eventdata->userto->id,
+                    $savemessage->id,
+                    $eventdata->courseid
                 )->trigger();
+            }
 
-            if ($savemessage->notification or empty($CFG->messaging)) {
+            if ($eventdata->notification or empty($CFG->messaging)) {
                 // If they have deselected all processors and its a notification mark it read. The user doesn't want to be bothered.
                 // The same goes if the messaging is completely disabled.
-                // We cannot insert directly to the message_read table because we want to get all events in proper order!
-                $messageid = message_mark_message_read($savemessage, time(), true);
-
-            } else {
-                // Just add it to the list of unread messages, there is no way it could be delivered to them,
-                // but they can read it via the messaging UI later.
-                $messageid = $savemessage->id;
+                if ($eventdata->notification) {
+                    $savemessage->timeread = null;
+                    \core_message\api::mark_notification_as_read($savemessage);
+                } else {
+                    \core_message\api::mark_message_as_read($eventdata->userto->id, $savemessage);
+                }
             }
 
-            return $messageid;
+            return $savemessage->id;
         }
 
         // Let the manager do the sending or buffering when db transaction in progress.
@@ -133,7 +141,6 @@ class manager {
             return $savemessage->id;
         }
 
-        $failed = false;
         foreach ($processorlist as $procname) {
             // Let new messaging class add custom content based on the processor.
             $proceventdata = ($eventdata instanceof message) ? $eventdata->get_eventobject_for_processor($procname) : $eventdata;
@@ -142,40 +149,38 @@ class manager {
             $processor = \core_message\api::get_processed_processor_object($stdproc);
             if (!$processor->object->send_message($proceventdata)) {
                 debugging('Error calling message processor ' . $procname);
-                $failed = true;
-                // Previously the $messageid = false here was overridden
-                // by other processors and message_mark_message_read() below.
             }
         }
 
-        // Trigger event for sending a message - must be done before marking as read.
-        \core\event\message_sent::create_from_ids(
-            $eventdata->userfrom->id,
-            $eventdata->userto->id,
-            $savemessage->id,
-            $eventdata->courseid
+        // Trigger event for sending a message or notification - we need to do this before marking as read!
+        if ($eventdata->notification) {
+            \core\event\notification_sent::create_from_ids(
+                $eventdata->userfrom->id,
+                $eventdata->userto->id,
+                $savemessage->id,
+                $eventdata->courseid
             )->trigger();
+        } else { // Must be a message.
+            \core\event\message_sent::create_from_ids(
+                $eventdata->userfrom->id,
+                $eventdata->userto->id,
+                $savemessage->id,
+                $eventdata->courseid
+            )->trigger();
+        }
 
         if (empty($CFG->messaging)) {
-            // If messaging is disabled and they previously had forum notifications handled by the popup processor
-            // or any processor that puts a row in message_working then the notification will remain forever
-            // unread. To prevent this mark the message read if messaging is disabled.
-            $messageid = message_mark_message_read($savemessage, time());
-
-        } else if ($failed) {
-            // Something failed, better keep it as unread then.
-            $messageid = $savemessage->id;
-
-        } else if ($DB->count_records('message_working', array('unreadmessageid' => $savemessage->id)) == 0) {
-            // If there is no more processors that want to process this we can move message to message_read.
-            $messageid = message_mark_message_read($savemessage, time(), true);
-
-        } else {
-            // Some processor is still working on the data, let's keep it unread.
-            $messageid = $savemessage->id;
+            // If they have deselected all processors and its a notification mark it read. The user doesn't want to be bothered.
+            // The same goes if the messaging is completely disabled.
+            if ($eventdata->notification) {
+                $savemessage->timeread = null;
+                \core_message\api::mark_notification_as_read($savemessage);
+            } else {
+                \core_message\api::mark_message_as_read($eventdata->userto->id, $savemessage);
+            }
         }
 
-        return $messageid;
+        return $savemessage->id;
     }
 
     /**
index 7af9689..302e536 100644 (file)
@@ -1884,7 +1884,7 @@ class core_plugin_manager {
             ),
 
             'search' => array(
-                'solr'
+                'simpledb', 'solr'
             ),
 
             'scormreport' => array(
@@ -1908,7 +1908,7 @@ class core_plugin_manager {
                 'dbtransfer', 'filetypes', 'generator', 'health', 'httpsreplace', 'innodb', 'installaddon',
                 'langimport', 'log', 'lp', 'lpimportcsv', 'lpmigrate', 'messageinbound', 'mobile', 'multilangupgrade',
                 'monitor', 'oauth2', 'phpunit', 'profiling', 'recyclebin', 'replace', 'spamcleaner', 'task', 'templatelibrary',
-                'unittest', 'uploadcourse', 'uploaduser', 'unsuproles', 'usertours', 'xmldb'
+                'uploadcourse', 'uploaduser', 'unsuproles', 'usertours', 'xmldb'
             ),
 
             'webservice' => array(
index 4debce3..6d1a2c4 100644 (file)
@@ -50,7 +50,7 @@ class messaging_cleanup_task extends scheduled_task {
         if (!empty($CFG->messagingdeletereadnotificationsdelay)) {
             $notificationdeletetime = $timenow - $CFG->messagingdeletereadnotificationsdelay;
             $params = array('notificationdeletetime' => $notificationdeletetime);
-            $DB->delete_records_select('message_read', 'notification=1 AND timeread<:notificationdeletetime', $params);
+            $DB->delete_records_select('notifications', 'timeread < :notificationdeletetime', $params);
         }
 
     }
index 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) {
index ec42975..cc96a9a 100644 (file)
@@ -1175,6 +1175,9 @@ class external_settings {
     /** @var string In which file should the urls be rewritten */
     private $file = 'webservice/pluginfile.php';
 
+    /** @var string The session lang */
+    private $lang = '';
+
     /**
      * Constructor - protected - can not be instanciated
      */
@@ -1277,6 +1280,24 @@ class external_settings {
     public function get_file() {
         return $this->file;
     }
+
+    /**
+     * Set lang
+     *
+     * @param string $lang
+     */
+    public function set_lang($lang) {
+        $this->lang = $lang;
+    }
+
+    /**
+     * Get lang
+     *
+     * @return string
+     */
+    public function get_lang() {
+        return $this->lang;
+    }
 }
 
 /**
index d575cbc..0b5d300 100644 (file)
@@ -127,33 +127,53 @@ function message_send($eventdata) {
         $userstate = 'loggedoff';
     }
 
-    // Create the message object
-    $savemessage = new stdClass();
-    $savemessage->courseid          = $eventdata->courseid;
-    $savemessage->useridfrom        = $eventdata->userfrom->id;
-    $savemessage->useridto          = $eventdata->userto->id;
-    $savemessage->subject           = $eventdata->subject;
-    $savemessage->fullmessage       = $eventdata->fullmessage;
-    $savemessage->fullmessageformat = $eventdata->fullmessageformat;
-    $savemessage->fullmessagehtml   = $eventdata->fullmessagehtml;
-    $savemessage->smallmessage      = $eventdata->smallmessage;
-    $savemessage->notification      = $eventdata->notification;
-    $savemessage->eventtype         = $eventdata->name;
-    $savemessage->component         = $eventdata->component;
-
-    if (!empty($eventdata->contexturl)) {
-        $savemessage->contexturl = (string)$eventdata->contexturl;
-    } else {
-        $savemessage->contexturl = null;
-    }
+    // Check if we are creating a notification or message.
+    if ($eventdata->notification) {
+        $table = 'notifications';
+
+        $tabledata = new stdClass();
+        $tabledata->useridfrom = $eventdata->userfrom->id;
+        $tabledata->useridto = $eventdata->userto->id;
+        $tabledata->subject = $eventdata->subject;
+        $tabledata->fullmessage = $eventdata->fullmessage;
+        $tabledata->fullmessageformat = $eventdata->fullmessageformat;
+        $tabledata->fullmessagehtml = $eventdata->fullmessagehtml;
+        $tabledata->smallmessage = $eventdata->smallmessage;
+        $tabledata->eventtype = $eventdata->name;
+        $tabledata->component = $eventdata->component;
+
+        if (!empty($eventdata->contexturl)) {
+            $tabledata->contexturl = (string)$eventdata->contexturl;
+        } else {
+            $tabledata->contexturl = null;
+        }
 
-    if (!empty($eventdata->contexturlname)) {
-        $savemessage->contexturlname = (string)$eventdata->contexturlname;
+        if (!empty($eventdata->contexturlname)) {
+            $tabledata->contexturlname = (string)$eventdata->contexturlname;
+        } else {
+            $tabledata->contexturlname = null;
+        }
     } else {
-        $savemessage->contexturlname = null;
+        $table = 'messages';
+
+        if (!$conversationid = \core_message\api::get_conversation_between_users([$eventdata->userfrom->id,
+                $eventdata->userto->id])) {
+            $conversationid = \core_message\api::create_conversation_between_users([$eventdata->userfrom->id,
+                $eventdata->userto->id]);
+        }
+
+        $tabledata = new stdClass();
+        $tabledata->courseid = $eventdata->courseid;
+        $tabledata->useridfrom = $eventdata->userfrom->id;
+        $tabledata->conversationid = $conversationid;
+        $tabledata->subject = $eventdata->subject;
+        $tabledata->fullmessage = $eventdata->fullmessage;
+        $tabledata->fullmessageformat = $eventdata->fullmessageformat;
+        $tabledata->fullmessagehtml = $eventdata->fullmessagehtml;
+        $tabledata->smallmessage = $eventdata->smallmessage;
     }
 
-    $savemessage->timecreated = time();
+    $tabledata->timecreated = time();
 
     if (PHPUNIT_TEST and class_exists('phpunit_util')) {
         // Add some more tests to make sure the normal code can actually work.
@@ -173,9 +193,21 @@ function message_send($eventdata) {
         unset($messageproviders);
         // Now ask phpunit if it wants to catch this message.
         if (phpunit_util::is_redirecting_messages()) {
-            $savemessage->timeread = time();
-            $messageid = $DB->insert_record('message_read', $savemessage);
-            $message = $DB->get_record('message_read', array('id'=>$messageid));
+            $messageid = $DB->insert_record($table, $tabledata);
+            $message = $DB->get_record($table, array('id' => $messageid));
+
+            // Add the useridto attribute for BC.
+            $message->useridto = $eventdata->userto->id;
+
+            // Mark the message/notification as read.
+            if ($eventdata->notification) {
+                \core_message\api::mark_notification_as_read($message);
+            } else {
+                \core_message\api::mark_message_as_read($eventdata->userto->id, $message);
+            }
+
+            // Unit tests need this detail.
+            $message->notification = $eventdata->notification;
             phpunit_util::message_sent($message);
             return $messageid;
         }
@@ -183,7 +215,7 @@ function message_send($eventdata) {
 
     // Fetch enabled processors.
     // If we are dealing with a message some processors may want to handle it regardless of user and site settings.
-    if (empty($savemessage->notification)) {
+    if (!$eventdata->notification) {
         $processors = array_filter(get_message_processors(false), function($processor) {
             if ($processor->object->force_process_messages()) {
                 return true;
@@ -226,7 +258,7 @@ function message_send($eventdata) {
         }
 
         // Populate the list of processors we will be using
-        if (empty($savemessage->notification) && $processor->object->force_process_messages()) {
+        if (!$eventdata->notification && $processor->object->force_process_messages()) {
             $processorlist[] = $processor->name;
         } else if ($permitted == 'forced' && $userisconfigured) {
             // An admin is forcing users to use this message processor. Use this processor unconditionally.
@@ -248,20 +280,20 @@ function message_send($eventdata) {
     }
 
     // Only cache messages, not notifications.
-    if (empty($savemessage->notification)) {
+    if (!$eventdata->notification) {
         // Cache the timecreated value of the last message between these two users.
         $cache = cache::make('core', 'message_time_last_message_between_users');
-        $key = \core_message\helper::get_last_message_time_created_cache_key($savemessage->useridfrom,
-            $savemessage->useridto);
-        $cache->set($key, $savemessage->timecreated);
+        $key = \core_message\helper::get_last_message_time_created_cache_key($eventdata->userfrom->id,
+            $eventdata->userto->id);
+        $cache->set($key, $tabledata->timecreated);
     }
 
     // Store unread message just in case we get a fatal error any time later.
-    $savemessage->id = $DB->insert_record('message', $savemessage);
-    $eventdata->savedmessageid = $savemessage->id;
+    $tabledata->id = $DB->insert_record($table, $tabledata);
+    $eventdata->savedmessageid = $tabledata->id;
 
     // Let the manager do the sending or buffering when db transaction in progress.
-    return \core\message\manager::send_message($eventdata, $savemessage, $processorlist);
+    return \core\message\manager::send_message($eventdata, $tabledata, $processorlist);
 }
 
 
index a8259ae..978720a 100644 (file)
@@ -4071,9 +4071,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.
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 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 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 would be:
-        // convo_signature, timecreated, id
-        //             2-1,           2,  2
-        //
-        // That would be incorrect since the message with id 2 actually has a lower timecreated. Hence why
-        // the two queries need to be split.
-        //
-        // The same result could also be achieved with an inner join in a single query however we're specifically
-        // avoiding multiple joins in the messaging queries because of the size of the messaging tables.
-        $whereclauses = [];
-        $createdtimes = [];
-        foreach ($conversationrecords as $convoid => $record) {
-            $whereclauses[] = "($convocase = '$convoid' AND timecreated = {$record->timecreated})";
-            $createdtimes[] = $record->timecreated;
-        }
-        $messageidwhere = implode(' OR ', $whereclauses);
-        list($timecreatedsql, $timecreatedparams) = $DB->get_in_or_equal($createdtimes);
-
-        $allmessagestimecreated = "SELECT id, useridfrom, useridto, timecreated
-                        FROM {message}
-                        WHERE
-                            (useridto = ? AND timeusertodeleted = 0 AND notification = 0)
-                            AND timecreated $timecreatedsql
-                        UNION ALL
-                        SELECT id, useridfrom, useridto, timecreated
-                        FROM {message}
-                        WHERE
-                            (useridfrom = ? AND timeuserfromdeleted = 0 AND notification = 0)
-                            AND timecreated $timecreatedsql
-                        UNION ALL
-                        SELECT id, useridfrom, useridto, timecreated
-                        FROM {message_read}
-                        WHERE
-                            (useridto = ? AND timeusertodeleted = 0 AND notification = 0)
-                            AND timecreated $timecreatedsql
-                        UNION ALL
-                        SELECT id, useridfrom, useridto, timecreated
-                        FROM {message_read}
-                        WHERE
-                            (useridfrom = ? AND timeuserfromdeleted = 0 AND notification = 0)
-                            AND timecreated $timecreatedsql";
-        $messageidsql = "SELECT $convosig, max(id) as id, timecreated
-                         FROM ($allmessagestimecreated) x
-                         WHERE $messageidwhere
-                         GROUP BY $convocase, timecreated";
-        $messageidparams = array_merge([$userid], $timecreatedparams, [$userid], $timecreatedparams,
-                [$userid], $timecreatedparams, [$userid], $timecreatedparams);
-        $messageidrecords = $DB->get_records_sql($messageidsql, $messageidparams);
-
-        // Ok, let's recap. We've pulled a descending ordered list of conversations by latest time created
-        // for the given user. For each of those conversations we've grabbed the max id for messages
-        // created at that time.
-        //
-        // So at this point we have the list of ids for the most recent message in each of the user's most
-        // recent conversations. Now we need to pull all of the message and user data for each message id.
-        $whereclauses = [];
-        foreach ($messageidrecords as $record) {
-            $whereclauses[] = "(id = {$record->id} AND timecreated = {$record->timecreated})";
-        }
-        $messagewhere = implode(' OR ', $whereclauses);
-        $messagesunionsql = "SELECT
-                                id, useridfrom, useridto, smallmessage, 0 as timeread
-                            FROM {message}
-                            WHERE
-                                {$messagewhere}
-                            UNION ALL
-                            SELECT
-                                id, useridfrom, useridto, smallmessage, timeread
-                            FROM {message_read}
-                            WHERE
-                                {$messagewhere}";
-        $messagesql = "SELECT $convosig, m.smallmessage, m.id, m.useridto, m.useridfrom, m.timeread
-                       FROM ($messagesunionsql) m";
-
-        // We need to handle the case where the $messageids contains two ids from the same conversation
-        // (which can happen because there can be id clashes between the read and unread tables). In
-        // this case we will prioritise the unread message.
-        $messageset = $DB->get_recordset_sql($messagesql, $allmessagesparams);
         $messages = [];
         foreach ($messageset as $message) {
-            $id = $message->convo_signature;
-            if (!isset($messages[$id]) || empty($message->timeread)) {
-                $messages[$id] = $message;
-            }
+            $messages[$message->id] = $message;
         }
         $messageset->close();
 
+        // If there are no messages return early.
+        if (empty($messages)) {
+            return [];
+        }
+
         // We need to pull out the list of other users that are part of each of these conversations. This
         // needs to be done in a separate query to avoid doing a join on the messages tables and the user
         // tables because on large sites these tables are massive which results in extremely slow
@@ -441,77 +312,64 @@ class api {
             return ($message->useridfrom == $userid) ? $message->useridto : $message->useridfrom;
         }, array_values($messages));
 
+        // Ok, let's get the other members in the conversations.
         list($useridsql, $usersparams) = $DB->get_in_or_equal($otheruserids);
-        $userfields = \user_picture::fields('', array('lastaccess'));
+        $userfields = \user_picture::fields('u', array('lastaccess'));
         $userssql = "SELECT $userfields
-                     FROM {user}
-                     WHERE id $useridsql
-                       AND deleted = 0";
+                       FROM {user} u
+                      WHERE id $useridsql
+                        AND deleted = 0";
         $otherusers = $DB->get_records_sql($userssql, $usersparams);
 
-        // Similar to the above use case, we need to pull the contact information and again this has
-        // specifically been separated into another query to avoid having to do joins on the message
-        // tables.
+        // If there are no other users (user may have been deleted), then do not continue.
+        if (empty($otherusers)) {
+            return [];
+        }
+
         $contactssql = "SELECT contactid, blocked
-                        FROM {message_contacts}
-                        WHERE userid = ? AND contactid $useridsql";
-        $contacts = $DB->get_records_sql($contactssql, array_merge([$userid], $otheruserids));
+                          FROM {message_contacts}
+                         WHERE userid = ?
+                           AND contactid $useridsql";
+        $contacts = $DB->get_records_sql($contactssql, array_merge([$userid], $usersparams));
 
         // Finally, let's get the unread messages count for this user so that we can add them
-        // to the conversation.
-        $unreadcountssql = 'SELECT useridfrom, count(*) as count
-                            FROM {message}
-                            WHERE useridto = ?
-                                AND timeusertodeleted = 0
-                                AND notification = 0
-                            GROUP BY useridfrom';
-        $unreadcounts = $DB->get_records_sql($unreadcountssql, [$userid]);
-
-        // We can close off the transaction now.
-        $transaction->allow_commit();
-
-        // Now we need to order the messages back into the same order of the conversations.
-        $orderedconvosigs = array_keys($conversationrecords);
-        usort($messages, function($a, $b) use ($orderedconvosigs) {
-            $aindex = array_search($a->convo_signature, $orderedconvosigs);
-            $bindex = array_search($b->convo_signature, $orderedconvosigs);
-
-            return ($aindex < $bindex) ? -1 : 1;
-        });
-
-        // Preload the contexts before we construct the conversation to prevent the
-        // create_contact helper from needing to query the DB so often.
-        $ctxselect = \context_helper::get_preload_record_columns_sql('ctx');
-        $sql = "SELECT {$ctxselect}
-                FROM {context} ctx
-                WHERE ctx.contextlevel = ? AND
-                ctx.instanceid {$useridsql}";
-        $contexts = [];
-        $contexts = $DB->get_records_sql($sql, array_merge([CONTEXT_USER], $usersparams));
-        foreach ($contexts as $context) {
-            \context_helper::preload_from_record($context);
-        }
-
+        // to the conversation. Remember we need to ignore the messages the user sent.
+        $unreadcountssql = 'SELECT m.useridfrom, count(m.id) as count
+                              FROM {messages} m
+                        INNER JOIN {message_conversations} mc
+                                ON mc.id = m.conversationid
+                        INNER JOIN {message_conversation_members} mcm
+                                ON m.conversationid = mcm.conversationid
+                         LEFT JOIN {message_user_actions} mua
+                                ON (mua.messageid = m.id AND mua.userid = ? AND
+                                   (mua.action = ? OR mua.action = ?))
+                             WHERE mcm.userid = ?
+                               AND m.useridfrom != ?
+                               AND mua.id is NULL
+                          GROUP BY useridfrom';
+        $unreadcounts = $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, self::MESSAGE_ACTION_DELETED,
+            $userid, $userid]);
+
+        // Get rid of the table prefix.
+        $userfields = str_replace('u.', '', $userfields);
         $userproperties = explode(',', $userfields);
         $arrconversations = array();
-        // The last step now is to bring all of the data we've gathered together to create
-        // a conversation (or contact, as the API is named...).
         foreach ($messages as $message) {
             $conversation = new \stdClass();
             $otheruserid = ($message->useridfrom == $userid) ? $message->useridto : $message->useridfrom;
             $otheruser = isset($otherusers[$otheruserid]) ? $otherusers[$otheruserid] : null;
             $contact = isset($contacts[$otheruserid]) ? $contacts[$otheruserid] : null;
 
+            // It's possible the other user was deleted, so, skip.
+            if (is_null($otheruser)) {
+                continue;
+            }
+
             // Add the other user's information to the conversation, if we have one.
             foreach ($userproperties as $prop) {
                 $conversation->$prop = ($otheruser) ? $otheruser->$prop : null;
             }
 
-            // Do not process a conversation with a deleted user.
-            if (empty($conversation->id)) {
-                continue;
-            }
-
             // Add the contact's information, if we have one.
             $conversation->blocked = ($contact) ? $contact->blocked : null;
 
@@ -562,6 +420,73 @@ class api {
         return $arrcontacts;
     }
 
+    /**
+     * Returns the an array of the users the given user is in a conversation
+     * with who are a contact and the number of unread messages.
+     *
+     * @param int $userid The user id
+     * @param int $limitfrom
+     * @param int $limitnum
+     * @return array
+     */
+    public static function get_contacts_with_unread_message_count($userid, $limitfrom = 0, $limitnum = 0) {
+        global $DB;
+
+        $userfields = \user_picture::fields('u', array('lastaccess'));
+        $unreadcountssql = "SELECT $userfields, count(m.id) as messagecount
+                              FROM {message_contacts} mc
+                        INNER JOIN {user} u
+                                ON u.id = mc.contactid
+                         LEFT JOIN {messages} m
+                                ON m.useridfrom = mc.contactid
+                         LEFT JOIN {message_conversation_members} mcm
+                                ON mcm.conversationid = m.conversationid AND mcm.userid = ? AND mcm.userid != m.useridfrom
+                         LEFT JOIN {message_user_actions} mua
+                                ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
+                             WHERE mua.id is NULL
+                               AND mc.userid = ?
+                               AND mc.blocked = 0
+                               AND u.deleted = 0
+                          GROUP BY $userfields";
+
+        return $DB->get_records_sql($unreadcountssql, [$userid, $userid, self::MESSAGE_ACTION_READ,
+            $userid, $userid], $limitfrom, $limitnum);
+    }
+
+    /**
+     * Returns the an array of the users the given user is in a conversation
+     * with who are not a contact and the number of unread messages.
+     *
+     * @param int $userid The user id
+     * @param int $limitfrom
+     * @param int $limitnum
+     * @return array
+     */
+    public static function get_non_contacts_with_unread_message_count($userid, $limitfrom = 0, $limitnum = 0) {
+        global $DB;
+
+        $userfields = \user_picture::fields('u', array('lastaccess'));
+        $unreadcountssql = "SELECT $userfields, count(m.id) as messagecount
+                              FROM {user} u
+                        INNER JOIN {messages} m
+                                ON m.useridfrom = u.id
+                        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 = ? AND mua.action = ?)
+                         LEFT JOIN {message_contacts} mc
+                                ON (mc.userid = ? AND mc.contactid = u.id)
+                             WHERE mcm.userid = ?
+                               AND mcm.userid != m.useridfrom
+                               AND mua.id is NULL
+                               AND mc.id is NULL
+                               AND u.deleted = 0
+                          GROUP BY $userfields";
+
+        return $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, $userid, $userid],
+            $limitfrom, $limitnum);
+    }
+
     /**
      * Returns the messages to display in the message area.
      *
@@ -710,54 +635,43 @@ class api {
      * @return bool
      */
     public static function delete_conversation($userid, $otheruserid) {
-        global $DB;
+        global $DB, $USER;
 
-        // We need to update the tables to mark all messages as deleted from and to the other user. This seems worse than it
-        // is, that's because our DB structure splits messages into two tables (great idea, huh?) which causes code like this.
-        // This won't be a particularly heavily used function (at least I hope not), so let's hope MDL-36941 gets worked on
-        // soon for the sake of any developers' sanity when dealing with the messaging system.
-        $now = time();
-        $sql = "UPDATE {message}
-                   SET timeuserfromdeleted = :time
-                 WHERE useridfrom = :userid
-                   AND useridto = :otheruserid
-                   AND notification = 0";
-        $DB->execute($sql, array('time' => $now, 'userid' => $userid, 'otheruserid' => $otheruserid));
-
-        $sql = "UPDATE {message}
-                   SET timeusertodeleted = :time
-                 WHERE useridto = :userid
-                   AND useridfrom = :otheruserid
-                   AND notification = 0";
-        $DB->execute($sql, array('time' => $now, 'userid' => $userid, 'otheruserid' => $otheruserid));
-
-        $sql = "UPDATE {message_read}
-                   SET timeuserfromdeleted = :time
-                 WHERE useridfrom = :userid
-                   AND useridto = :otheruserid
-                   AND notification = 0";
-        $DB->execute($sql, array('time' => $now, 'userid' => $userid, 'otheruserid' => $otheruserid));
-
-        $sql = "UPDATE {message_read}
-                   SET timeusertodeleted = :time
-                 WHERE useridto = :userid
-                   AND useridfrom = :otheruserid
-                   AND notification = 0";
-        $DB->execute($sql, array('time' => $now, 'userid' => $userid, 'otheruserid' => $otheruserid));
-
-        // Now we need to trigger events for these.
-        if ($messages = helper::get_messages($userid, $otheruserid, $now)) {
-            // Loop through and trigger a deleted event.
-            foreach ($messages as $message) {
-                $messagetable = 'message';
-                if (!empty($message->timeread)) {
-                    $messagetable = 'message_read';
-                }
+        $conversationid = self::get_conversation_between_users([$userid, $otheruserid]);
 
-                // Trigger event for deleting the message.
-                \core\event\message_deleted::create_from_ids($message->useridfrom, $message->useridto,
-                    $userid, $messagetable, $message->id)->trigger();
+        // If there is no conversation, there is nothing to do.
+        if (!$conversationid) {
+            return true;
+        }
+
+        // Get all messages belonging to this conversation that have not already been deleted by this user.
+        $sql = "SELECT m.*
+                 FROM {messages} m
+           INNER JOIN {message_conversations} mc
+                   ON m.conversationid = mc.id
+            LEFT JOIN {message_user_actions} mua
+                   ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
+                WHERE mua.id is NULL
+                  AND mc.id = ?
+             ORDER BY m.timecreated ASC";
+        $messages = $DB->get_records_sql($sql, [$userid, self::MESSAGE_ACTION_DELETED, $conversationid]);
+
+        // Ok, mark these as deleted.
+        foreach ($messages as $message) {
+            $mua = new \stdClass();
+            $mua->userid = $userid;
+            $mua->messageid = $message->id;
+            $mua->action = self::MESSAGE_ACTION_DELETED;
+            $mua->timecreated = time();
+            $mua->id = $DB->insert_record('message_user_actions', $mua);
+
+            if ($message->useridfrom == $userid) {
+                $useridto = $otheruserid;
+            } else {
+                $useridto = $userid;
             }
+            \core\event\message_deleted::create_from_ids($message->useridfrom, $useridto,
+                $USER->id, $message->id, $mua->id)->trigger();
         }
 
         return true;
@@ -777,11 +691,87 @@ class api {
             $user = $USER;
         }
 
-        return $DB->count_records_select(
-            'message',
-            'useridto = ? AND timeusertodeleted = 0 AND notification = 0',
-            [$user->id],
-            "COUNT(DISTINCT(useridfrom))");
+        $sql = "SELECT COUNT(DISTINCT(m.conversationid))
+                  FROM {messages} m
+            INNER JOIN {message_conversations} mc
+                    ON m.conversationid = mc.id
+            INNER JOIN {message_conversation_members} mcm
+                    ON mc.id = mcm.conversationid
+             LEFT JOIN {message_user_actions} mua
+                    ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
+                 WHERE mcm.userid = ?
+                   AND mcm.userid != m.useridfrom
+                   AND mua.id is NULL";
+
+        return $DB->count_records_sql($sql, [$user->id, self::MESSAGE_ACTION_READ, $user->id]);
+    }
+
+    /**
+     * Marks all messages being sent to a user in a particular conversation.
+     *
+     * If $conversationdid is null then it marks all messages as read sent to $userid.
+     *
+     * @param int $userid
+     * @param int|null $conversationid The conversation the messages belong to mark as read, if null mark all
+     */
+    public static function mark_all_messages_as_read($userid, $conversationid = null) {
+        global $DB;
+
+        $messagesql = "SELECT m.*
+                         FROM {messages} m
+                   INNER JOIN {message_conversations} mc
+                           ON mc.id = m.conversationid
+                   INNER JOIN {message_conversation_members} mcm
+                           ON mcm.conversationid = mc.id
+                    LEFT JOIN {message_user_actions} mua
+                           ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
+                        WHERE mua.id is NULL
+                          AND mcm.userid = ?
+                          AND m.useridfrom != ?";
+        $messageparams = [];
+        $messageparams[] = $userid;
+        $messageparams[] = self::MESSAGE_ACTION_READ;
+        $messageparams[] = $userid;
+        $messageparams[] = $userid;
+        if (!is_null($conversationid)) {
+            $messagesql .= " AND mc.id = ?";
+            $messageparams[] = $conversationid;
+        }
+
+        $messages = $DB->get_recordset_sql($messagesql, $messageparams);
+        foreach ($messages as $message) {
+            self::mark_message_as_read($userid, $message);
+        }
+        $messages->close();
+    }
+
+    /**
+     * Marks all notifications being sent from one user to another user as read.
+     *
+     * If the from user is null then it marks all notifications as read sent to the to user.
+     *
+     * @param int $touserid the id of the message recipient
+     * @param int|null $fromuserid the id of the message sender, null if all messages
+     * @return void
+     */
+    public static function mark_all_notifications_as_read($touserid, $fromuserid = null) {
+        global $DB;
+
+        $notificationsql = "SELECT n.*
+                              FROM {notifications} n
+                             WHERE useridto = ?
+                               AND timeread is NULL";
+        $notificationsparams = [$touserid];
+        if (!empty($fromuserid)) {
+            $notificationsql .= " AND useridfrom = ?";
+            $notificationsparams[] = $fromuserid;
+        }
+
+        $notifications = $DB->get_recordset_sql($notificationsql, $notificationsparams);
+        foreach ($notifications as $notification) {
+            self::mark_notification_as_read($notification);
+        }
+        $notifications->close();
     }
 
     /**
@@ -789,39 +779,42 @@ class api {
      *
      * Can be filtered by type.
      *
+     * @deprecated since 3.5
      * @param int $touserid the id of the message recipient
      * @param int $fromuserid the id of the message sender
      * @param string $type filter the messages by type, either MESSAGE_TYPE_NOTIFICATION, MESSAGE_TYPE_MESSAGE or '' for all.
      * @return void
      */
     public static function mark_all_read_for_user($touserid, $fromuserid = 0, $type = '') {
-        global $DB;
+        debugging('\core_message\api::mark_all_read_for_user is deprecated. Please either use ' .
+            '\core_message\api::mark_all_notifications_read_for_user or \core_message\api::mark_all_messages_read_for_user',
+            DEBUG_DEVELOPER);
 
-        $params = array();
-
-        if (!empty($touserid)) {
-            $params['useridto'] = $touserid;
-        }
+        $type = strtolower($type);
 
+        $conversationid = null;
+        $ignoremessages = false;
         if (!empty($fromuserid)) {
-            $params['useridfrom'] = $fromuserid;
+            $conversationid = self::get_conversation_between_users([$touserid, $fromuserid]);
+            if (!$conversationid) { // If there is no conversation between the users then there are no messages to mark.
+                $ignoremessages = true;
+            }
         }
 
         if (!empty($type)) {
-            if (strtolower($type) == MESSAGE_TYPE_NOTIFICATION) {
-                $params['notification'] = 1;
-            } else if (strtolower($type) == MESSAGE_TYPE_MESSAGE) {
-                $params['notification'] = 0;
+            if ($type == MESSAGE_TYPE_NOTIFICATION) {
+                self::mark_all_notifications_as_read($touserid, $fromuserid);
+            } else if ($type == MESSAGE_TYPE_MESSAGE) {
+                if (!$ignoremessages) {
+                    self::mark_all_messages_as_read($touserid, $conversationid);
+                }
+            }
+        } else { // We want both.
+            self::mark_all_notifications_as_read($touserid, $fromuserid);
+            if (!$ignoremessages) {
+                self::mark_all_messages_as_read($touserid, $conversationid);
             }
         }
-
-        $messages = $DB->get_recordset('message', $params);
-
-        foreach ($messages as $message) {
-            message_mark_message_read($message, time());
-        }
-
-        $messages->close();
     }
 
     /**
@@ -1076,4 +1069,224 @@ class api {
         }
         return $processor;
     }
+
+    /**
+     * Retrieve users blocked by $user1
+     *
+     * @param int $userid The user id of the user whos blocked users we are returning
+     * @return array the users blocked
+     */
+    public static function get_blocked_users($userid) {
+        global $DB;
+
+        $userfields = \user_picture::fields('u', array('lastaccess'));
+        $blockeduserssql = "SELECT $userfields
+                              FROM {message_contacts} mc
+                        INNER JOIN {user} u
+                                ON u.id = mc.contactid
+                             WHERE u.deleted = 0
+                               AND mc.userid = ?
+                               AND mc.blocked = 1
+                          GROUP BY $userfields
+                          ORDER BY u.firstname ASC";
+        return $DB->get_records_sql($blockeduserssql, [$userid]);
+    }
+
+    /**
+     * Mark a single message as read.
+     *
+     * @param int $userid The user id who marked the message as read
+     * @param \stdClass $message The message
+     * @param int|null $timeread The time the message was marked as read, if null will default to time()
+     */
+    public static function mark_message_as_read($userid, $message, $timeread = null) {
+        global $DB;
+
+        if (is_null($timeread)) {
+            $timeread = time();
+        }
+
+        $mua = new \stdClass();
+        $mua->userid = $userid;
+        $mua->messageid = $message->id;
+        $mua->action = self::MESSAGE_ACTION_READ;
+        $mua->timecreated = $timeread;
+        $mua->id = $DB->insert_record('message_user_actions', $mua);
+
+        // Get the context for the user who received the message.
+        $context = \context_user::instance($userid, 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();
+        }
+
+        // Trigger event for reading a message.
+        $event = \core\event\message_viewed::create(array(
+            'objectid' => $mua->id,
+            'userid' => $userid, // Using the user who read the message as they are the ones performing the action.
+            'context' => $context,
+            'relateduserid' => $message->useridfrom,
+            'other' => array(
+                'messageid' => $message->id
+            )
+        ));
+        $event->trigger();
+    }
+
+    /**
+     * Mark a single notification as read.
+     *
+     * @param \stdClass $notification The notification
+     * @param int|null $timeread The time the message was marked as read, if null will default to time()
+     */
+    public static function mark_notification_as_read($notification, $timeread = null) {
+        global $DB;
+
+        if (is_null($timeread)) {
+            $timeread = time();
+        }
+
+        if (is_null($notification->timeread)) {
+            $updatenotification = new \stdClass();
+            $updatenotification->id = $notification->id;
+            $updatenotification->timeread = $timeread;
+
+            $DB->update_record('notifications', $updatenotification);
+
+            // Trigger event for reading a notification.
+            \core\event\notification_viewed::create_from_ids(
+                $notification->useridfrom,
+                $notification->useridto,
+                $notification->id
+            )->trigger();
+        }
+    }
+
+    /**
+     * Checks if a user can delete a message.
+     *
+     * @param int $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)
+     * @param int $messageid The message id
+     * @return bool Returns true if a user can delete the message, false otherwise.
+     */
+    public static function can_delete_message($userid, $messageid) {
+        global $DB, $USER;
+
+        $sql = "SELECT m.id, m.useridfrom, mcm.userid as useridto
+                  FROM {messages} m
+            INNER JOIN {message_conversations} mc
+                    ON m.conversationid = mc.id
+            INNER JOIN {message_conversation_members} mcm
+                    ON mcm.conversationid = mc.id
+                 WHERE mcm.userid != m.useridfrom
+                   AND m.id = ?";
+        $message = $DB->get_record_sql($sql, [$messageid], MUST_EXIST);
+
+        if ($message->useridfrom == $userid) {
+            $userdeleting = 'useridfrom';
+        } else if ($message->useridto == $userid) {
+            $userdeleting = 'useridto';
+        } else {
+            return false;
+        }
+
+        $systemcontext = \context_system::instance();
+
+        // Let's check if the user is allowed to delete this message.
+        if (has_capability('moodle/site:deleteanymessage', $systemcontext) ||
+            ((has_capability('moodle/site:deleteownmessage', $systemcontext) &&
+                $USER->id == $message->$userdeleting))) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Deletes a message.
+     *
+     * This function does not verify any permissions.
+     *
+     * @param int $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)
+     * @param int $messageid The message id
+     * @return bool
+     */
+    public static function delete_message($userid, $messageid) {
+        global $DB;
+
+        $sql = "SELECT m.id, m.useridfrom, mcm.userid as useridto
+                  FROM {messages} m
+            INNER JOIN {message_conversations} mc
+                    ON m.conversationid = mc.id
+            INNER JOIN {message_conversation_members} mcm
+                    ON mcm.conversationid = mc.id
+                 WHERE mcm.userid != m.useridfrom
+                   AND m.id = ?";
+        $message = $DB->get_record_sql($sql, [$messageid], MUST_EXIST);
+
+        // Check if the user has already deleted this message.
+        if (!$DB->record_exists('message_user_actions', ['userid' => $userid,
+                'messageid' => $messageid, 'action' => self::MESSAGE_ACTION_DELETED])) {
+            $mua = new \stdClass();
+            $mua->userid = $userid;
+            $mua->messageid = $messageid;
+            $mua->action = self::MESSAGE_ACTION_DELETED;
+            $mua->timecreated = time();
+            $mua->id = $DB->insert_record('message_user_actions', $mua);
+
+            // Trigger event for deleting a message.
+            \core\event\message_deleted::create_from_ids($message->useridfrom, $message->useridto,
+                $userid, $message->id, $mua->id)->trigger();
+
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns the conversation between two users.
+     *
+     * @param array $userids
+     * @return int|bool The id of the conversation, false if not found
+     */
+    public static function get_conversation_between_users(array $userids) {
+        global $DB;
+
+        $hash = helper::get_conversation_hash($userids);
+
+        if ($conversation = $DB->get_record('message_conversations', ['convhash' => $hash])) {
+            return $conversation->id;
+        }
+
+        return false;
+    }
+
+    /**
+     * Creates a conversation between two users.
+     *
+     * @param array $userids
+     * @return int The id of the conversation
+     */
+    public static function create_conversation_between_users(array $userids) {
+        global $DB;
+
+        $conversation = new \stdClass();
+        $conversation->convhash = helper::get_conversation_hash($userids);
+        $conversation->timecreated = time();
+        $conversation->id = $DB->insert_record('message_conversations', $conversation);
+
+        // Add members to this conversation.
+        foreach ($userids as $userid) {
+            $member = new \stdClass();
+            $member->conversationid = $conversation->id;
+            $member->userid = $userid;
+            $member->timecreated = time();
+            $DB->insert_record('message_conversation_members', $member);
+        }
+
+        return $conversation->id;
+    }
 }
index b7f2a04..0c68fd7 100644 (file)
@@ -51,48 +51,61 @@ class helper {
                                         $sort = 'timecreated ASC', $timefrom = 0, $timeto = 0) {
         global $DB;
 
-        $messageid = $DB->sql_concat("'message_'", 'id');
-        $messagereadid = $DB->sql_concat("'messageread_'", 'id');
-
-        $sql = "SELECT {$messageid} AS fakeid, id, useridfrom, useridto, subject, fullmessage, fullmessagehtml, fullmessageformat,
-                       smallmessage, notification, timecreated, 0 as timeread
-                  FROM {message} m
-                 WHERE ((useridto = ? AND useridfrom = ? AND timeusertodeleted = ?)
-                    OR (useridto = ? AND useridfrom = ? AND timeuserfromdeleted = ?))
-                   AND notification = 0
-                   %where%
-             UNION ALL
-                SELECT {$messagereadid} AS fakeid, id, useridfrom, useridto, subject, fullmessage, fullmessagehtml, fullmessageformat,
-                       smallmessage, notification, timecreated, timeread
-                  FROM {message_read} mr
-                 WHERE ((useridto = ? AND useridfrom = ? AND timeusertodeleted = ?)
-                    OR (useridto = ? AND useridfrom = ? AND timeuserfromdeleted = ?))
-                   AND notification = 0
-                   %where%
-              ORDER BY $sort";
-        $params1 = array($userid, $otheruserid, $timedeleted,
-                         $otheruserid, $userid, $timedeleted);
-
-        $params2 = array($userid, $otheruserid, $timedeleted,
-                         $otheruserid, $userid, $timedeleted);
-        $where = array();
+        $hash = self::get_conversation_hash([$userid, $otheruserid]);
+
+        $sql = "SELECT m.id, m.useridfrom, m.subject, m.fullmessage, m.fullmessagehtml,
+                       m.fullmessageformat, m.smallmessage, m.timecreated, muaread.timecreated AS timeread
+                  FROM {message_conversations} mc
+            INNER JOIN {messages} m
+                    ON m.conversationid = mc.id
+             LEFT JOIN {message_user_actions} muaread
+                    ON (muaread.messageid = m.id
+                   AND muaread.userid = :userid1
+                   AND muaread.action = :readaction)";
+        $params = ['userid1' => $userid, 'readaction' => api::MESSAGE_ACTION_READ, 'convhash' => $hash];
+
+        if (empty($timedeleted)) {
+            $sql .= " LEFT JOIN {message_user_actions} mua
+                             ON (mua.messageid = m.id
+                            AND mua.userid = :userid2
+                            AND mua.action = :deleteaction
+                            AND mua.timecreated is NOT NULL)";
+        } else {
+            $sql .= " INNER JOIN {message_user_actions} mua
+                              ON (mua.messageid = m.id
+                             AND mua.userid = :userid2
+                             AND mua.action = :deleteaction
+                             AND mua.timecreated = :timedeleted)";
+            $params['timedeleted'] = $timedeleted;
+        }
+
+        $params['userid2'] = $userid;
+        $params['deleteaction'] = api::MESSAGE_ACTION_DELETED;
+
+        $sql .= " WHERE mc.convhash = :convhash";
 
         if (!empty($timefrom)) {
-            $where[] = 'AND timecreated >= ?';
-            $params1[] = $timefrom;
-            $params2[] = $timefrom;
+            $sql .= " AND m.timecreated >= :timefrom";
+            $params['timefrom'] = $timefrom;
         }
 
         if (!empty($timeto)) {
-            $where[] = 'AND timecreated <= ?';
-            $params1[] = $timeto;
-            $params2[] = $timeto;
+            $sql .= " AND m.timecreated <= :timeto";
+            $params['timeto'] = $timeto;
         }
 
-        $sql = str_replace('%where%', implode(' ', $where), $sql);
-        $params = array_merge($params1, $params2);
+        if (empty($timedeleted)) {
+            $sql .= " AND mua.id is NULL";
+        }
+
+        $sql .= " ORDER BY m.$sort";
+
+        $messages = $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
+        foreach ($messages as &$message) {
+            $message->useridto = ($message->useridfrom == $userid) ? $otheruserid : $userid;
+        }
 
-        return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
+        return $messages;
     }
 
     /**
@@ -293,6 +306,18 @@ class helper {
         return $params;
     }
 
+    /**
+     * Returns the conversation hash between users for easy look-ups in the DB.
+     *
+     * @param array $userids
+     * @return string
+     */
+    public static function get_conversation_hash(array $userids) {
+        sort($userids);
+
+        return sha1(implode('-', $userids));
+    }
+
     /**
      * Returns the cache key for the time created value of the last message between two users.
      *
index 35fce7f..58479f2 100644 (file)
@@ -145,9 +145,15 @@ abstract class base_message extends \core_search\base {
             $userfield) {
         global $DB;
 
+        if ($userfield == 'useridto') {
+            $userfield = 'mcm.userid';
+        } else {
+            $userfield = 'm.useridfrom';
+        }
+
         // Set up basic query.
         $where = $userfield . ' != :noreplyuser AND ' . $userfield .
-                ' != :supportuser AND timecreated >= :modifiedfrom';
+                ' != :supportuser AND m.timecreated >= :modifiedfrom';
         $params = [
             'noreplyuser' => \core_user::NOREPLY_USER,
             'supportuser' => \core_user::SUPPORT_USER,
@@ -179,6 +185,15 @@ abstract class base_message extends \core_search\base {
                 throw new \coding_exception('Unexpected contextlevel: ' . $context->contextlevel);
         }
 
-        return $DB->get_recordset_select('message_read', $where, $params, 'timeread ASC');
+        $sql = "SELECT m.*, mcm.userid as useridto
+                  FROM {messages} m
+            INNER JOIN {message_conversations} mc
+                    ON m.conversationid = mc.id
+            INNER JOIN {message_conversation_members} mcm
+                    ON mcm.conversationid = mc.id
+                 WHERE mcm.userid != m.useridfrom
+                   AND $where
+              ORDER BY m.timecreated ASC";
+        return $DB->get_recordset_sql($sql, $params);
     }
 }
index 26a2561..052d6bd 100644 (file)
@@ -70,7 +70,15 @@ class message_received extends base_message {
             return \core_search\manager::ACCESS_DENIED;
         }
 
-        $message = $DB->get_record('message_read', array('id' => $id));
+        $sql = "SELECT m.*, mcm.userid as useridto
+                  FROM {messages} m
+            INNER JOIN {message_conversations} mc
+                    ON m.conversationid = mc.id
+            INNER JOIN {message_conversation_members} mcm
+                    ON mcm.conversationid = mc.id
+                 WHERE mcm.userid != m.useridfrom
+                   AND m.id = :id";
+        $message = $DB->get_record_sql($sql, array('id' => $id));
         if (!$message) {
             return \core_search\manager::ACCESS_DELETED;
         }
@@ -86,7 +94,9 @@ class message_received extends base_message {
             return \core_search\manager::ACCESS_DENIED;
         }
 
-        if ($message->timeusertodeleted != 0) {
+        $usertodeleted = $DB->record_exists('message_user_actions', ['messageid' => $id, 'userid' => $message->useridto,
+            'action' => \core_message\api::MESSAGE_ACTION_DELETED]);
+        if ($usertodeleted) {
             return \core_search\manager::ACCESS_DELETED;
         }
 
index 9425a82..79dc0bd 100644 (file)
@@ -69,7 +69,15 @@ class message_sent extends base_message {
             return \core_search\manager::ACCESS_DENIED;
         }
 
-        $message = $DB->get_record('message_read', array('id' => $id));
+        $sql = "SELECT m.*, mcm.userid as useridto
+                  FROM {messages} m
+            INNER JOIN {message_conversations} mc
+                    ON m.conversationid = mc.id
+            INNER JOIN {message_conversation_members} mcm
+                    ON mcm.conversationid = mc.id
+                 WHERE mcm.userid != m.useridfrom
+                   AND m.id = :id";
+        $message = $DB->get_record_sql($sql, array('id' => $id));
         if (!$message) {
             return \core_search\manager::ACCESS_DELETED;
         }
@@ -85,7 +93,9 @@ class message_sent extends base_message {
             return \core_search\manager::ACCESS_DENIED;
         }
 
-        if ($message->timeuserfromdeleted != 0) {
+        $userfromdeleted = $DB->record_exists('message_user_actions', ['messageid' => $id, 'userid' => $message->useridfrom,
+            'action' => \core_message\api::MESSAGE_ACTION_DELETED]);
+        if ($userfromdeleted) {
             return \core_search\manager::ACCESS_DELETED;
         }
 
index 173b1c4..26c4ccd 100644 (file)
@@ -1161,7 +1161,7 @@ class core_message_external extends external_api {
      * @since Moodle 2.5
      */
     public static function get_contacts() {
-        global $CFG, $PAGE;
+        global $CFG, $PAGE, $USER;
 
         // Check if messaging is enabled.
         if (empty($CFG->messaging)) {
@@ -1170,25 +1170,68 @@ class core_message_external extends external_api {
 
         require_once($CFG->dirroot . '/user/lib.php');
 
-        list($online, $offline, $strangers) = message_get_contacts();
-        $allcontacts = array('online' => $online, 'offline' => $offline, 'strangers' => $strangers);
-        foreach ($allcontacts as $mode => $contacts) {
-            foreach ($contacts as $key => $contact) {
-                $newcontact = array(
-                    'id' => $contact->id,
-                    'fullname' => fullname($contact),
-                    'unread' => $contact->messagecount
-                );
+        $allcontacts = array('online' => [], 'offline' => [], 'strangers' => []);
+        $contacts = \core_message\api::get_contacts_with_unread_message_count($USER->id);
+        foreach ($contacts as $contact) {
+            // Set the mode.
+            $mode = 'offline';
+            if (\core_message\helper::is_online($contact->lastaccess)) {
+                $mode = 'online';
+            }
 
-                $userpicture = new user_picture($contact);
-                $userpicture->size = 1; // Size f1.
-                $newcontact['profileimageurl'] = $userpicture->get_url($PAGE)->out(false);
-                $userpicture->size = 0; // Size f2.
-                $newcontact['profileimageurlsmall'] = $userpicture->get_url($PAGE)->out(false);
+            $newcontact = array(
+                'id' => $contact->id,
+                'fullname' => fullname($contact),
+                'unread' => $contact->messagecount
+            );
+
+            $userpicture = new user_picture($contact);
+            $userpicture->size = 1; // Size f1.
+            $newcontact['profileimageurl'] = $userpicture->get_url($PAGE)->out(false);
+            $userpicture->size = 0; // Size f2.
+            $newcontact['profileimageurlsmall'] = $userpicture->get_url($PAGE)->out(false);
 
-                $allcontacts[$mode][$key] = $newcontact;
+            $allcontacts[$mode][$contact->id] = $newcontact;
+        }
+
+        $strangers = \core_message\api::get_non_contacts_with_unread_message_count($USER->id);
+        foreach ($strangers as $contact) {
+            $newcontact = array(
+                'id' => $contact->id,
+                'fullname' => fullname($contact),
+                'unread' => $contact->messagecount
+            );
+
+            $userpicture = new user_picture($contact);
+            $userpicture->size = 1; // Size f1.
+            $newcontact['profileimageurl'] = $userpicture->get_url($PAGE)->out(false);
+            $userpicture->size = 0; // Size f2.
+            $newcontact['profileimageurlsmall'] = $userpicture->get_url($PAGE)->out(false);
+
+            $allcontacts['strangers'][$contact->id] = $newcontact;
+        }
+
+        // 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) {
+                $supportuser->fullname = fullname($supportuser);
+                $supportuser->unread = $supportuser->messagecount;
+                $allcontacts['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) {
+                $noreplyuser->fullname = fullname($noreplyuser);
+                $noreplyuser->unread = $noreplyuser->messagecount;
+                $allcontacts['strangers'][$noreplyuser->id] = $noreplyuser;
             }
         }
+
         return $allcontacts;
     }
 
@@ -1486,13 +1529,16 @@ class core_message_external extends external_api {
             foreach ($messages as $mid => $message) {
 
                 // Do not return deleted messages.
-                if (($useridto == $USER->id and $message->timeusertodeleted) or
+                if (!$message->notification) {
+                    if (($useridto == $USER->id and $message->timeusertodeleted) or
                         ($useridfrom == $USER->id and $message->timeuserfromdeleted)) {
-
-                    unset($messages[$mid]);
-                    continue;
+                        unset($messages[$mid]);
+                        continue;
+                    }
                 }
 
+                $message->useridto = $useridto;
+
                 // We need to get the user from the query.
                 if (empty($userfromfullname)) {
                     // Check for non-reply and support users.
@@ -1517,11 +1563,6 @@ class core_message_external extends external_api {
                     $message->usertofullname = $usertofullname;
                 }
 
-                // This field is only available in the message_read table.
-                if (!isset($message->timeread)) {
-                    $message->timeread = 0;
-                }
-
                 $message->text = message_format_message_text($message);
                 $messages[$mid] = (array) $message;
             }
@@ -1635,7 +1676,7 @@ class core_message_external extends external_api {
             throw new moodle_exception('accessdenied', 'admin');
         }
 
-        \core_message\api::mark_all_read_for_user($useridto, $useridfrom, MESSAGE_TYPE_NOTIFICATION);
+        \core_message\api::mark_all_notifications_as_read($useridto, $useridfrom);
 
         return true;
     }
@@ -1774,7 +1815,7 @@ class core_message_external extends external_api {
         }
 
         // Now, we can get safely all the blocked users.
-        $users = message_get_blocked_users($user);
+        $users = \core_message\api::get_blocked_users($user->id);
 
         $blockedusers = array();
         foreach ($users as $user) {
@@ -1830,7 +1871,7 @@ class core_message_external extends external_api {
     public static function mark_message_read_parameters() {
         return new external_function_parameters(
             array(
-                'messageid' => new external_value(PARAM_INT, 'id of the message (in the message table)'),
+                'messageid' => new external_value(PARAM_INT, 'id of the message in the messages table'),
                 'timeread' => new external_value(PARAM_INT, 'timestamp for when the message should be marked read',
                     VALUE_DEFAULT, 0)
             )
@@ -1875,16 +1916,31 @@ class core_message_external extends external_api {
         $context = context_system::instance();
         self::validate_context($context);
 
-        $message = $DB->get_record('message', array('id' => $params['messageid']), '*', MUST_EXIST);
+        $sql = "SELECT m.*, mcm.userid as useridto
+                  FROM {messages} m
+            INNER JOIN {message_conversations} mc
+                    ON m.conversationid = mc.id
+            INNER JOIN {message_conversation_members} mcm
+                    ON mcm.conversationid = mc.id
+             LEFT JOIN {message_user_actions} mua
+                    ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
+                 WHERE mua.id is NULL
+                   AND mcm.userid != m.useridfrom
+                   AND m.id = ?";
+        $messageparams = [];
+        $messageparams[] = $USER->id;
+        $messageparams[] = \core_message\api::MESSAGE_ACTION_READ;
+        $messageparams[] = $params['messageid'];
+        $message = $DB->get_record_sql($sql, $messageparams, MUST_EXIST);
 
         if ($message->useridto != $USER->id) {
             throw new invalid_parameter_exception('Invalid messageid, you don\'t have permissions to mark this message as read');
         }
 
-        $messageid = message_mark_message_read($message, $timeread);
+        \core_message\api::mark_message_as_read($USER->id, $message, $timeread);
 
         $results = array(
-            'messageid' => $messageid,
+            'messageid' => $message->id,
             'warnings' => $warnings
         );
         return $results;
@@ -1899,7 +1955,92 @@ class core_message_external extends external_api {
     public static function mark_message_read_returns() {
         return new external_single_structure(
             array(
-                'messageid' => new external_value(PARAM_INT, 'the id of the message in the message_read table'),
+                'messageid' => new external_value(PARAM_INT, 'the id of the message in the messages table'),
+                'warnings' => new external_warnings()
+            )
+        );
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     */
+    public static function mark_notification_read_parameters() {
+        return new external_function_parameters(
+            array(
+                'notificationid' => new external_value(PARAM_INT, 'id of the notification'),
+                'timeread' => new external_value(PARAM_INT, 'timestamp for when the notification should be marked read',
+                    VALUE_DEFAULT, 0)
+            )
+        );
+    }
+
+    /**
+     * Mark a single notification as read.
+     *
+     * This will trigger a 'notification_viewed' event.
+     *
+     * @param int $notificationid id of the notification
+     * @param int $timeread timestamp for when the notification should be marked read
+     * @return external_description
+     * @throws invalid_parameter_exception
+     * @throws moodle_exception
+     */
+    public static function mark_notification_read($notificationid, $timeread) {
+        global $CFG, $DB, $USER;
+
+        // Check if private messaging between users is allowed.
+        if (empty($CFG->messaging)) {
+            throw new moodle_exception('disabled', 'message');
+        }
+
+        // Warnings array, it can be empty at the end but is mandatory.
+        $warnings = array();
+
+        // Validate params.
+        $params = array(
+            'notificationid' => $notificationid,
+            'timeread' => $timeread
+        );
+        $params = self::validate_parameters(self::mark_notification_read_parameters(), $params);
+
+        if (empty($params['timeread'])) {
+            $timeread = time();
+        } else {
+            $timeread = $params['timeread'];
+        }
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $notification = $DB->get_record('notifications', ['id' => $params['notificationid']], '*', MUST_EXIST);
+
+        if ($notification->useridto != $USER->id) {
+            throw new invalid_parameter_exception('Invalid notificationid, you don\'t have permissions to mark this ' .
+                'notification as read');
+        }
+
+        \core_message\api::mark_notification_as_read($notification, $timeread);
+
+        $results = array(
+            'notificationid' => $notification->id,
+            'warnings' => $warnings
+        );
+
+        return $results;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     */
+    public static function mark_notification_read_returns() {
+        return new external_single_structure(
+            array(
+                'notificationid' => new external_value(PARAM_INT, 'id of the notification'),
                 'warnings' => new external_warnings()
             )
         );
@@ -1974,7 +2115,13 @@ class core_message_external extends external_api {
             throw new moodle_exception('accessdenied', 'admin');
         }
 
-        \core_message\api::mark_all_read_for_user($useridto, $useridfrom, MESSAGE_TYPE_MESSAGE);
+        if ($useridfrom) {
+            if ($conversationid = \core_message\api::get_conversation_between_users([$useridto, $useridfrom])) {
+                \core_message\api::mark_all_messages_as_read($useridto, $conversationid);
+            }
+        } else {
+            \core_message\api::mark_all_messages_as_read($useridto);
+        }
 
         return true;
     }
@@ -2094,7 +2241,7 @@ class core_message_external extends external_api {
      * @since 3.1
      */
     public static function delete_message($messageid, $userid, $read = true) {
-        global $CFG, $DB;
+        global $CFG;
 
         // Check if private messaging between users is allowed.
         if (empty($CFG->messaging)) {
@@ -2116,15 +2263,11 @@ class core_message_external extends external_api {
         $context = context_system::instance();
         self::validate_context($context);
 
-        $messagestable = $params['read'] ? 'message_read' : 'message';
-        $message = $DB->get_record($messagestable, array('id' => $params['messageid']), '*', MUST_EXIST);
-
         $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
         core_user::require_active_user($user);
 
-        $status = false;
-        if (message_can_delete_message($message, $user->id)) {
-            $status = message_delete_message($message, $user->id);;
+        if (\core_message\api::can_delete_message($user->id, $messageid)) {
+            $status = \core_message\api::delete_message($user->id, $messageid);
         } else {
             throw new moodle_exception('You do not have permission to delete this message');
         }
index 65349b3..b90a8ea 100644 (file)
@@ -127,7 +127,10 @@ if (!$user2realuser) {
 if (!empty($user2->id)) {
     if ($currentuser && isset($conversations[$user2->id])) {
         // Mark the conversation we are loading as read.
-        \core_message\api::mark_all_read_for_user($user1->id, $user2->id);
+        if ($conversationid = \core_message\api::get_conversation_between_users([$user1->id, $user2->id])) {
+            \core_message\api::mark_all_messages_as_read($user1->id, $conversationid);
+        }
+
         // Ensure the UI knows it's read as well.
         $conversations[$user2->id]->isread = 1;
     }
index 90f7a29..9143d13 100644 (file)
@@ -64,147 +64,6 @@ define('MESSAGE_DEFAULT_MIN_POLL_IN_SECONDS', 10);
 define('MESSAGE_DEFAULT_MAX_POLL_IN_SECONDS', 2 * MINSECS);
 define('MESSAGE_DEFAULT_TIMEOUT_POLL_IN_SECONDS', 5 * MINSECS);
 
-/**
- * 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
- */
-function message_get_blocked_users($user1=null, $user2=null) {
-    global $DB, $USER;
-
-    if (empty($user1)) {
-        $user1 = $USER;
-    }
-
-    if (!empty($user2)) {
-        $user2->isblocked = false;
-    }
-
-    $blockedusers = array();
-
-    $userfields = user_picture::fields('u', array('lastaccess'));
-    $blockeduserssql = "SELECT $userfields, COUNT(m.id) AS messagecount
-                          FROM {message_contacts} mc
-                          JOIN {user} u ON u.id = mc.contactid
-                          LEFT OUTER JOIN {message} m ON m.useridfrom = mc.contactid AND m.useridto = :user1id1
-                         WHERE u.deleted = 0 AND mc.userid = :user1id2 AND mc.blocked = 1
-                      GROUP BY $userfields
-                      ORDER BY u.firstname ASC";
-    $rs =  $DB->get_recordset_sql($blockeduserssql, array('user1id1' => $user1->id, 'user1id2' => $user1->id));
-
-    foreach($rs as $rd) {
-        $blockedusers[] = $rd;
-
-        if (!empty($user2) && $user2->id == $rd->id) {
-            $user2->isblocked = true;
-        }
-    }
-    $rs->close();
-
-    return $blockedusers;
-}
-
-/**
- * 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)
- */
-function message_get_contacts($user1=null, $user2=null) {
-    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;
-    }
-
-    // time 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();
-
-    $userfields = user_picture::fields('u', array('lastaccess'));
-
-    // get all in our contactlist who are not blocked in our contact list
-    // and count messages we have waiting from each of them
-    $contactsql = "SELECT $userfields, COUNT(m.id) AS messagecount
-                     FROM {message_contacts} mc
-                     JOIN {user} u ON u.id = mc.contactid
-                     LEFT OUTER JOIN {message} m ON m.useridfrom = mc.contactid AND m.useridto = ?
-                    WHERE u.deleted = 0 AND mc.userid = ? AND mc.blocked = 0
-                 GROUP BY $userfields
-                 ORDER BY u.firstname ASC";
-
-    $rs = $DB->get_recordset_sql($contactsql, array($user1->id, $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;
-        }
-    }
-    $rs->close();
-
-    // get messages from anyone who isn't in our contact list and count the number
-    // of messages we have from each of them
-    $strangersql = "SELECT $userfields, count(m.id) as messagecount
-                      FROM {message} m
-                      JOIN {user} u  ON u.id = m.useridfrom
-                      LEFT OUTER JOIN {message_contacts} mc ON mc.contactid = m.useridfrom AND mc.userid = m.useridto
-                     WHERE u.deleted = 0 AND mc.id IS NULL AND m.useridto = ?
-                  GROUP BY $userfields
-                  ORDER BY u.firstname ASC";
-
-    $rs = $DB->get_recordset_sql($strangersql, array($USER->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;
-    }
-    $rs->close();
-
-    // 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->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->messagecount = message_count_unread_messages($USER, $noreplyuser);
-        if ($noreplyuser->messagecount > 0) {
-            $strangers[$noreplyuser->id] = $noreplyuser;
-        }
-    }
-    return array($onlinecontacts, $offlinecontacts, $strangers);
-}
-
 /**
  * Returns the count of unread messages for user. Either from a specific user or from all users.
  *
@@ -219,15 +78,24 @@ function message_count_unread_messages($user1=null, $user2=null) {
         $user1 = $USER;
     }
 
+    $sql = "SELECT COUNT(m.id)
+              FROM {messages} m
+        INNER JOIN {message_conversations} mc
+                ON mc.id = m.conversationid
+        INNER JOIN {message_conversation_members} mcm
+                ON mcm.conversationid = mc.id
+         LEFT JOIN {message_user_actions} mua
+                ON (mua.messageid = m.id AND mua.userid = ? AND (mua.action = ? OR mua.action = ?))
+             WHERE mua.id is NULL
+               AND mcm.userid = ?";
+    $params = [$user1->id, \core_message\api::MESSAGE_ACTION_DELETED, \core_message\api::MESSAGE_ACTION_READ,  $user1->id];
+
     if (!empty($user2)) {
-        return $DB->count_records_select('message', "useridto = ? AND useridfrom = ? AND notification = 0
-            AND timeusertodeleted = 0",
-            array($user1->id, $user2->id), "COUNT('id')");
-    } else {
-        return $DB->count_records_select('message', "useridto = ? AND notification = 0
-            AND timeusertodeleted = 0",
-            array($user1->id), "COUNT('id')");
+        $sql .= " AND m.useridfrom = ?";
+        $params[] = $user2->id;
     }
+
+    return $DB->count_records_sql($sql, $params);
 }
 
 /**
@@ -249,7 +117,7 @@ function message_format_message_text($message, $forcetexttohtml = false) {
     $format = $message->fullmessageformat;
 
     if (strval($message->smallmessage) !== '') {
-        if ($message->notification == 1) {
+        if (!empty($message->notification)) {
             if (strval($message->fullmessagehtml) !== '' or strval($message->fullmessage) !== '') {
                 $format = FORMAT_PLAIN;
             }
@@ -421,86 +289,6 @@ function message_block_contact($contactid, $userid = 0) {
     return message_add_contact($contactid, 1, $userid);
 }
 
-/**
- * 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.
- */
-function message_can_delete_message($message, $userid) {
-    global $USER;
-
-    if ($message->useridfrom == $userid) {
-        $userdeleting = 'useridfrom';
-    } else if ($message->useridto == $userid) {
-        $userdeleting = 'useridto';
-    } else {
-        return false;
-    }
-
-    $systemcontext = context_system::instance();
-
-    // Let's check if the user is allowed to delete this message.
-    if (has_capability('moodle/site:deleteanymessage', $systemcontext) ||
-        ((has_capability('moodle/site:deleteownmessage', $systemcontext) &&
-            $USER->id == $message->$userdeleting))) {
-        return true;
-    }
-
-    return false;
-}
-
-/**
- * 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
- */
-function message_delete_message($message, $userid) {
-    global $DB;
-
-    // The column we want to alter.
-    if ($message->useridfrom == $userid) {
-        $coltimedeleted = 'timeuserfromdeleted';
-    } else if ($message->useridto == $userid) {
-        $coltimedeleted = 'timeusertodeleted';
-    } else {
-        return false;
-    }
-
-    // Don't update it if it's already been deleted.
-    if ($message->$coltimedeleted > 0) {
-        return false;
-    }
-
-    // Get the table we want to update.
-    if (isset($message->timeread)) {
-        $messagetable = 'message_read';
-    } else {
-        $messagetable = 'message';
-    }
-
-    // Mark the message as deleted.
-    $updatemessage = new stdClass();
-    $updatemessage->id = $message->id;
-    $updatemessage->$coltimedeleted = time();
-    $success = $DB->update_record($messagetable, $updatemessage);
-
-    if ($success) {
-        // Trigger event for deleting a message.
-        \core\event\message_deleted::create_from_ids($message->useridfrom, $message->useridto,
-            $userid, $messagetable, $message->id)->trigger();
-    }
-
-    return $success;
-}
-
 /**
  * Load a user's contact record
  *
@@ -722,71 +510,6 @@ function message_post_message($userfrom, $userto, $message, $format) {
     return message_send($eventdata);
 }
 
-/**
- * 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
- */
-function message_move_userfrom_unread2read($userid) {
-    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;
-}
-
-/**
- * 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 message_read table
- */
-function message_mark_message_read($message, $timeread, $messageworkingempty=false) {
-    global $DB;
-
-    $message->timeread = $timeread;
-
-    $messageid = $message->id;
-    unset($message->id);//unset because it will get a new id on insert into message_read
-
-    //If any processors have pending actions abort them
-    if (!$messageworkingempty) {
-        $DB->delete_records('message_working', array('unreadmessageid' => $messageid));
-    }
-    $messagereadid = $DB->insert_record('message_read', $message);
-
-    $DB->delete_records('message', array('id' => $messageid));
-
-    // Get the context for the user who received the message.
-    $context = context_user::instance($message->useridto, 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();
-    }
-
-    // Trigger event for reading a message.
-    $event = \core\event\message_viewed::create(array(
-        'objectid' => $messagereadid,
-        'userid' => $message->useridto, // Using the user who read the message as they are the ones performing the action.
-        'context' => $context,
-        'relateduserid' => $message->useridfrom,
-        'other' => array(
-            'messageid' => $messageid
-        )
-    ));
-    $event->trigger();
-
-    return $messagereadid;
-}
-
 /**
  * Get all message processors, validate corresponding plugin existance and
  * system configuration
@@ -962,42 +685,108 @@ function message_get_messages($useridto, $useridfrom = 0, $notifications = -1, $
                                 $sort = 'mr.timecreated DESC', $limitfrom = 0, $limitnum = 0) {
     global $DB;
 
-    $messagetable = $read ? '{message_read}' : '{message}';
-    $params = array('deleted' => 0);
-
-    // Empty useridto means that we are going to retrieve messages send by the useridfrom to any user.
+    // If the 'useridto' value is empty then we are going to retrieve messages sent by the useridfrom to any user.
     if (empty($useridto)) {
         $userfields = get_all_user_name_fields(true, 'u', '', 'userto');
-        $joinsql = "JOIN {user} u ON u.id = mr.useridto";
-        $usersql = "mr.useridfrom = :useridfrom AND u.deleted = :deleted";
-        $params['useridfrom'] = $useridfrom;
     } else {
         $userfields = get_all_user_name_fields(true, 'u', '', 'userfrom');
+    }
+
+    // Create the SQL we will be using.
+    $messagesql = "SELECT mr.*, $userfields, 0 as notification, '' as contexturl, '' as contexturlname,
+                          mua.timecreated as timeusertodeleted, mua2.timecreated as timeread,
+                          mua3.timecreated as timeuserfromdeleted
+                     FROM {messages} mr
+               INNER JOIN {message_conversations} mc
+                       ON mc.id = mr.conversationid
+               INNER JOIN {message_conversation_members} mcm
+                       ON mcm.conversationid = mc.id ";
+
+    $notificationsql = "SELECT mr.*, $userfields, 1 as notification
+                          FROM {notifications} mr ";
+
+    $messagejoinsql = "LEFT JOIN {message_user_actions} mua
+                              ON (mua.messageid = mr.id AND mua.userid = mcm.userid AND mua.action = ?)
+                       LEFT JOIN {message_user_actions} mua2
+                              ON (mua2.messageid = mr.id AND mua2.userid = mcm.userid AND mua2.action = ?)
+                       LEFT JOIN {message_user_actions} mua3
+                              ON (mua3.messageid = mr.id AND mua3.userid = mr.useridfrom AND mua3.action = ?)";
+    $messagejoinparams = [\core_message\api::MESSAGE_ACTION_DELETED, \core_message\api::MESSAGE_ACTION_READ,
+        \core_message\api::MESSAGE_ACTION_DELETED];
+    $notificationsparams = [];
+
+    // If the 'useridto' value is empty then we are going to retrieve messages sent by the useridfrom to any user.
+    if (empty($useridto)) {
+        // Create the messaging query and params.
+        $messagesql .= "INNER JOIN {user} u
+                                ON u.id = mcm.userid
+                                $messagejoinsql
+                             WHERE mr.useridfrom = ?
+                               AND mr.useridfrom != mcm.userid
+                               AND u.deleted = 0 ";
+        $messageparams = array_merge($messagejoinparams, [$useridfrom]);
+
+        // Create the notifications query and params.
+        $notificationsql .= "INNER JOIN {user} u
+                                     ON u.id = mr.useridto
+                                  WHERE mr.useridfrom = ?
+                                    AND u.deleted = 0 ";
+        $notificationsparams[] = $useridfrom;
+    } else {
+        // Create the messaging query and params.
         // Left join because useridfrom may be -10 or -20 (no-reply and support users).
-        $joinsql = "LEFT JOIN {user} u ON u.id = mr.useridfrom";
-        $usersql = "mr.useridto = :useridto AND (u.deleted IS NULL OR u.deleted = :deleted)";
-        $params['useridto'] = $useridto;
+        $messagesql .= "LEFT JOIN {user} u
+                               ON u.id = mr.useridfrom
+                               $messagejoinsql
+                            WHERE mcm.userid = ?
+                              AND mr.useridfrom != mcm.userid
+                              AND u.deleted = 0 ";
+        $messageparams = array_merge($messagejoinparams, [$useridto]);
         if (!empty($useridfrom)) {
-            $usersql .= " AND mr.useridfrom = :useridfrom";
-            $params['useridfrom'] = $useridfrom;
+            $messagesql .= " AND mr.useridfrom = ? ";
+            $messageparams[] = $useridfrom;
+        }
+
+        // Create the notifications query and params.
+        // Left join because useridfrom may be -10 or -20 (no-reply and support users).
+        $notificationsql .= "LEFT JOIN {user} u
+                                    ON (u.id = mr.useridfrom AND u.deleted = 0)
+                                 WHERE mr.useridto = ? ";
+        $notificationsparams[] = $useridto;
+        if (!empty($useridfrom)) {
+            $notificationsql .= " AND mr.useridfrom = ? ";
+            $notificationsparams[] = $useridfrom;
         }
     }
+    if ($read) {
+        $notificationsql .= "AND mr.timeread IS NOT NULL ";
+    } else {
+        $notificationsql .= "AND mr.timeread IS NULL ";
+    }
+    $messagesql .= "ORDER BY $sort";
+    $notificationsql .= "ORDER BY $sort";
+
+    // Handle messages if needed.
+    if ($notifications === -1 || $notifications === 0) {
+        $messages = $DB->get_records_sql($messagesql, $messageparams, $limitfrom, $limitnum);
+        // Get rid of the messages that have either been read or not read depending on the value of $read.
+        $messages = array_filter($messages, function ($message) use ($read) {
+            if ($read) {
+                return !is_null($message->timeread);
+            }
 
-    // Now, if retrieve notifications, conversations or both.
-    $typesql = "";
-    if ($notifications !== -1) {
-        $typesql = "AND mr.notification = :notification";
-        $params['notification'] = ($notifications) ? 1 : 0;
+            return is_null($message->timeread);
+        });
     }
 
-    $sql = "SELECT mr.*, $userfields
-              FROM $messagetable mr
-                   $joinsql
-             WHERE $usersql
-                   $typesql
-             ORDER BY $sort";
+    // All.
+    if ($notifications === -1) {
+        return array_merge($messages, $DB->get_records_sql($notificationsql, $notificationsparams, $limitfrom, $limitnum));
+    } else if ($notifications === 1) { // Just notifications.
+        return $DB->get_records_sql($notificationsql, $notificationsparams, $limitfrom, $limitnum);
+    }
 
-    $messages = $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
+    // Just messages.
     return $messages;
 }
 
index 6d076df..54afde0 100644 (file)
Binary files a/message/output/popup/amd/build/notification_repository.min.js and b/message/output/popup/amd/build/notification_repository.min.js differ
index 2fdb83e..801f9b9 100644 (file)
@@ -97,7 +97,7 @@ define(['core/ajax', 'core/notification'], function(Ajax, Notification) {
      */
     var markAsRead = function(id, timeread) {
         var args = {
-            messageid: id,
+            notificationid: id,
         };
 
         if (timeread) {
@@ -105,7 +105,7 @@ define(['core/ajax', 'core/notification'], function(Ajax, Notification) {
         }
 
         var request = {
-            methodname: 'core_message_mark_message_read',
+            methodname: 'core_message_mark_notification_read',
             args: args
         };
 
index 84826f6..775d25e 100644 (file)
@@ -69,34 +69,20 @@ class api {
             $disabled = $user->emailstop;
         }
         if ($disabled) {
-            // Notifications are disabled, no need to run giant queries.
+            // Notifications are disabled.
             return array();
         }
 
-        $sql = "SELECT r.id, r.useridfrom, r.useridto,
-                       r.subject, r.fullmessage, r.fullmessageformat,
-                       r.fullmessagehtml, r.smallmessage, r.notification, r.contexturl,
-                       r.contexturlname, r.timecreated, r.timeuserfromdeleted, r.timeusertodeleted,
-                       r.component, r.eventtype, r.timeread
-                  FROM {message_read} r
-                 WHERE r.notification = 1
-                       AND r.id IN (SELECT messageid FROM {message_popup} WHERE isread = 1)
-                       AND r.useridto = :useridto1
-             UNION ALL
-                SELECT u.id, u.useridfrom, u.useridto,
-                       u.subject, u.fullmessage, u.fullmessageformat,
-                       u.fullmessagehtml, u.smallmessage, u.notification, u.contexturl,
-                       u.contexturlname, u.timecreated, u.timeuserfromdeleted, u.timeusertodeleted,
-                       u.component, u.eventtype, 0 as timeread
-                  FROM {message} u
-                 WHERE u.notification = 1
-                       AND u.id IN (SELECT messageid FROM {message_popup} WHERE isread = 0)
-                       AND u.useridto = :useridto2
+        $sql = "SELECT n.id, n.useridfrom, n.useridto,
+                       n.subject, n.fullmessage, n.fullmessageformat,
+                       n.fullmessagehtml, n.smallmessage, n.contexturl,
+                       n.contexturlname, n.timecreated, n.component,
+                       n.eventtype, n.timeread
+                  FROM {notifications} n
+                 WHERE n.useridto = :useridto1
               ORDER BY timecreated $sort, timeread $sort, id $sort";
 
         $notifications = [];
-        // Use recordset here to ensure records with the same id aren't ignored because
-        // we can have id clashes between the message and message_read tables.
         $records = $DB->get_recordset_sql($sql, $params, $offset, $limit);
         foreach ($records as $record) {
             $notifications[] = (object) $record;
@@ -122,9 +108,9 @@ class api {
 
         return $DB->count_records_sql(
             "SELECT count(id)
-            FROM {message}
-            WHERE id IN (SELECT messageid FROM {message_popup} WHERE isread = 0)
-            AND useridto = ?",
+               FROM {notifications}
+              WHERE useridto = ?
+                AND timeread is NULL",
             [$useridto]
         );
     }
index 0644a22..0511873 100644 (file)
@@ -57,16 +57,7 @@ class popup_notification implements templatable, renderable {
     }
 
     public function export_for_template(\renderer_base $output) {
-        global $USER;
-
         $context = clone $this->notification;
-
-        if ($context->useridto == $USER->id && $context->timeusertodeleted) {
-            $context->deleted = true;
-        } else {
-            $context->deleted = false;
-        }
-
         $context->timecreatedpretty = get_string('ago', 'message', format_time(time() - $context->timecreated));
         $context->text = message_format_message_text($context);
         $context->read = $context->timeread ? true : false;
index 6a92e56..bb7a992 100644 (file)
@@ -19,4 +19,4 @@
       </INDEXES>
     </TABLE>
   </TABLES>
-</XMLDB>
\ No newline at end of file
+</XMLDB>
index 87e93b0..ffbc092 100644 (file)
@@ -117,6 +117,9 @@ class message_popup_external extends external_api {
                 $notificationoutput = new \message_popup\output\popup_notification($notification);
 
                 $notificationcontext = $notificationoutput->export_for_template($renderer);
+
+                // Keep this for BC.
+                $notificationcontext->deleted = false;
                 $notificationcontexts[] = $notificationcontext;
             }
         }
index 67409cc..dde8b2c 100644 (file)
@@ -41,35 +41,6 @@ class message_output_popup extends message_output {
      * @return true if ok, false if error
      */
     public function send_message($eventdata) {
-        global $DB;
-
-        //hold onto the popup processor id because /admin/cron.php sends a lot of messages at once
-        static $processorid = null;
-
-        //prevent users from getting popup notifications of messages to themselves (happens with forum notifications)
-        if ($eventdata->userfrom->id != $eventdata->userto->id) {
-            if (empty($processorid)) {
-                $processor = $DB->get_record('message_processors', array('name'=>'popup'));
-                $processorid = $processor->id;
-            }
-            $procmessage = new stdClass();
-            $procmessage->unreadmessageid = $eventdata->savedmessageid;
-            $procmessage->processorid     = $processorid;
-
-            //save this message for later delivery
-            $DB->insert_record('message_working', $procmessage);
-
-            if ($eventdata->notification) {
-                if (!$DB->record_exists('message_popup', ['messageid' => $eventdata->savedmessageid, 'isread' => 0])) {
-                    $record = new StdClass();
-                    $record->messageid = $eventdata->savedmessageid;
-                    $record->isread = 0;
-
-                    $DB->insert_record('message_popup', $record);
-                }
-            }
-        }
-
         return true;
     }
 
@@ -113,22 +84,6 @@ class message_output_popup extends message_output {
         return false;
     }
 
-    /**
-     * Handles the message_viewed event to keep data in sync.
-     *
-     * @param \core\event\base $event The event data
-     */
-    public static function message_viewed(\core\event\base $event) {
-        global $DB;
-
-        if ($record = $DB->get_record('message_popup', ['messageid' => $event->other['messageid']])) {
-            // The id can change when the moving to the message_read table.
-            $record->messageid = $event->objectid;
-            $record->isread = 1;
-            $DB->update_record('message_popup', $record);
-        }
-    }
-
     /**
      * Determines if this processor should process a message regardless of user preferences or site settings.
      *
index 2d082ae..338a6c0 100644 (file)
@@ -50,15 +50,7 @@ trait message_popup_test_helper {
         $record->smallmessage = $message;
         $record->timecreated = $timecreated ? $timecreated : time();
 
-        $id = $DB->insert_record('message', $record);
-
-        $popup = new stdClass();
-        $popup->messageid = $id;
-        $popup->isread = 0;
-
-        $DB->insert_record('message_popup', $popup);
-
-        return $id;
+        return $DB->insert_record('notifications', $record);
     }
 
     /**
@@ -89,14 +81,11 @@ trait message_popup_test_helper {
         $record->timecreated = $timecreated ? $timecreated : time();
         $record->timeread = $timeread ? $timeread : time();
 
-        $id = $DB->insert_record('message_read', $record);
-
-        $popup = new stdClass();
-        $popup->messageid = $id;
-        $popup->isread = 1;
+        $record->id = $DB->insert_record('notifications', $record);
 
-        $DB->insert_record('message_popup', $popup);
+        // Mark it as read.
+        \core_message\api::mark_notification_as_read($record);
 
-        return $id;
+        return $record->id;
     }
 }
index fdee795..67289a2 100644 (file)
@@ -13,7 +13,7 @@ Feature: Message popover unread messages
     And I send "Test message" message to "Student 1" user
     And I log out
 
-  Scenario: Notification popover shows correct unread count
+  Scenario: Message popover shows correct unread count
     When I log in as "student2"
     And I send "Test message 2" message to "Student 1" user
     And I log out
@@ -38,7 +38,7 @@ Feature: Message popover unread messages
     # Confirm the message was loaded in the messaging page.
     And I should see "Test message" in the "[data-region='message-text']" "css_element"
 
-  Scenario: Mark all notifications as read
+  Scenario: Mark all messages as read
     When I log in as "student1"
     # Open the popover.
     And I open the message popover
index 94b0a2a..048daf7 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2017111300;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2018022000;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2017110800;        // Requires this Moodle version
 $plugin->component = 'message_popup';  // Full name of the plugin (used for diagnostics)
index 7139800..60a5bb7 100644 (file)
@@ -51,6 +51,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->send_fake_message($sender, $recipient);
 
         \core_message\api::mark_all_read_for_user($recipient->id);
+        $this->assertDebuggingCalled();
         $this->assertEquals(message_count_unread_messages($recipient), 0);
     }
 
@@ -73,6 +74,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->send_fake_message($sender2, $recipient);
 
         \core_message\api::mark_all_read_for_user($recipient->id, $sender1->id);
+        $this->assertDebuggingCalled();
         $this->assertEquals(message_count_unread_messages