Merge branch 'MDL-63401-master' of git://github.com/andrewnicols/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 9 Oct 2018 09:35:29 +0000 (11:35 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 9 Oct 2018 09:35:29 +0000 (11:35 +0200)
129 files changed:
admin/settings/appearance.php
admin/tool/dataprivacy/classes/metadata_registry.php
admin/tool/dataprivacy/classes/output/renderer.php
admin/tool/dataprivacy/classes/output/summary_page.php [new file with mode: 0644]
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/dataprivacy/lib.php
admin/tool/dataprivacy/summary.php [new file with mode: 0644]
admin/tool/dataprivacy/templates/summary.mustache [new file with mode: 0644]
auth/classes/output/login.php
blocks/timeline/tests/behat/block_timeline_dates.feature
blog/classes/external.php [new file with mode: 0644]
blog/classes/external/post_exporter.php [new file with mode: 0644]
blog/index.php
blog/lib.php
blog/tests/external_test.php [new file with mode: 0644]
calendar/lib.php
course/classes/list_element.php
course/classes/output/activity_navigation.php
course/externallib.php
course/renderer.php
course/tests/behat/behat_course.php
course/tests/behat/course_contact.feature [new file with mode: 0644]
course/tests/category_test.php
course/upgrade.txt
grade/edit/tree/lib.php
install/lang/el/error.php
install/lang/el/install.php
lang/en/admin.php
lang/en/deprecated.txt
lang/en/message.php
lang/en/moodle.php
lang/en/privacy.php
lib/amd/build/tree.min.js
lib/amd/src/tree.js
lib/badgeslib.php
lib/behat/behat_base.php
lib/classes/event/message_contact_blocked.php
lib/classes/event/message_contact_unblocked.php
lib/classes/event/message_user_blocked.php [new file with mode: 0644]
lib/classes/event/message_user_unblocked.php [new file with mode: 0644]
lib/db/install.xml
lib/db/messages.php
lib/db/services.php
lib/db/upgrade.php
lib/deprecatedlib.php
lib/editor/tinymce/module.js
lib/jquery/readme_moodle.txt
lib/moodlelib.php
lib/outputrenderers.php
lib/requirejs/readme_moodle.txt
lib/tests/moodlelib_test.php
lib/upgrade.txt
message/amd/build/message_area_profile.min.js
message/amd/src/message_area_profile.js
message/classes/api.php
message/classes/output/messagearea/messages.php
message/classes/privacy/provider.php
message/externallib.php
message/lib.php
message/pendingcontactrequests.php [new file with mode: 0644]
message/tests/api_test.php
message/tests/behat/manage_contacts.feature
message/tests/events_test.php
message/tests/externallib_test.php
message/tests/messagelib_test.php
message/tests/privacy_provider_test.php
message/upgrade.txt
mod/assign/locallib.php
mod/assign/submission/onlinetext/classes/privacy/provider.php
mod/assign/tests/locallib_test.php
mod/forum/lib.php
mod/forum/styles.css
mod/forum/tests/behat/posts_ordering_blog.feature
mod/forum/upgrade.txt
mod/imscp/lib.php
mod/imscp/tests/lib_test.php
mod/label/lib.php
mod/label/tests/lib_test.php
mod/lesson/lib.php
mod/lesson/tests/lib_test.php
mod/lti/classes/local/ltiservice/resource_base.php
mod/quiz/attemptlib.php
mod/quiz/locallib.php
mod/quiz/report/grading/tests/behat/grading.feature
mod/quiz/report/overview/tests/behat/basic.feature
mod/quiz/report/reportlib.php
mod/quiz/report/responses/tests/behat/basic.feature
mod/quiz/report/statistics/lang/en/quiz_statistics.php
mod/quiz/report/statistics/statistics_table.php
mod/quiz/report/statistics/tests/behat/basic.feature [new file with mode: 0644]
mod/quiz/report/statistics/tests/statistics_table_test.php [new file with mode: 0644]
mod/quiz/tests/behat/attempt_basic.feature
mod/quiz/tests/behat/behat_mod_quiz.php
mod/quiz/tests/behat/completion_condition_attempts_used.feature
mod/quiz/tests/behat/completion_condition_passing_grade.feature
mod/quiz/tests/behat/preview.feature
mod/quiz/tests/behat/quiz_reset.feature
mod/quiz/tests/generator/lib.php
mod/quiz/tests/reportlib_test.php
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-debug.js
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-min.js
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop.js
mod/quiz/yui/src/dragdrop/js/resource.js
mod/workshop/form/numerrors/edit_form.php
mod/workshop/form/numerrors/lang/en/workshopform_numerrors.php
privacy/classes/local/request/moodle_content_writer.php
privacy/classes/output/exported_html_page.php [new file with mode: 0644]
privacy/classes/output/exported_navigation_page.php [new file with mode: 0644]
privacy/classes/output/renderer.php [new file with mode: 0644]
privacy/export_files/general.css [new file with mode: 0644]
privacy/export_files/general.js [new file with mode: 0644]
privacy/templates/htmlpage.mustache [new file with mode: 0644]
privacy/templates/navigation.mustache [new file with mode: 0644]
privacy/tests/moodle_content_writer_test.php
question/behaviour/interactive/behaviour.php
question/classes/statistics/questions/all_calculated_for_qubaid_condition.php
question/classes/statistics/questions/calculated_question_summary.php [new file with mode: 0644]
question/tests/calculated_question_summary_test.php [new file with mode: 0644]
question/tests/generator/lib.php
question/type/essay/question.php
question/type/missingtype/question.php
question/type/numerical/question.php
question/type/questionbase.php
question/type/shortanswer/question.php
question/type/truefalse/question.php
repository/dropbox/pix/icon.png
repository/dropbox/pix/icon.svg [new file with mode: 0644]
theme/boost/classes/output/core_renderer.php
version.php

index b971e30..57a230e 100644 (file)
@@ -218,6 +218,9 @@ preferences,moodle|/user/preferences.php|t/preferences',
     // coursecontact is the person responsible for course - usually manages enrolments, receives notification, etc.
     $temp = new admin_settingpage('coursecontact', new lang_string('courses'));
     $temp->add(new admin_setting_special_coursecontact());
+    $temp->add(new admin_setting_configcheckbox('coursecontactduplicates',
+            new lang_string('coursecontactduplicates', 'admin'),
+            new lang_string('coursecontactduplicates_desc', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('courselistshortnames',
             new lang_string('courselistshortnames', 'admin'),
             new lang_string('courselistshortnames_desc', 'admin'), 0));
index e29174a..96c9e55 100644 (file)
@@ -80,9 +80,11 @@ class metadata_registry {
                 // Check if the interface is deprecated.
                 if (!$manager->is_empty_subsystem($component)) {
                     $classname = $manager->get_provider_classname_for_component($component);
-                    $componentclass = new $classname();
-                    if ($componentclass instanceof \core_privacy\local\deprecated) {
-                        $internaldata['deprecated'] = true;
+                    if (class_exists($classname)) {
+                        $componentclass = new $classname();
+                        if ($componentclass instanceof \core_privacy\local\deprecated) {
+                            $internaldata['deprecated'] = true;
+                        }
                     }
                 }
 
index 09bcb1c..341bc8a 100644 (file)
@@ -139,4 +139,15 @@ class renderer extends plugin_renderer_base {
         $data = $page->export_for_template($this);
         return parent::render_from_template('tool_dataprivacy/data_deletion', $data);
     }
+
+    /**
+     * Render the user data retention summary page.
+     *
+     * @param  summary_page $page
+     * @return string html for the page.
+     */
+    public function render_summary_page(summary_page $page) {
+        $data = $page->export_for_template($this);
+        return parent::render_from_template('tool_dataprivacy/summary', $data);
+    }
 }
diff --git a/admin/tool/dataprivacy/classes/output/summary_page.php b/admin/tool/dataprivacy/classes/output/summary_page.php
new file mode 100644 (file)
index 0000000..4b93b82
--- /dev/null
@@ -0,0 +1,132 @@
+<?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/>.
+
+/**
+ * Summary page renderable.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Adrian Greeve
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_dataprivacy\output;
+defined('MOODLE_INTERNAL') || die();
+
+use renderable;
+use renderer_base;
+use templatable;
+
+
+/**
+ * Class containing the summary page renderable.
+ *
+ * @copyright  2018 Adrian Greeve
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class summary_page implements renderable, templatable {
+
+    /**
+     * Export this data so it can be used as the context for a mustache template.
+     *
+     * @param renderer_base $output
+     * @return array
+     */
+    public function export_for_template(renderer_base $output) {
+        $contextlevels = [
+            'contextlevelname10' => CONTEXT_SYSTEM,
+            'contextlevelname30' => CONTEXT_USER,
+            'contextlevelname40' => CONTEXT_COURSECAT,
+            'contextlevelname50' => CONTEXT_COURSE,
+            'contextlevelname70' => CONTEXT_MODULE,
+            'contextlevelname80' => CONTEXT_BLOCK
+        ];
+
+        $data = [];
+        $context = \context_system::instance();
+
+        foreach ($contextlevels as $levelname => $level) {
+            $classname = \context_helper::get_class_for_level($level);
+            list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
+            $purposeid = get_config('tool_dataprivacy', $purposevar);
+            $categoryid = get_config('tool_dataprivacy', $categoryvar);
+
+            $section = [];
+            $section['contextname'] = get_string($levelname, 'tool_dataprivacy');
+
+            if (empty($purposeid)) {
+                list($purposeid, $categoryid) =
+                        \tool_dataprivacy\data_registry::get_effective_default_contextlevel_purpose_and_category($level);
+            }
+            if ($purposeid == -1) {
+                $purposeid = 0;
+            }
+            $purpose = new \tool_dataprivacy\purpose($purposeid);
+            $export = new \tool_dataprivacy\external\purpose_exporter($purpose, ['context' => $context]);
+            $purposedata = $export->export($output);
+            $section['purpose'] = $purposedata;
+
+            if (empty($categoryid)) {
+                list($purposeid, $categoryid) =
+                        \tool_dataprivacy\data_registry::get_effective_default_contextlevel_purpose_and_category($level);
+            }
+            if ($categoryid == -1) {
+                $categoryid = 0;
+            }
+            $category = new \tool_dataprivacy\category($categoryid);
+            $export = new \tool_dataprivacy\external\category_exporter($category, ['context' => $context]);
+            $categorydata = $export->export($output);
+            $section['category'] = $categorydata;
+            $data['contexts'][] = $section;
+        }
+
+        // Get activity module plugin info.
+        $pluginmanager = \core_plugin_manager::instance();
+        $modplugins = $pluginmanager->get_enabled_plugins('mod');
+
+        foreach ($modplugins as $name) {
+            $classname = \context_helper::get_class_for_level($contextlevels['contextlevelname70']);
+            list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname, $name);
+            $categoryid = get_config('tool_dataprivacy', $categoryvar);
+            $purposeid = get_config('tool_dataprivacy', $purposevar);
+            if ($categoryid === false && $purposeid === false) {
+                // If no purpose and category has been set for this plugin, then there's no need to show this on the list.
+                continue;
+            }
+
+            $section = [];
+            $section['contextname'] = $pluginmanager->plugin_name('mod_' . $name);
+
+            if ($purposeid == -1) {
+                $purposeid = 0;
+            }
+            $purpose = new \tool_dataprivacy\purpose($purposeid);
+            $export = new \tool_dataprivacy\external\purpose_exporter($purpose, ['context' => $context]);
+            $purposedata = $export->export($output);
+            $section['purpose'] = $purposedata;
+
+            if ($categoryid == -1) {
+                $categoryid = 0;
+            }
+            $category = new \tool_dataprivacy\category($categoryid);
+            $export = new \tool_dataprivacy\external\category_exporter($category, ['context' => $context]);
+            $categorydata = $export->export($output);
+            $section['category'] = $categorydata;
+
+            $data['contexts'][] = $section;
+        }
+
+        return $data;
+    }
+}
index 1ddfdc9..0cbc55f 100644 (file)
@@ -76,6 +76,8 @@ $string['dataprivacy:downloadownrequest'] = 'Download your own exported data';
 $string['dataprivacy:downloadallrequests'] = 'Download exported data for everyone';
 $string['dataregistry'] = 'Data registry';
 $string['dataregistryinfo'] = 'The data registry enables categories (types of data) and purposes (the reasons for processing data) to be set for all content on the site - from users and courses down to activities and blocks. For each purpose, a retention period may be set. When a retention period has expired, the data is flagged and listed for deletion, awaiting admin confirmation.';
+$string['dataretentionexplanation'] = 'This summary shows the default categories and purposes for retaining user\'s information on this system. Certain areas of the system may have more specific categories and purposes than those listed here.';
+$string['dataretentionsummary'] = 'Data retention summary';
 $string['datarequestcreatedforuser'] = 'Data request created for {$a}';
 $string['datarequestemailsubject'] = 'Data request: {$a}';
 $string['datarequests'] = 'Data requests';
@@ -285,6 +287,7 @@ $string['statuspending'] = 'Pending';
 $string['statusrejected'] = 'Rejected';
 $string['subjectscope'] = 'Subject scope';
 $string['subjectscope_help'] = 'The subject scope lists the roles which may be assigned in this context.';
+$string['summary'] = 'Registry configuration summary';
 $string['user'] = 'User';
 $string['viewrequest'] = 'View the request';
 $string['visible'] = 'Expand all';
index fbeb61d..a9712d1 100644 (file)
@@ -65,6 +65,11 @@ function tool_dataprivacy_myprofile_navigation(tree $tree, $user, $iscurrentuser
         $category->add_node($node);
     }
 
+    $summaryurl = new moodle_url('/admin/tool/dataprivacy/summary.php');
+    $summarynode = new core_user\output\myprofile\node('privacyandpolicies', 'retentionsummary',
+            get_string('dataretentionsummary', 'tool_dataprivacy'), null, $summaryurl);
+    $category->add_node($summarynode);
+
     // Add the Privacy category to the tree if it's not empty and it doesn't exist.
     $nodes = $category->nodes;
     if (!empty($nodes)) {
@@ -77,6 +82,20 @@ function tool_dataprivacy_myprofile_navigation(tree $tree, $user, $iscurrentuser
     return false;
 }
 
+/**
+ * Callback to add footer elements.
+ *
+ * @return string HTML footer content
+ */
+function tool_dataprivacy_standard_footer_html() {
+
+    $url = new moodle_url('/admin/tool/dataprivacy/summary.php');
+    $output = html_writer::link($url, get_string('dataretentionsummary', 'tool_dataprivacy'));
+    $output = html_writer::div($output, 'summaryfooter');
+
+    return $output;
+}
+
 /**
  * Fragment to add a new purpose.
  *
diff --git a/admin/tool/dataprivacy/summary.php b/admin/tool/dataprivacy/summary.php
new file mode 100644 (file)
index 0000000..3b99ee3
--- /dev/null
@@ -0,0 +1,41 @@
+<?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/>.
+
+/**
+ * Prints the compliance data registry main page.
+ *
+ * @copyright 2018 onwards Adrian Greeve <adriangreeve.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ * @package tool_dataprivacy
+ */
+
+require_once(__DIR__ . '/../../../config.php');
+require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
+
+$url = new moodle_url('/' . $CFG->admin . '/tool/dataprivacy/summary.php');
+$title = get_string('summary', 'tool_dataprivacy');
+
+$context = \context_system::instance();
+$PAGE->set_url($url);
+$PAGE->set_context($context);
+$PAGE->set_title($title);
+$PAGE->set_heading($SITE->fullname);
+
+$output = $PAGE->get_renderer('tool_dataprivacy');
+echo $output->header();
+$summarypage = new \tool_dataprivacy\output\summary_page();
+echo $output->render($summarypage);
+echo $output->footer();
diff --git a/admin/tool/dataprivacy/templates/summary.mustache b/admin/tool/dataprivacy/templates/summary.mustache
new file mode 100644 (file)
index 0000000..22c0221
--- /dev/null
@@ -0,0 +1,123 @@
+{{!
+    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/>.
+}}
+{{!
+    @template tool_dataprivacy/summary
+
+    Summary
+
+    Classes required for JS:
+
+    Data attributes required for JS:
+
+    Context variables required for this template:
+
+    Example context (json):
+    {
+
+        "contexts": [
+            {
+                "contextname": "Site",
+                "category":
+                {
+                    "name": "Test category",
+                    "description": "<p>Description for category</p>"
+                },
+                "purpose":
+                {
+                    "name": "Test purpose",
+                    "description": "<p>Description for purpose</p>",
+                    "lawfulbases": "gdpr_art_6_1_c",
+                    "sensitivedatareasons": "gdpr_art_9_2_f",
+                    "formattedlawfulbases": [
+                        {
+                            "name": "Lawful base 1(a)",
+                            "description": "We need your information"
+                        },
+                        {
+                            "name": "Lawful base 1(b)",
+                            "description": "We really do need your information"
+                        }
+                    ],
+                    "formattedsensitivedatareasons": [
+                        {
+                            "name": "Sensitive data reason number 1",
+                            "description": "Number 1"
+                        },
+                        {
+                            "name": "Sensitive data reason number 1",
+                            "description": "Number 2"
+                        }
+                    ],
+                    "formattedretentionperiod": "10 Years"
+                }
+            }
+        ]
+    }
+}}
+<h2>{{#str}}dataretentionsummary, tool_dataprivacy{{/str}}</h2>
+<p>{{#str}}dataretentionexplanation, tool_dataprivacy{{/str}}</p>
+<div>
+    {{#contexts}}
+        <div class="card mb-3">
+            <div class="card-header"><h3>{{contextname}}</h3></div>
+            <div class="card-body p-l-2 p-r-2">
+
+                {{#category.name}}
+                <h4>{{#str}}category, tool_dataprivacy{{/str}}</h4>
+                <dl>
+                    <dt>{{category.name}}</dt>
+                    <dd>{{{category.description}}}</dd>
+                </dl>
+                <hr />
+                {{/category.name}}
+                <h4>{{#str}}purpose, tool_dataprivacy{{/str}}</h4>
+                <dl>
+                    <dt>{{purpose.name}}</dt>
+                    <dd>{{{purpose.description}}}</dd>
+                    <dt>{{#str}}retentionperiod, tool_dataprivacy{{/str}}</dt>
+                    <dd>{{purpose.formattedretentionperiod}}</dd>
+                </dl>
+                {{#purpose.lawfulbases}}
+                <table class="table table-bordered">
+                    <thead><tr><th colspan="2">{{#str}}lawfulbases, tool_dataprivacy{{/str}}</th></tr></thead>
+                    <tbody>
+                    {{#purpose.formattedlawfulbases}}
+                        <tr>
+                            <td>{{name}}</td>
+                            <td>{{description}}</td>
+                        </tr>
+                    {{/purpose.formattedlawfulbases}}
+                    </tbody>
+                </table>
+                {{/purpose.lawfulbases}}
+                {{#purpose.sensitivedatareasons}}
+                <table class="table table-bordered">
+                    <thead><tr><th colspan="2">{{#str}}sensitivedatareasons, tool_dataprivacy{{/str}}</th></tr></thead>
+                    <tbody>
+                    {{#purpose.formattedsensitivedatareasons}}
+                        <tr>
+                            <td>{{name}}</td>
+                            <td>{{description}}</td>
+                        </tr>
+                    {{/purpose.formattedsensitivedatareasons}}
+                    </tbody>
+                </table>
+                {{/purpose.sensitivedatareasons}}
+            </div>
+        </div>
+    {{/contexts}}
+</div>
\ No newline at end of file
index 9ec9c32..973c7d7 100644 (file)
@@ -85,7 +85,11 @@ class login implements renderable, templatable {
         $this->canloginasguest = $CFG->guestloginbutton and !isguestuser();
         $this->canloginbyemail = !empty($CFG->authloginviaemail);
         $this->cansignup = $CFG->registerauth == 'email' || !empty($CFG->registerauth);
-        $this->cookieshelpicon = new help_icon('cookiesenabled', 'core');
+        if ($CFG->rememberusername == 0) {
+            $this->cookieshelpicon = new help_icon('cookiesenabledonlysession', 'core');
+        } else {
+            $this->cookieshelpicon = new help_icon('cookiesenabled', 'core');
+        }
 
         $this->autofocusform = !empty($CFG->loginpageautofocus);
         $this->rememberusername = isset($CFG->rememberusername) and $CFG->rememberusername == 2;
index 6aaf269..22c04ec 100644 (file)
@@ -64,7 +64,7 @@ Feature: The timeline block allows users to see upcoming activities
     And I should see "Test feedback 3 closes" in the "Timeline" "block"
     And I should not see "Test choice 2 closes" in the "Timeline" "block"
     And I should not see "Test feedback 2 closes" in the "Timeline" "block"
-    And I click on "[data-region='paging-bar'] [data-control='next']" "css_element" in the "Timeline" "block"
+    And I click on "[data-region='paging-bar'] [data-control='next'] [data-region='page-link']" "css_element" in the "Timeline" "block"
     And I should see "Test feedback 2 closes" in the "Timeline" "block"
     And I should not see "Test assign 1 is due" in the "Timeline" "block"
     And I should not see "Test feedback 1 closes" in the "Timeline" "block"
diff --git a/blog/classes/external.php b/blog/classes/external.php
new file mode 100644 (file)
index 0000000..9333d2b
--- /dev/null
@@ -0,0 +1,180 @@
+<?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/>.
+
+/**
+ * This is the external API for blogs.
+ *
+ * @package    core_blog
+ * @copyright  2018 Juan Leyva
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_blog;
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir .'/externallib.php');
+require_once($CFG->dirroot .'/blog/lib.php');
+require_once($CFG->dirroot .'/blog/locallib.php');
+
+use external_api;
+use external_function_parameters;
+use external_value;
+use external_single_structure;
+use external_multiple_structure;
+use external_warnings;
+use context_system;
+use context_course;
+use moodle_exception;
+use core_blog\external\post_exporter;
+
+/**
+ * This is the external API for blogs.
+ *
+ * @copyright  2018 Juan Leyva
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class external extends external_api {
+
+    /**
+     * Returns description of get_entries() parameters.
+     *
+     * @return external_function_parameters
+     * @since  Moodle 3.6
+     */
+    public static function get_entries_parameters() {
+        return new external_function_parameters(
+            array(
+                'filters' => new external_multiple_structure (
+                    new external_single_structure(
+                        array(
+                            'name' => new external_value(PARAM_ALPHA,
+                                'The expected keys (value format) are:
+                                tag      PARAM_NOTAGS blog tag
+                                tagid    PARAM_INT    blog tag id
+                                userid   PARAM_INT    blog author (userid)
+                                cmid    PARAM_INT    course module id
+                                entryid  PARAM_INT    entry id
+                                groupid  PARAM_INT    group id
+                                courseid PARAM_INT    course id
+                                search   PARAM_RAW    search term
+                                '
+                            ),
+                            'value' => new external_value(PARAM_RAW, 'The value of the filter.')
+                        )
+                    ), 'Parameters to filter blog listings.', VALUE_DEFAULT, array()
+                ),
+                'page' => new external_value(PARAM_INT, 'The blog page to return.', VALUE_DEFAULT, 0),
+                'perpage' => new external_value(PARAM_INT, 'The number of posts to return per page.', VALUE_DEFAULT, 10),
+            )
+        );
+    }
+
+    /**
+     * Return blog entries.
+     *
+     * @param array $filters the parameters to filter the blog listing
+     * @param int $page the blog page to return
+     * @param int $perpage the number of posts to return per page
+     * @return array with the blog entries and warnings
+     * @since  Moodle 3.6
+     */
+    public static function get_entries($filters = array(), $page = 0, $perpage = 10) {
+        global $CFG, $DB, $PAGE;
+
+        $warnings = array();
+        $params = self::validate_parameters(self::get_entries_parameters(),
+            array('filters' => $filters, 'page' => $page, 'perpage' => $perpage));
+
+        if (empty($CFG->enableblogs)) {
+            throw new moodle_exception('blogdisable', 'blog');
+        }
+
+        // Init filters.
+        $filterstype = array('courseid' => PARAM_INT, 'groupid' => PARAM_INT, 'userid' => PARAM_INT, 'tagid' => PARAM_INT,
+            'tag' => PARAM_NOTAGS, 'cmid' => PARAM_INT, 'entryid' => PARAM_INT, 'search' => PARAM_RAW);
+        $filters = array('courseid' => null, 'groupid' => null, 'userid' => null, 'tagid' => null,
+            'tag' => null, 'cmid' => null, 'entryid' => null, 'search' => null);
+
+        foreach ($params['filters'] as $filter) {
+            $name = trim($filter['name']);
+            if (!isset($filterstype[$name])) {
+                throw new moodle_exception('errorinvalidparam', 'webservice', '', $name);
+            }
+            $filters[$name] = clean_param($filter['value'], $filterstype[$name]);
+        }
+
+        // Do not overwrite here the filters, blog_get_headers and blog_listing will take care of that.
+        list($courseid, $userid) = blog_validate_access($filters['courseid'], $filters['cmid'], $filters['groupid'],
+            $filters['entryid'], $filters['userid']);
+
+        if ($courseid && $courseid != SITEID) {
+            $context = context_course::instance($courseid);
+            self::validate_context($context);
+        } else {
+            $context = context_system::instance();
+            if ($CFG->bloglevel == BLOG_GLOBAL_LEVEL) {
+                // Everybody can see anything - no login required unless site is locked down using forcelogin.
+                if ($CFG->forcelogin) {
+                    self::validate_context($context);
+                }
+            } else {
+                self::validate_context($context);
+            }
+        }
+        $PAGE->set_context($context); // Needed by internal APIs.
+
+        // Get filters.
+        $blogheaders = blog_get_headers($filters['courseid'], $filters['groupid'], $filters['userid'], $filters['tagid'],
+            $filters['tag'], $filters['cmid'], $filters['entryid'], $filters['search']);
+        $bloglisting = new \blog_listing($blogheaders['filters']);
+
+        $page  = $params['page'];
+        $limit = empty($params['perpage']) ? get_user_preferences('blogpagesize', 10) : $params['perpage'];
+        $start = $page * $limit;
+        $entries = $bloglisting->get_entries($start, $limit);
+        $totalentries = $bloglisting->count_entries();
+
+        $exportedentries = array();
+        $output = $PAGE->get_renderer('core');
+        foreach ($entries as $entry) {
+            $exporter = new post_exporter($entry, array('context' => $context));
+            $exportedentries[] = $exporter->export($output);
+        }
+        return array(
+            'warnings' => $warnings,
+            'entries' => $exportedentries,
+            'totalentries' => $totalentries,
+        );
+    }
+
+    /**
+     * Returns description of get_entries() result value.
+     *
+     * @return external_description
+     * @since  Moodle 3.6
+     */
+    public static function get_entries_returns() {
+        return new external_single_structure(
+            array(
+                'entries' => new external_multiple_structure(
+                    post_exporter::get_read_structure()
+                ),
+                'totalentries' => new external_value(PARAM_INT, 'The total number of entries found.'),
+                'warnings' => new external_warnings(),
+            )
+        );
+    }
+}
diff --git a/blog/classes/external/post_exporter.php b/blog/classes/external/post_exporter.php
new file mode 100644 (file)
index 0000000..3d69299
--- /dev/null
@@ -0,0 +1,185 @@
+<?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/>.
+
+/**
+ * Class for exporting a blog post (entry).
+ *
+ * @package    core_blog
+ * @copyright  2018 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_blog\external;
+defined('MOODLE_INTERNAL') || die();
+
+use core\external\exporter;
+use external_util;
+use external_files;
+use renderer_base;
+use context_system;
+
+/**
+ * Class for exporting a blog post (entry).
+ *
+ * @copyright  2018 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class post_exporter extends exporter {
+
+    /**
+     * Return the list of properties.
+     *
+     * @return array list of properties
+     */
+    protected static function define_properties() {
+        return array(
+            'id' => array(
+                'type' => PARAM_INT,
+                'null' => NULL_ALLOWED,
+                'description' => 'Post/entry id.',
+            ),
+            'module' => array(
+                'type' => PARAM_ALPHANUMEXT,
+                'null' => NULL_NOT_ALLOWED,
+                'description' => 'Where it was published the post (blog, blog_external...).',
+            ),
+            'userid' => array(
+                'type' => PARAM_INT,
+                'null' => NULL_NOT_ALLOWED,
+                'default' => 0,
+                'description' => 'Post author.',
+            ),
+            'courseid' => array(
+                'type' => PARAM_INT,
+                'null' => NULL_NOT_ALLOWED,
+                'default' => 0,
+                'description' => 'Course where the post was created.',
+            ),
+            'groupid' => array(
+                'type' => PARAM_INT,
+                'null' => NULL_NOT_ALLOWED,
+                'default' => 0,
+                'description' => 'Group post was created for.',
+            ),
+            'moduleid' => array(
+                'type' => PARAM_INT,
+                'null' => NULL_NOT_ALLOWED,
+                'default' => 0,
+                'description' => 'Module id where the post was created (not used anymore).',
+            ),
+            'coursemoduleid' => array(
+                'type' => PARAM_INT,
+                'null' => NULL_NOT_ALLOWED,
+                'default' => 0,
+                'description' => 'Course module id where the post was created.',
+            ),
+            'subject' => array(
+                'type' => PARAM_TEXT,
+                'null' => NULL_NOT_ALLOWED,
+                'description' => 'Post subject.',
+            ),
+            'summary' => array(
+                'type' => PARAM_RAW,
+                'null' => NULL_ALLOWED,
+                'description' => 'Post summary.',
+            ),
+            'content' => array(
+                'type' => PARAM_RAW,
+                'null' => NULL_ALLOWED,
+                'description' => 'Post content.',
+            ),
+            'uniquehash' => array(
+                'type' => PARAM_RAW,
+                'null' => NULL_NOT_ALLOWED,
+                'description' => 'Post unique hash.',
+            ),
+            'rating' => array(
+                'type' => PARAM_INT,
+                'null' => NULL_NOT_ALLOWED,
+                'default' => 0,
+                'description' => 'Post rating.',
+            ),
+            'format' => array(
+                'type' => PARAM_INT,
+                'null' => NULL_NOT_ALLOWED,
+                'default' => 0,
+                'description' => 'Post content format.',
+            ),
+            'summaryformat' => array(
+                'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN),
+                'type' => PARAM_INT,
+                'default' => FORMAT_MOODLE,
+                'description' => 'Format for the summary field.',
+            ),
+            'attachment' => array(
+                'type' => PARAM_RAW,
+                'null' => NULL_ALLOWED,
+                'description' => 'Post atachment.',
+            ),
+            'publishstate' => array(
+                'type' => PARAM_ALPHA,
+                'null' => NULL_NOT_ALLOWED,
+                'default' => 'draft',
+                'description' => 'Post publish state.',
+            ),
+            'lastmodified' => array(
+                'type' => PARAM_INT,
+                'null' => NULL_NOT_ALLOWED,
+                'default' => 0,
+                'description' => 'When it was last modified.',
+            ),
+            'created' => array(
+                'type' => PARAM_INT,
+                'null' => NULL_NOT_ALLOWED,
+                'default' => 0,
+                'description' => 'When it was created.',
+            ),
+            'usermodified' => array(
+                'type' => PARAM_INT,
+                'null' => NULL_ALLOWED,
+                'description' => 'User that updated the post.',
+            ),
+        );
+    }
+
+    protected static function define_related() {
+        return array(
+            'context' => 'context'
+        );
+    }
+
+    protected static function define_other_properties() {
+        return array(
+            'summaryfiles' => array(
+                'type' => external_files::get_properties_for_exporter(),
+                'multiple' => true
+            ),
+            'attachmentfiles' => array(
+                'type' => external_files::get_properties_for_exporter(),
+                'multiple' => true,
+                'optional' => true
+            ),
+        );
+    }
+
+    protected function get_other_values(renderer_base $output) {
+        $context = context_system::instance(); // Files always on site context.
+
+        $values['summaryfiles'] = external_util::get_area_files($context->id, 'blog', 'post', $this->data->id);
+        $values['attachmentfiles'] = external_util::get_area_files($context->id, 'blog', 'attachment', $this->data->id);
+
+        return $values;
+    }
+}
index 664d70a..a7b8c97 100644 (file)
@@ -71,8 +71,6 @@ if (isset($userid) && empty($courseid) && empty($modid)) {
 }
 $PAGE->set_context($context);
 
-$sitecontext = context_system::instance();
-
 if (isset($userid) && $USER->id == $userid) {
     $blognode = $PAGE->navigation->find('siteblog', null);
     if ($blognode) {
@@ -108,125 +106,20 @@ if (empty($CFG->enableblogs)) {
     print_error('blogdisable', 'blog');
 }
 
-// Add courseid if modid or groupid is specified: This is used for navigation and title.
-if (!empty($modid) && empty($courseid)) {
-    $courseid = $DB->get_field('course_modules', 'course', array('id' => $modid));
-}
-
-if (!empty($groupid) && empty($courseid)) {
-    $courseid = $DB->get_field('groups', 'courseid', array('id' => $groupid));
-}
-
-
-if (!$userid && has_capability('moodle/blog:view', $sitecontext) && $CFG->bloglevel > BLOG_USER_LEVEL) {
-    if ($entryid) {
-        if (!$entryobject = $DB->get_record('post', array('id' => $entryid))) {
-            print_error('nosuchentry', 'blog');
-        }
-        $userid = $entryobject->userid;
-    }
-} else if (!$userid) {
-    $userid = $USER->id;
-}
-
-if (!empty($modid)) {
-    if ($CFG->bloglevel < BLOG_SITE_LEVEL) {
-        print_error(get_string('nocourseblogs', 'blog'));
-    }
-    if (!$mod = $DB->get_record('course_modules', array('id' => $modid))) {
-        print_error(get_string('invalidmodid', 'blog'));
-    }
-    $courseid = $mod->course;
-}
-
-if ((empty($courseid) ? true : $courseid == SITEID) && empty($userid)) {
-    if ($CFG->bloglevel < BLOG_SITE_LEVEL) {
-        print_error('siteblogdisable', 'blog');
-    }
-    if (!has_capability('moodle/blog:view', $sitecontext)) {
-        print_error('cannotviewsiteblog', 'blog');
-    }
-
-    $COURSE = $DB->get_record('course', array('format' => 'site'));
-    $courseid = $COURSE->id;
-}
-
-if (!empty($courseid)) {
-    if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-        print_error('invalidcourseid');
-    }
-
-    $courseid = $course->id;
-    require_login($course);
-
-    if (!has_capability('moodle/blog:view', $sitecontext)) {
-        print_error('cannotviewcourseblog', 'blog');
-    }
-} else {
-    $coursecontext = context_course::instance(SITEID);
-}
-
-if (!empty($groupid)) {
-    if ($CFG->bloglevel < BLOG_SITE_LEVEL) {
-        print_error('groupblogdisable', 'blog');
-    }
-
-    if (! $group = groups_get_group($groupid)) {
-        print_error(get_string('invalidgroupid', 'blog'));
-    }
+list($courseid, $userid) = blog_validate_access($courseid, $modid, $groupid, $entryid, $userid);
 
-    if (!$course = $DB->get_record('course', array('id' => $group->courseid))) {
-        print_error('invalidcourseid');
-    }
+$courseid = (empty($courseid)) ? SITEID : $courseid;
 
-    $coursecontext = context_course::instance($course->id);
-    $courseid = $course->id;
+if ($courseid != SITEID) {
+    $course = get_course($courseid);
     require_login($course);
-
-    if (!has_capability('moodle/blog:view', $sitecontext)) {
-        print_error(get_string('cannotviewcourseorgroupblog', 'blog'));
-    }
-
-    if (groups_get_course_groupmode($course) == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $coursecontext)) {
-        if (!groups_is_member($groupid)) {
-            print_error('notmemberofgroup');
-        }
-    }
 }
 
 if (!empty($userid)) {
-    if ($CFG->bloglevel < BLOG_USER_LEVEL) {
-        print_error('blogdisable', 'blog');
-    }
-
-    if (!$user = $DB->get_record('user', array('id' => $userid))) {
-        print_error('invaliduserid');
-    }
-
-    if ($user->deleted) {
-        echo $OUTPUT->header();
-        echo $OUTPUT->heading(get_string('userdeleted'));
-        echo $OUTPUT->footer();
-        die;
-    }
-
-    if ($USER->id == $userid) {
-        if (!has_capability('moodle/blog:create', $sitecontext)
-          && !has_capability('moodle/blog:view', $sitecontext)) {
-            print_error('donothaveblog', 'blog');
-        }
-    } else {
-        if (!has_capability('moodle/blog:view', $sitecontext) || !blog_user_can_view_user_entry($userid)) {
-            print_error('cannotviewcourseblog', 'blog');
-        }
-
-        $PAGE->navigation->extend_for_user($user);
-    }
+    $user = core_user::get_user($userid);
+    $PAGE->navigation->extend_for_user($user);
 }
 
-$courseid = (empty($courseid)) ? SITEID : $courseid;
-
-
 $blogheaders = blog_get_headers();
 
 $rsscontext = null;
index b81c645..f575ff7 100644 (file)
@@ -628,20 +628,29 @@ function blog_get_options_for_module($module, $user=null) {
  * It uses the current URL to build these variables.
  * A number of mutually exclusive use cases are used to structure this function.
  *
+ * @param  int $courseid   course id the the blog is associated to (can be null).
+ * @param  int $groupid    group id to filter blogs I can see (can be null)
+ * @param  int $userid     blog author id (can be null)
+ * @param  int $tagid      tag id to filter (can be null)
+ * @param  string $tag     tag name to filter (can be null)
+ * @param  int $modid      module id the blog is associated to (can be null).
+ * @param  int $entryid    blog entry id to filter(can be null)
+ * @param  string $search  string to search (can be null)
  * @return array
  */
-function blog_get_headers($courseid=null, $groupid=null, $userid=null, $tagid=null) {
+function blog_get_headers($courseid=null, $groupid=null, $userid=null, $tagid=null, $tag=null, $modid=null, $entryid=null,
+        $search = null) {
     global $CFG, $PAGE, $DB, $USER;
 
     $id       = optional_param('id', null, PARAM_INT);
-    $tag      = optional_param('tag', null, PARAM_NOTAGS);
+    $tag      = optional_param('tag', $tag, PARAM_NOTAGS);
     $tagid    = optional_param('tagid', $tagid, PARAM_INT);
     $userid   = optional_param('userid', $userid, PARAM_INT);
-    $modid    = optional_param('modid', null, PARAM_INT);
-    $entryid  = optional_param('entryid', null, PARAM_INT);
+    $modid    = optional_param('modid', $modid, PARAM_INT);
+    $entryid  = optional_param('entryid', $entryid, PARAM_INT);
     $groupid  = optional_param('groupid', $groupid, PARAM_INT);
     $courseid = optional_param('courseid', $courseid, PARAM_INT);
-    $search   = optional_param('search', null, PARAM_RAW);
+    $search   = optional_param('search', $search, PARAM_RAW);
     $action   = optional_param('action', null, PARAM_ALPHA);
     $confirm  = optional_param('confirm', false, PARAM_BOOL);
 
@@ -1166,3 +1175,131 @@ function blog_get_tagged_posts($tag, $exclusivemode = false, $fromctx = 0, $ctx
     $rv->exclusiveurl = null;
     return $rv;
 }
+
+/**
+ * Validate the access to a blog.
+ *
+ * @param  int $courseid course id the the blog is associated to (can be null).
+ * @param  int $modid    module id the blog is associated to (can be null).
+ * @param  int $groupid  group id to filter blogs I can see (can be null)
+ * @param  int $entryid  blog entry id (can be null)
+ * @param  int $userid   blog author id (can be null)
+ * @return array with the calculated course and id
+ * @since  Moodle 3.6
+ */
+function blog_validate_access($courseid, $modid, $groupid, $entryid, $userid) {
+    global $CFG, $DB, $USER, $COURSE;
+
+    $sitecontext = context_system::instance();
+
+    // Add courseid if modid or groupid is specified: This is used for navigation and title.
+    if (!empty($modid) && empty($courseid)) {
+        $courseid = $DB->get_field('course_modules', 'course', array('id' => $modid));
+    }
+
+    if (!empty($groupid) && empty($courseid)) {
+        $courseid = $DB->get_field('groups', 'courseid', array('id' => $groupid));
+    }
+
+    if (!$userid && has_capability('moodle/blog:view', $sitecontext) && $CFG->bloglevel > BLOG_USER_LEVEL) {
+        if ($entryid) {
+            if (!$entryobject = $DB->get_record('post', array('id' => $entryid))) {
+                print_error('nosuchentry', 'blog');
+            }
+            $userid = $entryobject->userid;
+        }
+    } else if (!$userid) {
+        $userid = $USER->id;
+    }
+
+    if (!empty($modid)) {
+        if ($CFG->bloglevel < BLOG_SITE_LEVEL) {
+            print_error(get_string('nocourseblogs', 'blog'));
+        }
+        if (!$mod = $DB->get_record('course_modules', array('id' => $modid))) {
+            print_error(get_string('invalidmodid', 'blog'));
+        }
+        $courseid = $mod->course;
+    }
+
+    if ((empty($courseid) ? true : $courseid == SITEID) && empty($userid)) {
+        if ($CFG->bloglevel < BLOG_SITE_LEVEL) {
+            print_error('siteblogdisable', 'blog');
+        }
+        if (!has_capability('moodle/blog:view', $sitecontext)) {
+            print_error('cannotviewsiteblog', 'blog');
+        }
+
+        $COURSE = $DB->get_record('course', array('format' => 'site'));
+        $courseid = $COURSE->id;
+    }
+
+    if (!empty($courseid)) {
+        if (!$course = $DB->get_record('course', array('id' => $courseid))) {
+            print_error('invalidcourseid');
+        }
+
+        $courseid = $course->id;
+
+        if (!has_capability('moodle/blog:view', $sitecontext)) {
+            print_error('cannotviewcourseblog', 'blog');
+        }
+    } else {
+        $coursecontext = context_course::instance(SITEID);
+    }
+
+    if (!empty($groupid)) {
+        if ($CFG->bloglevel < BLOG_SITE_LEVEL) {
+            print_error('groupblogdisable', 'blog');
+        }
+
+        if (! $group = groups_get_group($groupid)) {
+            print_error(get_string('invalidgroupid', 'blog'));
+        }
+
+        if (!$course = $DB->get_record('course', array('id' => $group->courseid))) {
+            print_error('invalidcourseid');
+        }
+
+        $coursecontext = context_course::instance($course->id);
+        $courseid = $course->id;
+
+        if (!has_capability('moodle/blog:view', $sitecontext)) {
+            print_error(get_string('cannotviewcourseorgroupblog', 'blog'));
+        }
+
+        if (groups_get_course_groupmode($course) == SEPARATEGROUPS &&
+                !has_capability('moodle/site:accessallgroups', $coursecontext)) {
+
+            if (!groups_is_member($groupid)) {
+                print_error('notmemberofgroup');
+            }
+        }
+    }
+
+    if (!empty($userid)) {
+        if ($CFG->bloglevel < BLOG_USER_LEVEL) {
+            print_error('blogdisable', 'blog');
+        }
+
+        if (!$user = $DB->get_record('user', array('id' => $userid))) {
+            print_error('invaliduserid');
+        }
+
+        if ($user->deleted) {
+            print_error('userdeleted');
+        }
+
+        if ($USER->id == $userid) {
+            if (!has_capability('moodle/blog:create', $sitecontext)
+              && !has_capability('moodle/blog:view', $sitecontext)) {
+                print_error('donothaveblog', 'blog');
+            }
+        } else {
+            if (!has_capability('moodle/blog:view', $sitecontext) || !blog_user_can_view_user_entry($userid)) {
+                print_error('cannotviewcourseblog', 'blog');
+            }
+        }
+    }
+    return array($courseid, $userid);
+}
diff --git a/blog/tests/external_test.php b/blog/tests/external_test.php
new file mode 100644 (file)
index 0000000..016865c
--- /dev/null
@@ -0,0 +1,565 @@
+<?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/>.
+
+/**
+ * Unit tests for blog external API.
+ *
+ * @package    core_blog
+ * @copyright  2018 Juan Leyva
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/blog/locallib.php');
+require_once($CFG->dirroot . '/blog/lib.php');
+
+/**
+ * Unit tests for blog external API.
+ *
+ * @package    core_blog
+ * @copyright  2018 Juan Leyva
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_blog_external_testcase extends advanced_testcase {
+
+    private $courseid;
+    private $cmid;
+    private $userid;
+    private $groupid;
+    private $tagid;
+    private $postid;
+
+    protected function setUp() {
+        global $DB, $CFG;
+        parent::setUp();
+
+        $this->resetAfterTest();
+
+        // Create default course.
+        $course = $this->getDataGenerator()->create_course(array('category' => 1, 'shortname' => 'ANON'));
+        $this->assertNotEmpty($course);
+        $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id));
+        $this->assertNotEmpty($page);
+
+        // Create default user.
+        $user = $this->getDataGenerator()->create_user(array(
+                'username' => 'testuser',
+                'firstname' => 'Jimmy',
+                'lastname' => 'Kinnon'
+        ));
+        // Enrol user.
+        $this->getDataGenerator()->enrol_user($user->id, $course->id);
+
+        $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+        $this->getDataGenerator()->create_group_member(array('groupid' => $group->id, 'userid' => $user->id));
+
+        // Create default post.
+        $post = new stdClass();
+        $post->userid = $user->id;
+        $post->courseid = $course->id;
+        $post->groupid = $group->id;
+        $post->content = 'test post content text';
+        $post->module = 'blog';
+        $post->id = $DB->insert_record('post', $post);
+
+        core_tag_tag::set_item_tags('core', 'post', $post->id, context_user::instance($user->id), array('tag1'));
+        $tagid = $DB->get_field('tag', 'id', array('name' => 'tag1'));
+
+        // Grab important ids.
+        $this->courseid = $course->id;
+        $this->cmid = $page->cmid;
+        $this->userid  = $user->id;
+        $this->groupid  = $group->id;
+        $this->tagid  = $tagid;
+        $this->postid = $post->id;
+        $this->publishstate = 'site';   // To be override in tests.
+
+        // Set default blog level.
+        $CFG->bloglevel = BLOG_SITE_LEVEL;
+    }
+
+    /**
+     * Get global public entries even for not authenticated users.
+     * We get the entry since is public.
+     */
+    public function test_get_public_entries_global_level_by_non_logged_users() {
+        global $CFG, $DB;
+
+        $CFG->bloglevel = BLOG_GLOBAL_LEVEL;
+        $CFG->forcelogin = 0;
+        // Set current entry global.
+        $DB->set_field('post', 'publishstate', 'public', array('id' => $this->postid));
+
+        $result = core_blog\external::get_entries();
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        $this->assertEquals($this->postid, $result['entries'][0]['id']);
+    }
+
+    /**
+     * Get global public entries even for not authenticated users in closed site.
+     */
+    public function test_get_public_entries_global_level_by_non_logged_users_closed_site() {
+        global $CFG, $DB;
+
+        $CFG->bloglevel = BLOG_GLOBAL_LEVEL;
+        $CFG->forcelogin = 1;
+        // Set current entry global.
+        $DB->set_field('post', 'publishstate', 'public', array('id' => $this->postid));
+
+        $this->expectException('moodle_exception');
+        core_blog\external::get_entries();
+    }
+
+    /**
+     * Get global public entries for guest users.
+     * We get the entry since is public.
+     */
+    public function test_get_public_entries_global_level_by_guest_users() {
+        global $CFG, $DB;
+
+        $CFG->bloglevel = BLOG_GLOBAL_LEVEL;
+        // Set current entry global.
+        $DB->set_field('post', 'publishstate', 'public', array('id' => $this->postid));
+
+        $this->setGuestUser();
+        $result = core_blog\external::get_entries();
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        $this->assertEquals($this->postid, $result['entries'][0]['id']);
+    }
+
+    /**
+     * Get global not public entries even for not authenticated users withouth being authenticated.
+     * We don't get any because they are not public (restricted to site users).
+     */
+    public function test_get_not_public_entries_global_level_by_non_logged_users() {
+        global $CFG;
+
+        $CFG->bloglevel = BLOG_GLOBAL_LEVEL;
+
+        $result = core_blog\external::get_entries();
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+    }
+
+    /**
+     * Get global not public entries users being guest.
+     * We don't get any because they are not public (restricted to real site users).
+     */
+    public function test_get_not_public_entries_global_level_by_guest_user() {
+        global $CFG;
+
+        $CFG->bloglevel = BLOG_GLOBAL_LEVEL;
+
+        $this->setGuestUser();
+        $result = core_blog\external::get_entries();
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+    }
+
+    /**
+     * Get site not public entries for not authenticated users.
+     * We don't get any because they are not public (restricted to real site users).
+     */
+    public function test_get_not_public_entries_site_level_by_non_logged_users() {
+        $this->expectException('require_login_exception'); // In this case we get a security exception.
+        $result = core_blog\external::get_entries();
+    }
+
+    /**
+     * Get site not public entries for guest users.
+     * We don't get any because they are not public (restricted to real site users).
+     */
+    public function test_get_not_public_entries_site_level_by_guest_users() {
+
+        $this->setGuestUser();
+        $result = core_blog\external::get_entries();
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+    }
+
+    /**
+     * Get site entries at site level by system users.
+     */
+    public function test_get_site_entries_site_level_by_normal_users() {
+
+        $this->setUser($this->userid);
+        $result = core_blog\external::get_entries();
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        $this->assertEquals($this->postid, $result['entries'][0]['id']);
+    }
+
+    /**
+     * Get draft site entries by authors.
+     */
+    public function test_get_draft_entries_site_level_by_author_users() {
+        global $DB;
+
+        // Set current entry global.
+        $DB->set_field('post', 'publishstate', 'draft', array('id' => $this->postid));
+
+        $this->setUser($this->userid);
+        $result = core_blog\external::get_entries();
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        $this->assertEquals($this->postid, $result['entries'][0]['id']);
+    }
+
+    /**
+     * Get draft site entries by not authors.
+     */
+    public function test_get_draft_entries_site_level_by_not_author_users() {
+        global $DB;
+
+        // Set current entry global.
+        $DB->set_field('post', 'publishstate', 'draft', array('id' => $this->postid));
+        $user = $this->getDataGenerator()->create_user();
+
+        $this->setUser($user);
+        $result = core_blog\external::get_entries();
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+    }
+
+    /**
+     * Get draft site entries by admin.
+     */
+    public function test_get_draft_entries_site_level_by_admin_users() {
+        global $DB;
+
+        // Set current entry global.
+        $DB->set_field('post', 'publishstate', 'draft', array('id' => $this->postid));
+        $user = $this->getDataGenerator()->create_user();
+
+        $this->setAdminUser();
+        $result = core_blog\external::get_entries();
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        $this->assertEquals($this->postid, $result['entries'][0]['id']);
+    }
+
+    /**
+     * Get draft user entries by authors.
+     */
+    public function test_get_draft_entries_user_level_by_author_users() {
+        global $CFG, $DB;
+
+        $CFG->bloglevel = BLOG_USER_LEVEL;
+        // Set current entry global.
+        $DB->set_field('post', 'publishstate', 'draft', array('id' => $this->postid));
+
+        $this->setUser($this->userid);
+        $result = core_blog\external::get_entries();
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        $this->assertEquals($this->postid, $result['entries'][0]['id']);
+    }
+
+    /**
+     * Get draft user entries by not authors.
+     */
+    public function test_get_draft_entries_user_level_by_not_author_users() {
+        global $CFG, $DB;
+
+        $CFG->bloglevel = BLOG_USER_LEVEL;
+        // Set current entry global.
+        $DB->set_field('post', 'publishstate', 'draft', array('id' => $this->postid));
+        $user = $this->getDataGenerator()->create_user();
+
+        $this->setUser($user);
+        $result = core_blog\external::get_entries();
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+    }
+
+    /**
+     * Get draft user entries by admin.
+     */
+    public function test_get_draft_entries_user_level_by_admin_users() {
+        global $CFG, $DB;
+
+        $CFG->bloglevel = BLOG_USER_LEVEL;
+        // Set current entry global.
+        $DB->set_field('post', 'publishstate', 'draft', array('id' => $this->postid));
+        $user = $this->getDataGenerator()->create_user();
+
+        $this->setAdminUser();
+        $result = core_blog\external::get_entries();
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        $this->assertEquals($this->postid, $result['entries'][0]['id']);
+    }
+
+    /**
+     * Test get all entries including testing pagination.
+     */
+    public function test_get_all_entries_including_pagination() {
+        global $DB, $USER;
+
+        $DB->set_field('post', 'publishstate', 'site', array('id' => $this->postid));
+
+        // Create another entry.
+        $this->setAdminUser();
+        $newpost = new stdClass();
+        $newpost->userid = $USER->id;
+        $newpost->content = 'test post content text';
+        $newpost->module = 'blog';
+        $newpost->publishstate = 'site';
+        $newpost->created = time() + HOURSECS;
+        $newpost->lastmodified = time() + HOURSECS;
+        $newpost->id = $DB->insert_record('post', $newpost);
+
+        $this->setUser($this->userid);
+        $result = core_blog\external::get_entries();
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(2, $result['entries']);
+        $this->assertEquals(2, $result['totalentries']);
+
+        $result = core_blog\external::get_entries(array(), 0, 1);
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        $this->assertEquals(2, $result['totalentries']);
+        $this->assertEquals($newpost->id, $result['entries'][0]['id']);
+
+        $result = core_blog\external::get_entries(array(), 1, 1);
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        $this->assertEquals(2, $result['totalentries']);
+        $this->assertEquals($this->postid, $result['entries'][0]['id']);
+    }
+
+    /**
+     * Test get entries filtering by course.
+     */
+    public function test_get_entries_filtering_by_course() {
+        global $CFG, $DB;
+
+        $DB->set_field('post', 'publishstate', 'site', array('id' => $this->postid));
+
+        $this->setAdminUser();
+        $coursecontext = context_course::instance($this->courseid);
+        $anothercourse = $this->getDataGenerator()->create_course();
+
+        // Add blog associations with a course.
+        $blog = new blog_entry($this->postid);
+        $blog->add_association($coursecontext->id);
+
+        // There is one entry associated with a course.
+        $result = core_blog\external::get_entries(array(array('name' => 'courseid', 'value' => $this->courseid)));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+
+        // There is no entry associated with a wrong course.
+        $result = core_blog\external::get_entries(array(array('name' => 'courseid', 'value' => $anothercourse->id)));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+
+        // There is no entry associated with a module.
+        $result = core_blog\external::get_entries(array(array('name' => 'cmid', 'value' => $this->cmid)));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+    }
+
+    /**
+     * Test get entries filtering by module.
+     */
+    public function test_get_entries_filtering_by_module() {
+        global $CFG, $DB;
+
+        $DB->set_field('post', 'publishstate', 'site', array('id' => $this->postid));
+
+        $this->setAdminUser();
+        $coursecontext = context_course::instance($this->courseid);
+        $contextmodule = context_module::instance($this->cmid);
+        $anothermodule = $this->getDataGenerator()->create_module('page', array('course' => $this->courseid));
+
+        // Add blog associations with a module.
+        $blog = new blog_entry($this->postid);
+        $blog->add_association($contextmodule->id);
+
+        // There is no entry associated with a course.
+        $result = core_blog\external::get_entries(array(array('name' => 'courseid', 'value' => $this->courseid)));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+
+        // There is one entry associated with a module.
+        $result = core_blog\external::get_entries(array(array('name' => 'cmid', 'value' => $this->cmid)));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+
+        // There is no entry associated with a wrong module.
+        $result = core_blog\external::get_entries(array(array('name' => 'cmid', 'value' => $anothermodule->cmid)));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+    }
+
+    /**
+     * Test get entries filtering by author.
+     */
+    public function test_get_entries_filtering_by_author() {
+        $this->setAdminUser();
+        // Filter by author.
+        $result = core_blog\external::get_entries(array(array('name' => 'userid', 'value' => $this->userid)));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        // No author.
+        $anotheruser = $this->getDataGenerator()->create_user();
+        $result = core_blog\external::get_entries(array(array('name' => 'userid', 'value' => $anotheruser->id)));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+    }
+
+    /**
+     * Test get entries filtering by entry.
+     */
+    public function test_get_entries_filtering_by_entry() {
+        $this->setAdminUser();
+        // Filter by correct entry.
+        $result = core_blog\external::get_entries(array(array('name' => 'entryid', 'value' => $this->postid)));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        // Non-existent entry.
+        $this->expectException('moodle_exception');
+        $result = core_blog\external::get_entries(array(array('name' => 'entryid', 'value' => -1)));
+    }
+
+    /**
+     * Test get entries filtering by search.
+     */
+    public function test_get_entries_filtering_by_search() {
+        $this->setAdminUser();
+        // Filter by correct search.
+        $result = core_blog\external::get_entries(array(array('name' => 'search', 'value' => 'test')));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        // Non-existent search.
+        $result = core_blog\external::get_entries(array(array('name' => 'search', 'value' => 'abc')));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+    }
+
+    /**
+     * Test get entries filtering by tag.
+     */
+    public function test_get_entries_filtering_by_tag() {
+        $this->setAdminUser();
+        // Filter by correct tag.
+        $result = core_blog\external::get_entries(array(array('name' => 'tag', 'value' => 'tag1')));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        // Create tag.
+        $tag = $this->getDataGenerator()->create_tag(array('userid' => $this->userid, 'name' => 'tag2',
+            'isstandard' => 1));
+
+        $result = core_blog\external::get_entries(array(array('name' => 'tag', 'value' => 'tag2')));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+    }
+
+    /**
+     * Test get entries filtering by tag id.
+     */
+    public function test_get_entries_filtering_by_tagid() {
+        $this->setAdminUser();
+        // Filter by correct tag.
+        $result = core_blog\external::get_entries(array(array('name' => 'tagid', 'value' => $this->tagid)));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        // Non-existent tag.
+
+        // Create tag.
+        $tag = $this->getDataGenerator()->create_tag(array('userid' => $this->userid, 'name' => 'tag2',
+            'isstandard' => 1));
+
+        $result = core_blog\external::get_entries(array(array('name' => 'tagid', 'value' => $tag->id)));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+    }
+
+    /**
+     * Test get entries filtering by group.
+     */
+    public function test_get_entries_filtering_by_group() {
+        $this->setAdminUser();
+        // Add blog associations with a course.
+        $coursecontext = context_course::instance($this->courseid);
+        $blog = new blog_entry($this->postid);
+        $blog->add_association($coursecontext->id);
+
+        // Filter by correct group.
+        $result = core_blog\external::get_entries(array(array('name' => 'groupid', 'value' => $this->groupid)));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        // Non-existent group.
+        $anotheruser = $this->getDataGenerator()->create_user();
+        $this->expectException('moodle_exception');
+        core_blog\external::get_entries(array(array('name' => 'groupid', 'value' => -1)));
+    }
+
+    /**
+     * Test get entries multiple filter.
+     */
+    public function test_get_entries_multiple_filter() {
+        $this->setAdminUser();
+        // Add blog associations with a course.
+        $coursecontext = context_course::instance($this->courseid);
+        $blog = new blog_entry($this->postid);
+        $blog->add_association($coursecontext->id);
+
+        $result = core_blog\external::get_entries(array(
+            array('name' => 'tagid', 'value' => $this->tagid),
+            array('name' => 'userid', 'value' => $this->userid),
+        ));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+
+        // Non-existent multiple filter.
+        $result = core_blog\external::get_entries(array(
+            array('name' => 'search', 'value' => 'www'),
+            array('name' => 'userid', 'value' => $this->userid),
+        ));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+    }
+
+    /**
+     * Test get entries filtering by invalid_filter.
+     */
+    public function test_get_entries_filtering_by_invalid_filter() {
+        $this->setAdminUser();
+        // Filter by incorrect filter.
+        $this->expectException('moodle_exception');
+        $result = core_blog\external::get_entries(array(array('name' => 'zzZZzz', 'value' => 'wwWWww')));
+    }
+
+    /**
+     * Test get entries when blog is disabled.
+     */
+    public function test_get_entries_blog_disabled() {
+        global $CFG;
+
+        $this->setAdminUser();
+        $CFG->enableblogs = 0;
+        // Filter by incorrect filter.
+        $this->expectException('moodle_exception');
+        $result = core_blog\external::get_entries(array(array('name' => 'zzZZzz', 'value' => 'wwWWww')));
+    }
+}
+
index 2a0bc58..2a2b7a2 100644 (file)
@@ -3744,7 +3744,7 @@ function calendar_get_allowed_event_types(int $courseid = null) {
                 $ctxfields = context_helper::get_preload_record_columns_sql('ctx');
                 $sql = "SELECT
                             c.id, c.visible, {$ctxfields}
-                        FROM {course}
+                        FROM {course} c
                         JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
                 $params = [
                     'contextlevel' => CONTEXT_COURSE,
index cb9defc..b9d3010 100644 (file)
@@ -151,37 +151,50 @@ class core_course_list_element implements IteratorAggregate {
             // No roles are configured to be displayed as course contacts.
             return array();
         }
+
+        if (!$this->has_course_contacts()) {
+            // No course contacts exist.
+            return array();
+        }
+
         if ($this->coursecontacts === null) {
             $this->coursecontacts = array();
-            $context = context_course::instance($this->id);
 
-            if (!isset($this->record->managers)) {
-                // Preload course contacts from DB.
-                $courses = array($this->id => &$this->record);
-                core_course_category::preload_course_contacts($courses);
-            }
+            $context = context_course::instance($this->id);
 
-            // Build return array with full roles names (for this course context) and users names.
             $canviewfullnames = has_capability('moodle/site:viewfullnames', $context);
+
+            $displayall = get_config('core', 'coursecontactduplicates');
+
             foreach ($this->record->managers as $ruser) {
-                if (isset($this->coursecontacts[$ruser->id])) {
-                    // Only display a user once with the highest sortorder role.
+                $processed = array_key_exists($ruser->id, $this->coursecontacts);
+                if (!$displayall && $processed) {
                     continue;
                 }
-                $user = new stdClass();
-                $user = username_load_fields_from_object($user, $ruser, null, array('id', 'username'));
-                $role = new stdClass();
-                $role->id = $ruser->roleid;
-                $role->name = $ruser->rolename;
-                $role->shortname = $ruser->roleshortname;
-                $role->coursealias = $ruser->rolecoursealias;
-
-                $this->coursecontacts[$user->id] = array(
-                    'user' => $user,
-                    'role' => $role,
-                    'rolename' => role_get_name($role, $context, ROLENAME_ALIAS),
-                    'username' => fullname($user, $canviewfullnames)
-                );
+
+                $role = (object)[
+                        'id'          => $ruser->roleid,
+                        'name'        => $ruser->rolename,
+                        'shortname'   => $ruser->roleshortname,
+                        'coursealias' => $ruser->rolecoursealias,
+                ];
+                $role->displayname = role_get_name($role, $context, ROLENAME_ALIAS);
+
+                if (!$processed) {
+                    $user = username_load_fields_from_object((object)[], $ruser, null, ['id', 'username']);
+                    $this->coursecontacts[$ruser->id] = [
+                            'user'     => $user,
+                            'username' => fullname($user, $canviewfullnames),
+
+                            // List of all roles.
+                            'roles'    => [],
+
+                            // Primary role of this user.
+                            'role'     => $role,
+                            'rolename' => $role->displayname,
+                    ];
+                }
+                $this->coursecontacts[$ruser->id]['roles'][$ruser->roleid] = $role;
             }
         }
         return $this->coursecontacts;
index 3fbf4e9..8446142 100644 (file)
@@ -72,7 +72,7 @@ class activity_navigation implements renderable, templatable {
             }
 
             $attributes = [
-                'classes' => 'btn btn-link',
+                'class' => 'btn btn-link',
                 'id' => 'prev-activity-link',
                 'title' => $linkname,
             ];
@@ -88,7 +88,7 @@ class activity_navigation implements renderable, templatable {
             }
 
             $attributes = [
-                'classes' => 'btn btn-link',
+                'class' => 'btn btn-link',
                 'id' => 'next-activity-link',
                 'title' => $linkname,
             ];
index b7cabe7..e570119 100644 (file)
@@ -2263,8 +2263,13 @@ class core_course_external extends external_api {
         foreach ($course->get_course_contacts() as $contact) {
              $coursecontacts[] = array(
                 'id' => $contact['user']->id,
-                'fullname' => $contact['username']
-            );
+                'fullname' => $contact['username'],
+                'roles' => array_map(function($role){
+                    return array('id' => $role->id, 'name' => $role->displayname);
+                }, $contact['role']),
+                'role' => array('id' => $contact['role']->id, 'name' => $contact['role']->displayname),
+                'rolename' => $contact['rolename']
+             );
         }
 
         // Allowed enrolment methods (maybe we can self-enrol).
index 91a871f..974414b 100644 (file)
@@ -1197,10 +1197,13 @@ class core_course_renderer extends plugin_renderer_base {
         // Display course contacts. See core_course_list_element::get_course_contacts().
         if ($course->has_course_contacts()) {
             $content .= html_writer::start_tag('ul', array('class' => 'teachers'));
-            foreach ($course->get_course_contacts() as $userid => $coursecontact) {
-                $name = $coursecontact['rolename'].': '.
+            foreach ($course->get_course_contacts() as $coursecontact) {
+                $rolenames = array_map(function ($role) {
+                    return $role->displayname;
+                }, $coursecontact['roles']);
+                $name = implode(", ", $rolenames).': '.
                         html_writer::link(new moodle_url('/user/view.php',
-                                array('id' => $userid, 'course' => SITEID)),
+                                array('id' => $coursecontact['user']->id, 'course' => SITEID)),
                             $coursecontact['username']);
                 $content .= html_writer::tag('li', $name);
             }
index 44b9342..93b8860 100644 (file)
@@ -1879,4 +1879,48 @@ class behat_course extends behat_base {
         $xpath = "//div[contains(@class,'block')]//li[p/*[string(.)=$coursestr or string(.)=$mycoursestr]]";
         $this->execute('behat_general::i_click_on_in_the', [get_string('participants'), 'link', $xpath, 'xpath_element']);
     }
+
+    /**
+     * Check that one teacher appears before another in the course contacts.
+     *
+     * @Given /^I should see teacher "(?P<pteacher_string>(?:[^"]|\\")*)" before "(?P<fteacher_string>(?:[^"]|\\")*)" in the course contact listing$/
+     *
+     * @param string $pteacher The first teacher to find
+     * @param string $fteacher The second teacher to find (should be after the first teacher)
+     *
+     * @throws ExpectationException
+     */
+    public function i_should_see_teacher_before($pteacher, $fteacher) {
+        $xpath = "//ul[contains(@class,'teachers')]//li//a[text()='{$pteacher}']/ancestor::li//following::a[text()='{$fteacher}']";
+        $msg = "Teacher {$pteacher} does not appear before Teacher {$fteacher}";
+        if (!$this->getSession()->getDriver()->find($xpath)) {
+            throw new ExpectationException($msg, $this->getSession());
+        }
+    }
+
+    /**
+     * Check that one teacher oes not appears after another in the course contacts.
+     *
+     * @Given /^I should not see teacher "(?P<fteacher_string>(?:[^"]|\\")*)" after "(?P<pteacher_string>(?:[^"]|\\")*)" in the course contact listing$/
+     *
+     * @param string $fteacher The teacher that should not be found (after the other teacher)
+     * @param string $pteacher The teacher after who the other should not be found (this teacher must be found!)
+     *
+     * @throws ExpectationException
+     */
+    public function i_should_not_see_teacher_after($fteacher, $pteacher) {
+        $xpathliteral = behat_context_helper::escape($pteacher);
+        $xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
+                "[count(descendant::*[contains(., $xpathliteral)]) = 0]";
+        try {
+            $nodes = $this->find_all('xpath', $xpath);
+        } catch (ElementNotFoundException $e) {
+            throw new ExpectationException('"' . $pteacher . '" text was not found in the page', $this->getSession());
+        }
+        $xpath = "//ul[contains(@class,'teachers')]//li//a[text()='{$pteacher}']/ancestor::li//following::a[text()='{$fteacher}']";
+        $msg = "Teacher {$fteacher} appears after Teacher {$pteacher}";
+        if ($this->getSession()->getDriver()->find($xpath)) {
+            throw new ExpectationException($msg, $this->getSession());
+        }
+    }
 }
diff --git a/course/tests/behat/course_contact.feature b/course/tests/behat/course_contact.feature
new file mode 100644 (file)
index 0000000..78e2e01
--- /dev/null
@@ -0,0 +1,151 @@
+@core @core_course
+Feature: Test if displaying the course contacts works correctly:
+  As a user I need to see the course contacts of a course.
+  As an admin I need to be able to control the appearance of the course contacts.
+
+  Background:
+    Given the following "categories" exist:
+      | name | category | idnumber |
+      | Cat 1 | 0 | CAT1 |
+    And the following "courses" exist:
+      | fullname | shortname | category | format |
+      | Course 1 | C1 | CAT1 | topics |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | teacher2 | Teacher | 2 | teacher2@example.com |
+      | teacher3 | Teacher | 3 | teacher3@example.com |
+      | manager1 | Manager | 1 | manager1@example.com |
+      | student1 | Student | 1 | student1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | teacher1 | C1 | teacher |
+      | teacher2 | C1 | teacher |
+      | teacher3 | C1 | editingteacher |
+      | manager1 | C1 | manager |
+
+  Scenario: Test general course contacts functionality for all user roles
+    Given I log in as "admin"
+    And I navigate to "Appearance > Courses" in site administration
+    And I set the following fields to these values:
+      | Manager | 0 |
+      | Teacher | 1 |
+      | Non-editing teacher | 0 |
+      | Display all course contact roles | 0 |
+    And I press "Save changes"
+
+    When I am on course index
+    And I should see "Cat 1" in the "#region-main" "css_element"
+    And I follow "Cat 1"
+    And I wait until the page is ready
+    And I should see "Course 1" in the "#region-main" "css_element"
+    Then I should see "Teacher 1" in the ".teachers" "css_element"
+    And I should not see "Teacher 2" in the ".teachers" "css_element"
+    And I should not see "Manager 1" in the ".teachers" "css_element"
+
+    When I log out
+    And I log in as "manager1"
+    And I am on course index
+    And I should see "Cat 1" in the "#region-main" "css_element"
+    And I follow "Cat 1"
+    And I wait until the page is ready
+    And I should see "Course 1" in the "#region-main" "css_element"
+    Then I should see "Teacher 1" in the ".teachers" "css_element"
+    And I should not see "Teacher 2" in the ".teachers" "css_element"
+    And I should not see "Manager 1" in the ".teachers" "css_element"
+
+    When I log out
+    And I log in as "teacher1"
+    And I am on course index
+    And I should see "Cat 1" in the "#region-main" "css_element"
+    And I follow "Cat 1"
+    And I wait until the page is ready
+    And I should see "Course 1" in the "#region-main" "css_element"
+    Then I should see "Teacher 1" in the ".teachers" "css_element"
+    And I should not see "Teacher 2" in the ".teachers" "css_element"
+    And I should not see "Manager 1" in the ".teachers" "css_element"
+
+    When I log out
+    And I log in as "student1"
+    And I am on course index
+    And I should see "Cat 1" in the "#region-main" "css_element"
+    And I follow "Cat 1"
+    And I wait until the page is ready
+    And I should see "Course 1" in the "#region-main" "css_element"
+    Then I should see "Teacher 1" in the ".teachers" "css_element"
+    And I should not see "Teacher 2" in the ".teachers" "css_element"
+    And I should not see "Manager 1" in the ".teachers" "css_element"
+
+  Scenario: Test course contact roles without displaying all roles
+    Given I log in as "admin"
+    And I navigate to "Appearance > Courses" in site administration
+    And I set the following fields to these values:
+      | Manager | 0 |
+      | Teacher | 1 |
+      | Non-editing teacher | 1 |
+      | Display all course contact roles | 0 |
+    And I press "Save changes"
+    When I am on course index
+    And I should see "Cat 1" in the "#region-main" "css_element"
+    And I follow "Cat 1"
+    And I wait until the page is ready
+    And I should see "Course 1" in the "#region-main" "css_element"
+    Then I should see "Teacher 1" in the ".teachers" "css_element"
+    And I should see "Teacher 2" in the ".teachers" "css_element"
+    And I should see "Teacher 3" in the ".teachers" "css_element"
+    And I should see "Teacher: Teacher 1" in the ".teachers" "css_element"
+    And I should not see "Teacher, Non-editing teacher: Teacher 1" in the ".teachers" "css_element"
+    And I should not see "Manager 1" in the ".teachers" "css_element"
+
+  Scenario: Test course contact roles with displaying all roles and standard sorting
+    Given I log in as "admin"
+    And I navigate to "Appearance > Courses" in site administration
+    And I set the following fields to these values:
+      | Manager | 0 |
+      | Teacher | 1 |
+      | Non-editing teacher | 1 |
+      | Display all course contact roles | 1 |
+    And I press "Save changes"
+    When I am on course index
+    And I should see "Cat 1" in the "#region-main" "css_element"
+    And I follow "Cat 1"
+    And I wait until the page is ready
+    And I should see "Course 1" in the "#region-main" "css_element"
+    Then I should see "Teacher 1" in the ".teachers" "css_element"
+    And I should see "Teacher 2" in the ".teachers" "css_element"
+    And I should see "Teacher 3" in the ".teachers" "css_element"
+    And I should see "Teacher, Non-editing teacher: Teacher 1" in the ".teachers" "css_element"
+    And I should not see "Teacher: Teacher 1" in the ".teachers" "css_element"
+    And I should not see "Manager 1" in the ".teachers" "css_element"
+    And I should see teacher "Teacher 1" before "Teacher 3" in the course contact listing
+    And I should see teacher "Teacher 3" before "Teacher 2" in the course contact listing
+    And I should not see teacher "Teacher 1" after "Teacher 3" in the course contact listing
+    And I should not see teacher "Teacher 3" after "Teacher 2" in the course contact listing
+
+  Scenario: Test course contact roles with displaying all roles and modified sorting
+    Given I log in as "admin"
+    And I navigate to "Appearance > Courses" in site administration
+    And I set the following fields to these values:
+      | Manager | 0 |
+      | Teacher | 1 |
+      | Non-editing teacher | 1 |
+      | Display all course contact roles | 1 |
+    And I press "Save changes"
+    And I navigate to "Users > Permissions > Define roles" in site administration
+    And I click on "Move up" "link" in the "//td[text()[contains(.,'Non-editing teacher')]]/parent::tr/td[contains(@class, 'lastcol')]" "xpath_element"
+    When I am on course index
+    And I should see "Cat 1" in the "#region-main" "css_element"
+    And I follow "Cat 1"
+    And I wait until the page is ready
+    And I should see "Course 1" in the "#region-main" "css_element"
+    Then I should see "Teacher 1" in the ".teachers" "css_element"
+    And I should see "Teacher 2" in the ".teachers" "css_element"
+    And I should see "Teacher 3" in the ".teachers" "css_element"
+    And I should see "Non-editing teacher, Teacher: Teacher 1" in the ".teachers" "css_element"
+    And I should not see "Non-editing teacher: Teacher 1" in the ".teachers" "css_element"
+    And I should not see "Manager 1" in the ".teachers" "css_element"
+    And I should see teacher "Teacher 1" before "Teacher 2" in the course contact listing
+    And I should see teacher "Teacher 2" before "Teacher 3" in the course contact listing
+    And I should not see teacher "Teacher 1" after "Teacher 2" in the course contact listing
+    And I should not see teacher "Teacher 2" after "Teacher 3" in the course contact listing
index 239744d..1dcd1ef 100644 (file)
@@ -570,6 +570,9 @@ class core_course_category_testcase extends advanced_testcase {
 
     public function test_course_contacts() {
         global $DB, $CFG;
+
+        set_config('coursecontactduplicates', false);
+
         $teacherrole = $DB->get_record('role', array('shortname'=>'editingteacher'));
         $managerrole = $DB->get_record('role', array('shortname'=>'manager'));
         $studentrole = $DB->get_record('role', array('shortname'=>'student'));
@@ -681,8 +684,167 @@ class core_course_category_testcase extends advanced_testcase {
 
         // Suspend user 4 and make sure he is no longer in contacts of course 1 in category 4.
         $manual->enrol_user($enrol[4][1], $user[4], $teacherrole->id, 0, 0, ENROL_USER_SUSPENDED);
+        $allcourses = core_course_category::get(0)->get_courses(array(
+                'recursive' => true,
+                'coursecontacts' => true,
+                'sort' => array('idnumber' => 1))
+        );
+        $contacts = $allcourses[$course[4][1]]->get_course_contacts();
+        $this->assertCount(1, $contacts);
+        $contact = reset($contacts);
+        $this->assertEquals('F5 L5', $contact['username']);
+
+        $CFG->coursecontact = $oldcoursecontact;
+    }
+
+    public function test_course_contacts_with_duplicates() {
+        global $DB, $CFG;
+
+        set_config('coursecontactduplicates', true);
+
+        $displayall = get_config('core', 'coursecontactduplicates');
+        $this->assertEquals(true, $displayall);
+
+        $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
+        $managerrole = $DB->get_record('role', array('shortname' => 'manager'));
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $oldcoursecontact = $CFG->coursecontact;
+
+        $CFG->coursecontact = $managerrole->id. ','. $teacherrole->id;
+
+        /*
+        * User is listed in course contacts for the course if he has one of the
+        * "course contact" roles ($CFG->coursecontact) AND is enrolled in the course.
+        * If the user has several roles all roles are displayed, but each role only once per user.
+        */
+
+        /*
+        * Test case:
+        *
+        * == Cat1 (user2 has teacher role)
+        *    == Cat2
+        *      -- course21 (user2 is enrolled as manager) | [Expected] Manager: F2 L2
+        *      -- course22 (user2 is enrolled as student) | [Expected] Teacher: F2 L2
+        *      == Cat4 (user2 has manager role)
+        *        -- course41 (user4 is enrolled as teacher, user5 is enrolled as manager)
+        *                                                 | [Expected] Manager: F5 L5, Teacher: F4 L4
+        *        -- course42 (user2 is enrolled as teacher) | [Expected] Manager: F2 L2
+        *    == Cat3 (user3 has manager role)
+        *      -- course31 (user3 is enrolled as student) | [Expected] Manager: F3 L3
+        *      -- course32                                | [Expected]
+        *    -- course11 (user1 is enrolled as teacher)   | [Expected] Teacher: F1 L1
+        *    -- course12 (user1 has teacher role)         | [Expected]
+        *                 also user4 is enrolled as teacher but enrolment is not active
+        */
+        $category = $course = $enrol = $user = array();
+        $category[1] = core_course_category::create(array('name' => 'Cat1'))->id;
+        $category[2] = core_course_category::create(array('name' => 'Cat2', 'parent' => $category[1]))->id;
+        $category[3] = core_course_category::create(array('name' => 'Cat3', 'parent' => $category[1]))->id;
+        $category[4] = core_course_category::create(array('name' => 'Cat4', 'parent' => $category[2]))->id;
+        foreach (array(1, 2, 3, 4) as $catid) {
+            foreach (array(1, 2) as $courseid) {
+                $course[$catid][$courseid] = $this->getDataGenerator()->create_course(array(
+                        'idnumber' => 'id'.$catid.$courseid,
+                        'category' => $category[$catid])
+                )->id;
+                $enrol[$catid][$courseid] = $DB->get_record(
+                        'enrol',
+                        array('courseid' => $course[$catid][$courseid], 'enrol' => 'manual'),
+                        '*',
+                        MUST_EXIST
+                );
+            }
+        }
+        foreach (array(1, 2, 3, 4, 5) as $userid) {
+            $user[$userid] = $this->getDataGenerator()->create_user(array(
+                            'firstname' => 'F'.$userid,
+                            'lastname' => 'L'.$userid)
+            )->id;
+        }
+
+        $manual = enrol_get_plugin('manual');
+
+        // Nobody is enrolled now and course contacts are empty.
+        $allcourses = core_course_category::get(0)->get_courses(array(
+                'recursive' => true,
+                'coursecontacts' => true,
+                'sort' => array('idnumber' => 1))
+        );
+        foreach ($allcourses as $onecourse) {
+            $this->assertEmpty($onecourse->get_course_contacts());
+        }
+
+        // Cat1: user2 has teacher role.
+        role_assign($teacherrole->id, $user[2], context_coursecat::instance($category[1]));
+        // Course21: user2 is enrolled as manager.
+        $manual->enrol_user($enrol[2][1], $user[2], $managerrole->id);
+        // Course22: user2 is enrolled as student.
+        $manual->enrol_user($enrol[2][2], $user[2], $studentrole->id);
+        // Cat4: user2 has manager role.
+        role_assign($managerrole->id, $user[2], context_coursecat::instance($category[4]));
+        // Course41: user4 is enrolled as teacher, user5 is enrolled as manager.
+        $manual->enrol_user($enrol[4][1], $user[4], $teacherrole->id);
+        $manual->enrol_user($enrol[4][1], $user[5], $managerrole->id);
+        // Course42: user2 is enrolled as teacher.
+        $manual->enrol_user($enrol[4][2], $user[2], $teacherrole->id);
+        // Cat3: user3 has manager role.
+        role_assign($managerrole->id, $user[3], context_coursecat::instance($category[3]));
+        // Course31: user3 is enrolled as student.
+        $manual->enrol_user($enrol[3][1], $user[3], $studentrole->id);
+        // Course11: user1 is enrolled as teacher and user4 is enrolled as teacher and has manager role.
+        $manual->enrol_user($enrol[1][1], $user[1], $teacherrole->id);
+        $manual->enrol_user($enrol[1][1], $user[4], $teacherrole->id);
+        role_assign($managerrole->id, $user[4], context_course::instance($course[1][1]));
+        // Course12: user1 has teacher role, but is not enrolled, as well as user4 is enrolled as teacher, but user4's enrolment is
+        // not active.
+        role_assign($teacherrole->id, $user[1], context_course::instance($course[1][2]));
+        $manual->enrol_user($enrol[1][2], $user[4], $teacherrole->id, 0, 0, ENROL_USER_SUSPENDED);
+
         $allcourses = core_course_category::get(0)->get_courses(
-            array('recursive' => true, 'coursecontacts' => true, 'sort' => array('idnumber' => 1)));
+                array('recursive' => true, 'coursecontacts' => true, 'sort' => array('idnumber' => 1)));
+        // Simplify the list of contacts for each course (similar as renderer would do).
+        $contacts = array();
+        foreach (array(1, 2, 3, 4) as $catid) {
+            foreach (array(1, 2) as $courseid) {
+                $tmp = array();
+                foreach ($allcourses[$course[$catid][$courseid]]->get_course_contacts() as $contact) {
+                    $rolenames = array_map(function ($role) {
+                        return $role->displayname;
+                    }, $contact['roles']);
+                    $tmp[] = implode(", ", $rolenames). ': '.
+                            $contact['username'];
+                }
+                $contacts[$catid][$courseid] = join(', ', $tmp);
+            }
+        }
+
+        // Assert:
+        // Course21: user2 is enrolled as manager. [Expected] Manager: F2 L2, Teacher: F2 L2.
+        $this->assertSame('Manager, Teacher: F2 L2', $contacts[2][1]);
+        // Course22: user2 is enrolled as student. [Expected] Teacher: F2 L2.
+        $this->assertSame('Teacher: F2 L2', $contacts[2][2]);
+        // Course41: user4 is enrolled as teacher, user5 is enrolled as manager. [Expected] Manager: F5 L5, Teacher: F4 L4.
+        $this->assertSame('Manager: F5 L5, Teacher: F4 L4', $contacts[4][1]);
+        // Course42: user2 is enrolled as teacher. [Expected] Manager: F2 L2, Teacher: F2 L2.
+        $this->assertSame('Manager, Teacher: F2 L2', $contacts[4][2]);
+        // Course31: user3 is enrolled as student. [Expected] Manager: F3 L3.
+        $this->assertSame('Manager: F3 L3', $contacts[3][1]);
+        // Course32: nobody is enrolled. [Expected] (nothing).
+        $this->assertSame('', $contacts[3][2]);
+        // Course11: user1 is enrolled as teacher and user4 is enrolled as teacher and has manager role. [Expected] Manager: F4 L4,
+        // Teacher: F1 L1, Teacher: F4 L4.
+        $this->assertSame('Manager, Teacher: F4 L4, Teacher: F1 L1', $contacts[1][1]);
+        // Course12: user1 has teacher role, but is not enrolled, as well as user4 is enrolled as teacher, but user4's enrolment is
+        // not active. [Expected] (nothing).
+        $this->assertSame('', $contacts[1][2]);
+
+        // Suspend user 4 and make sure he is no longer in contacts of course 1 in category 4.
+        $manual->enrol_user($enrol[4][1], $user[4], $teacherrole->id, 0, 0, ENROL_USER_SUSPENDED);
+        $allcourses = core_course_category::get(0)->get_courses(array(
+                'recursive' => true,
+                'coursecontacts' => true,
+                'sort' => array('idnumber' => 1)
+        ));
         $contacts = $allcourses[$course[4][1]]->get_course_contacts();
         $this->assertCount(1, $contacts);
         $contact = reset($contacts);
index 62a9d36..d952170 100644 (file)
@@ -1,7 +1,13 @@
 This files describes API changes in /course/*,
 information provided here is intended especially for developers.
 
+=== 3.6 ===
+
+  * External function core_course_external::get_course_public_information now returns the roles and the primary role of course
+   contacts.
+
 === 3.5 ===
+
  * There is a new capability 'moodle/course:setforcedlanguage' to control which users can force the course
    language; create_course and update_course functions delegate access control to the caller code; if you
    are calling those functions you may be interested in checking if the logged in user has 'moodle/course:setforcedlanguage' capability.
index fe7c132..86cf33e 100644 (file)
@@ -113,10 +113,8 @@ class grade_edit_tree {
 
         $object = $element['object'];
         $eid    = $element['eid'];
-        $object->name = format_string($this->gtree->get_element_header($element, true, true, true, true, true),
-            false, array("context" => context_course::instance($COURSE->id)));
+        $object->name = $this->gtree->get_element_header($element, true, true, true, true, true);
         $object->stripped_name = $this->gtree->get_element_header($element, false, false, false);
-
         $is_category_item = false;
         if ($element['type'] == 'categoryitem' || $element['type'] == 'courseitem') {
             $is_category_item = true;
index 3ffb542..b4e8d96 100644 (file)
@@ -30,7 +30,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$string['cannotcreatedboninstall'] = '<p>Î\91δÏ\85ναμία Î´Î·Î¼Î¹Î¿Ï\85Ï\81γίαÏ\82 Î²Î¬Ï\83ηÏ\82 Î´ÎµÎ´Î¿Î¼Î­Î½Ï\89ν.</p> <p>Î\97 Î²Î¬Ï\83η Î´ÎµÎ´Î¿Î¼Î­Î½Ï\89ν Ï\80οÏ\85 Ï\80Ï\81οÏ\83διοÏ\81ίÏ\83Ï\84ηκε Î´ÎµÎ½ Ï\85Ï\80άÏ\81Ï\87ει ÎºÎ±Î¹ Î¿ Ï\87Ï\81ήÏ\83Ï\84ηÏ\82 Ï\80οÏ\85 Î´Ï\8eθηκε Î´ÎµÎ½ Î­Ï\87ει Î´Î¹ÎºÎ±Î¯Ï\89μα Î½Î± Î´Î·Î¼Î¹Î¿Ï\85Ï\81γήÏ\83ει Ï\84ην Î²Î¬Ï\83η Î´ÎµÎ´Î¿Î¼Î­Î½Ï\89ν.</p> <p>Î\9f Î´Î¹Î±Ï\87ειÏ\81ιÏ\83Ï\84ήÏ\82 Ï\84οÏ\85 Î¹Ï\83Ï\84οÏ\84Ï\8cÏ\80οÏ\85 Ï\80Ï\81έÏ\80ει Î½Î± ÎµÏ\80αληθεÏ\8dÏ\83ει Ï\84ην Ï\81Ï\8dθμιÏ\83η της βάσης δεδομένων.</p>';
+$string['cannotcreatedboninstall'] = '<p>Î\91δÏ\85ναμία Î´Î·Î¼Î¹Î¿Ï\85Ï\81γίαÏ\82 Î²Î¬Ï\83ηÏ\82 Î´ÎµÎ´Î¿Î¼Î­Î½Ï\89ν.</p> <p>Î\97 Î²Î¬Ï\83η Î´ÎµÎ´Î¿Î¼Î­Î½Ï\89ν Ï\80οÏ\85 Ï\80Ï\81οÏ\83διοÏ\81ίÏ\83Ï\84ηκε Î´ÎµÎ½ Ï\85Ï\80άÏ\81Ï\87ει ÎºÎ±Î¹ Î¿ Ï\87Ï\81ήÏ\83Ï\84ηÏ\82 Ï\80οÏ\85 Î´Ï\8cθηκε Î´ÎµÎ½ Î­Ï\87ει Î´Î¹ÎºÎ±Î¯Ï\89μα Î½Î± Î´Î·Î¼Î¹Î¿Ï\85Ï\81γήÏ\83ει Ï\84ην Î²Î¬Ï\83η Î´ÎµÎ´Î¿Î¼Î­Î½Ï\89ν.</p> <p>Î\9f Î´Î¹Î±Ï\87ειÏ\81ιÏ\83Ï\84ήÏ\82 Ï\84οÏ\85 Î¹Ï\83Ï\84οÏ\84Ï\8cÏ\80οÏ\85 Ï\80Ï\81έÏ\80ει Î½Î± ÎµÏ\80αληθεÏ\8dÏ\83ει Ï\84ιÏ\82 Ï\81Ï\85θμίÏ\83ειÏ\82 της βάσης δεδομένων.</p>';
 $string['cannotcreatelangdir'] = 'Δε δημιουργήθηκε φάκελος γλώσσας.';
 $string['cannotcreatetempdir'] = 'Αδυναμία δημιουργίας προσωρινού φακέλου';
 $string['cannotdownloadcomponents'] = 'Δεν μπορεί να γίνει λήψη των στοιχείων λογισμικού';
index 0132d53..dcef1e0 100644 (file)
@@ -34,6 +34,8 @@ $string['admindirname'] = 'Φάκελος Admin';
 $string['availablelangs'] = 'Λίστα διαθέσιμων πακέτων γλωσσών';
 $string['chooselanguagehead'] = 'Επιλογή γλώσσας';
 $string['chooselanguagesub'] = 'Παρακαλούμε, επιλέξτε γλώσσα για την εγκατάσταση ΜΟΝΟ. Θα μπορείτε να επιλέξετε γλώσσα ιστοσελίδας και χρηστών σε μια μετέπειτα οθόνη.';
+$string['clialreadyconfigured'] = 'Το αρχείο ρυθμίσεων config.php υπάρχει ήδη. Χρησιμοποιήστε το admin/cli/install_database.php για να εγκαταστήσετε το Moodle για αυτόν τον ιστότοπο.';
+$string['clialreadyinstalled'] = 'Το αρχείο ρυθμίσεων config.php υπάρχει ήδη. Χρησιμοποιήστε το admin/cli/install_database.php για να αναβαθμίσετε το Moodle για αυτόν τον ιστότοπο.';
 $string['databasehost'] = 'Κεντρικός Υπολογιστής Βάσης Δεδομένων';
 $string['databasename'] = 'Όνομα Βάσης Δεδομένων';
 $string['databasetypehead'] = 'Επιλογή οδηγού βάσης δεδομένων';
index 1c93d92..6824b16 100644 (file)
@@ -382,6 +382,8 @@ $string['cookiesecure'] = 'Secure cookies only';
 $string['country'] = 'Default country';
 $string['coursecontact'] = 'Course contacts';
 $string['coursecontact_desc'] = 'This setting allows you to control who appears on the course description. Users need to have at least one of these roles in a course to be shown on the course description for that course.';
+$string['coursecontactduplicates'] = 'Display all course contact roles';
+$string['coursecontactduplicates_desc'] = 'If enabled, users with more than one of the selected course contact roles will be displayed in the course description with each of their roles. Otherwise, they will be displayed with only one role (whichever is listed highest in \'Define roles\' in the Site administration).';
 $string['coursegraceperiodafter'] = 'Grace period for past courses';
 $string['coursegraceperiodbefore'] = 'Grace period for future courses';
 $string['courselistshortnames'] = 'Display extended course names';
index 5a3e194..873ad79 100644 (file)
@@ -135,3 +135,5 @@ keepsearching,core
 allfieldsrequired,core
 previewhtml,core
 messagedselecteduserfailed,core
+eventmessagecontactblocked,core_message
+eventmessagecontactunblocked,core_message
index 4517a30..41fb26e 100644 (file)
@@ -28,9 +28,11 @@ $string['ago'] = '{$a} ago';
 $string['allusers'] = 'All messages from all users';
 $string['backupmessageshelp'] = 'If enabled, then instant messages will be included in SITE automated backups';
 $string['blockcontact'] = 'Block contact';
+$string['blockedusers'] = 'Blocked users';
 $string['blocknoncontacts'] = 'Prevent non-contacts from messaging me';
 $string['canceledit'] = 'Cancel editing messages';
 $string['contactblocked'] = 'Contact blocked';
+$string['contactrequests'] = 'Contact requests';
 $string['contacts'] = 'Contacts';
 $string['defaultmessageoutputs'] = 'Default message outputs';
 $string['defaults'] = 'Defaults';
@@ -48,10 +50,10 @@ $string['errortranslatingdefault'] = 'Error translating default setting provided
 $string['eventnotificationviewed'] = 'Notification viewed';
 $string['eventnotificationsent'] = 'Notification sent';
 $string['eventmessagecontactadded'] = 'Message contact added';
-$string['eventmessagecontactblocked'] = 'Message contact blocked';
 $string['eventmessagecontactremoved'] = 'Message contact removed';
-$string['eventmessagecontactunblocked'] = 'Message contact unblocked';
 $string['eventmessagedeleted'] = 'Message deleted';
+$string['eventmessageuserblocked'] = 'User blocked';
+$string['eventmessageuserunblocked'] = 'User unblocked';
 $string['eventmessageviewed'] = 'Message viewed';
 $string['eventmessagesent'] = 'Message sent';
 $string['forced'] = 'Locked';
@@ -69,6 +71,8 @@ $string['managemessageoutputs'] = 'Manage message outputs';
 $string['messageoutputs'] = 'Message outputs';
 $string['messagepreferences'] = 'Message preferences';
 $string['message'] = 'Message';
+$string['messagecontactrequestsnotification'] = '{$a} wants to be added as a contact';
+$string['messagecontactrequestsnotificationsubject'] = '{$a} wants to be added as a contact';
 $string['messagepreferences'] = 'Message preferences';
 $string['messages'] = 'Messages';
 $string['messagingdatahasnotbeenmigrated'] = 'Your messages are temporarily unavailable due to upgrades in the messaging infrastructure. Please wait for them to be migrated.';
@@ -107,9 +111,13 @@ $string['privacy:metadata:messages:smallmessage'] = 'A small version of the mess
 $string['privacy:metadata:messages:subject'] = 'The subject of the message';
 $string['privacy:metadata:messages:timecreated'] = 'The time when the message was created';
 $string['privacy:metadata:message_contacts'] = 'The list of contacts';
-$string['privacy:metadata:message_contacts:blocked'] = 'Flag whether or not the user is blocked';
 $string['privacy:metadata:message_contacts:contactid'] = 'The ID of the user who is a contact';
+$string['privacy:metadata:message_contacts:timecreated'] = 'The time when the contact was created';
 $string['privacy:metadata:message_contacts:userid'] = 'The ID of the user whose contact list we are viewing';
+$string['privacy:metadata:message_contact_requests'] = 'The list of contact requests';
+$string['privacy:metadata:message_contact_requests:requesteduserid'] = 'The ID of the user who received the contact request';
+$string['privacy:metadata:message_contact_requests:timecreated'] = 'The time when the contact request was created';
+$string['privacy:metadata:message_contact_requests:userid'] = 'The ID of the user who sent the contact request';
 $string['privacy:metadata:message_conversation_members'] = 'The list of users in a conversation';
 $string['privacy:metadata:message_conversation_members:conversationid'] = 'The ID of the conversation';
 $string['privacy:metadata:message_conversation_members:timecreated'] = 'The time when the member was created';
@@ -119,6 +127,10 @@ $string['privacy:metadata:message_user_actions:action'] = 'The action that was p
 $string['privacy:metadata:message_user_actions:messageid'] = 'The ID of the message this action belongs to';
 $string['privacy:metadata:message_user_actions:timecreated'] = 'The time when the action was created';
 $string['privacy:metadata:message_user_actions:userid'] = 'The ID of the user who performed this action';
+$string['privacy:metadata:message_users_blocked'] = 'The list of blocked users';
+$string['privacy:metadata:message_users_blocked:blockeduserid'] = 'The ID of the user who was blocked';
+$string['privacy:metadata:message_users_blocked:timecreated'] = 'The time when the blocked action was created';
+$string['privacy:metadata:message_users_blocked:userid'] = 'The ID of the user who performed the blocked action';
 $string['privacy:metadata:notifications'] = 'Notifications';
 $string['privacy:metadata:notifications:component'] = 'The component responsible for sending the notification';
 $string['privacy:metadata:notifications:contexturl'] = 'The URL related to this notification';
@@ -180,4 +192,6 @@ $string['writeamessage'] = 'Write a message...';
 $string['you'] = 'You:';
 
 // Deprecated since Moodle 3.6.
+$string['eventmessagecontactblocked'] = 'Message contact blocked';
+$string['eventmessagecontactunblocked'] = 'Message contact unblocked';
 $string['messagingdisabled'] = 'Messaging is disabled on this site, emails will be sent instead';
index eb72e18..ee02e87 100644 (file)
@@ -276,6 +276,8 @@ $string['cookiesenabled_help'] = 'Two cookies are used by this site:
 The essential one is the session cookie, usually called MoodleSession. You must allow this cookie into your browser to provide continuity and maintain your login from page to page. When you log out or close the browser this cookie is destroyed (in your browser and on the server).
 
 The other cookie is purely for convenience, usually called something like MOODLEID. It just remembers your username within the browser. This means when you return to this site the username field on the login page will be already filled out for you. It is safe to refuse this cookie - you will just have to retype your username every time you log in.';
+$string['cookiesenabledonlysession'] = 'Cookies must be enabled in your browser';
+$string['cookiesenabledonlysession_help'] = 'This site uses one session cookie, usually called MoodleSession. You must allow this cookie into your browser to provide continuity and maintain your login from page to page. When you log out or close the browser this cookie is destroyed (in your browser and on the server).';
 $string['cookiesnotenabled'] = 'Unfortunately, cookies are currently not enabled in your browser';
 $string['copy'] = 'copy';
 $string['copyasnoun'] = 'copy';
@@ -1194,6 +1196,7 @@ $string['messageprovider:courserequested'] = 'Course creation request notificati
 $string['messageprovider:courserequestrejected'] = 'Course creation request rejection notification';
 $string['messageprovider:errors'] = 'Important errors with the site';
 $string['messageprovider:errors_help'] = 'These are important errors that an administrator should know about.';
+$string['messageprovider:messagecontactrequests'] = 'Message contact requests notification';
 $string['messageprovider:notices'] = 'Notices about minor problems';
 $string['messageprovider:notices_help'] = 'These are notices that an administrator might be interested in seeing.';
 $string['messageprovider:insights'] = 'Insights generated by prediction models';
@@ -1512,6 +1515,7 @@ $string['payments'] = 'Payments';
 $string['paymentsorry'] = 'Thank you for your payment!  Unfortunately your payment has not yet been fully processed, and you are not yet registered to enter the course "{$a->fullname}".  Please try continuing to the course in a few seconds, but if you continue to have trouble then please alert the {$a->teacher} or the site administrator';
 $string['paymentthanks'] = 'Thank you for your payment!  You are now enrolled in your course:<br />"{$a}"';
 $string['pendingrequests'] = 'Pending requests';
+$string['percents'] = '{$a} %';
 $string['periodending'] = 'Period ending ({$a})';
 $string['perpage'] = 'Per page';
 $string['perpagea'] = 'Per page: {$a}';
index 95c19a8..0045cb6 100644 (file)
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['broughtbymoodle'] = 'This data export provided by Moodle';
+$string['exportfrom'] = 'Exported from {$a}';
+$string['exporttime'] = 'Exported on {$a}';
+$string['exportuser'] = 'Data for {$a}';
 $string['privacy:metadata'] = 'The privacy subsystem does not store any data of its own and is designed to act as a channel between components and the interface used to describe, export, and remove their data.';
 $string['trace:done'] = 'Complete';
 $string['trace:exportcomplete'] = 'Export complete';
@@ -32,4 +36,6 @@ $string['trace:processingcomponent'] = 'Processing {$a->component} ({$a->progres
 $string['trace:fetchcomponents'] = 'Fetching {$a->total} components ({$a->datetime})';
 $string['trace:deletingapproved'] = 'Performing removal of approved {$a->total} contexts ({$a->datetime})';
 $string['trace:deletingcontext'] = 'Performing removal of context from {$a->total} components ({$a->datetime})';
+$string['navigation'] = 'Navigation';
 $string['privacy:subsystem:empty'] = 'This subsystem does not store any data.';
+$string['viewdata'] = 'Click on a link in the navigation to view data.';
index 8d52c61..077b92c 100644 (file)
Binary files a/lib/amd/build/tree.min.js and b/lib/amd/build/tree.min.js differ
index 0938056..24fd7da 100644 (file)
@@ -70,6 +70,10 @@ define(['jquery'], function($) {
         this.bindEventHandlers();
     };
 
+    Tree.prototype.registerEnterCallback = function(callback) {
+        this.enterCallback = callback;
+    };
+
     /**
      * Find all visible tree items and save a cache of them on the tree object.
      *
@@ -136,7 +140,13 @@ define(['jquery'], function($) {
      * @returns {bool}
      */
     Tree.prototype.getGroupFromItem = function(item) {
-        return this.treeRoot.find('#' + item.attr('aria-owns')) || item.children('[role=group]');
+        var ariaowns = this.treeRoot.find('#' + item.attr('aria-owns'));
+        var plain = item.children('[role=group]');
+        if (ariaowns.length > plain.length) {
+            return ariaowns;
+        } else {
+            return plain;
+        }
     };
 
     /**
@@ -371,7 +381,12 @@ define(['jquery'], function($) {
             case this.keys.enter: {
                 var links = item.children('a').length ? item.children('a') : item.children().not(SELECTORS.GROUP).find('a');
                 if (links.length) {
-                    window.location.href = links.first().attr('href');
+                    // See if we have a callback.
+                    if (typeof this.enterCallback === 'function') {
+                        this.enterCallback(item);
+                    } else {
+                        window.location.href = links.first().attr('href');
+                    }
                 } else if (this.isGroupItem(item)) {
                     this.toggleGroup(item, true);
                 }
index ddbdbfb..9c1e698 100644 (file)
@@ -1054,7 +1054,7 @@ function badges_bake($hash, $badgeid, $userid = 0, $pathhash = false) {
 
     $fs = get_file_storage();
     if (!$fs->file_exists($user_context->id, 'badges', 'userbadge', $badge->id, '/', $hash . '.png')) {
-        if ($file = $fs->get_file($badge_context->id, 'badges', 'badgeimage', $badge->id, '/', 'f1.png')) {
+        if ($file = $fs->get_file($badge_context->id, 'badges', 'badgeimage', $badge->id, '/', 'f3.png')) {
             $contents = $file->get_content();
 
             $filehandler = new PNG_MetaDataHandler($contents);
index e7c631f..f1daac0 100644 (file)
@@ -946,6 +946,34 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
         return $DB->get_record('user', ['id' => $userid]);
     }
 
+    /**
+     * Set current $USER, reset access cache.
+     *
+     * In some cases, behat will execute the code as admin but in many cases we need to set an specific user as some
+     * API's might rely on the logged user to take some action.
+     *
+     * @param null|int|stdClass $user user record, null or 0 means non-logged-in, positive integer means userid
+     */
+    public static function set_user($user = null) {
+        global $DB;
+
+        if (is_object($user)) {
+            $user = clone($user);
+        } else if (!$user) {
+            // Assign valid data to admin user (some generator-related code needs a valid user).
+            $user = $DB->get_record('user', array('username' => 'admin'));
+        } else {
+            $user = $DB->get_record('user', array('id' => $user));
+        }
+        unset($user->description);
+        unset($user->access);
+        unset($user->preference);
+
+        // Ensure session is empty, as it may contain caches and user specific info.
+        \core\session\manager::init_empty_session();
+
+        \core\session\manager::set_user($user);
+    }
     /**
      * Trigger click on node via javascript instead of actually clicking on it via pointer.
      *
index da9fc4c..1c207a3 100644 (file)
@@ -26,6 +26,9 @@ namespace core\event;
 
 defined('MOODLE_INTERNAL') || die();
 
+debugging('core\\event\\message_contact_blocked has been deprecated. Please use
+        core\\event\\message_user_blocked instead', DEBUG_DEVELOPER);
+
 /**
  * Message contact blocked event class.
  *
@@ -99,4 +102,13 @@ class message_contact_blocked extends base {
         // Messaging contacts are not backed up, so no need to map them on restore.
         return array('db' => 'message_contacts', 'restore' => base::NOT_MAPPED);
     }
+
+    /**
+     * This event has been deprecated.
+     *
+     * @return boolean
+     */
+    public static function is_deprecated() {
+        return true;
+    }
 }
index 417a1a6..61f83e6 100644 (file)
@@ -26,6 +26,9 @@ namespace core\event;
 
 defined('MOODLE_INTERNAL') || die();
 
+debugging('core\\event\\message_contact_unblocked has been deperecated. Please use
+        core\\event\\message_user_unblocked instead', DEBUG_DEVELOPER);
+
 /**
  * Message contact unblocked event class.
  *
@@ -99,4 +102,13 @@ class message_contact_unblocked extends base {
         // Messaging contacts are not backed up, so no need to map them on restore.
         return array('db' => 'message_contacts', 'restore' => base::NOT_MAPPED);
     }
+
+    /**
+     * This event has been deprecated.
+     *
+     * @return boolean
+     */
+    public static function is_deprecated() {
+        return true;
+    }
 }
diff --git a/lib/classes/event/message_user_blocked.php b/lib/classes/event/message_user_blocked.php
new file mode 100644 (file)
index 0000000..cd6abbf
--- /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/>.
+
+/**
+ * Message user blocked 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();
+
+/**
+ * Message user blocked event class.
+ *
+ * @package    core
+ * @since      Moodle 3.6
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class message_user_blocked extends base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'message_users_blocked';
+        $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('eventmessageusersblocked', 'message');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' blocked the user with id '$this->relateduserid'.";
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     */
+    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() {
+        // Blocked users are not backed up, so no need to map them on restore.
+        return array('db' => 'message_users_blocked', 'restore' => base::NOT_MAPPED);
+    }
+}
diff --git a/lib/classes/event/message_user_unblocked.php b/lib/classes/event/message_user_unblocked.php
new file mode 100644 (file)
index 0000000..2463c80
--- /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/>.
+
+/**
+ * Message users unblocked 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();
+
+/**
+ * Message users unblocked event class.
+ *
+ * @package    core
+ * @since      Moodle 3.6
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class message_user_unblocked extends base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'message_users_blocked';
+        $this->data['crud'] = 'd';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventmessageusersunblocked', 'message');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' unblocked the user with id '$this->relateduserid'.";
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     */
+    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() {
+        // Messaging contacts are not backed up, so no need to map them on restore.
+        return array('db' => 'message_users_blocked', 'restore' => base::NOT_MAPPED);
+    }
+}
index 2101c71..455b29c 100644 (file)
         <KEY NAME="useridto" TYPE="foreign" FIELDS="useridto" REFTABLE="user" REFFIELDS="id"/>
       </KEYS>
     </TABLE>
-    <TABLE NAME="message_contacts" COMMENT="Maintains lists of relationships between users">
+    <TABLE NAME="message_contacts" COMMENT="Maintains lists of contacts between users">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
-        <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-        <FIELD NAME="contactid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-        <FIELD NAME="blocked" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="contactid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
+        <KEY NAME="contactid" TYPE="foreign" FIELDS="contactid" REFTABLE="user" REFFIELDS="id"/>
       </KEYS>
       <INDEXES>
         <INDEX NAME="userid-contactid" UNIQUE="true" FIELDS="userid, contactid"/>
       </INDEXES>
     </TABLE>
+    <TABLE NAME="message_contact_requests" COMMENT="Maintains list of contact requests between users">
+      <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="requesteduserid" 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="requesteduserid" TYPE="foreign" FIELDS="requesteduserid" REFTABLE="user" REFFIELDS="id"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="userid-requesteduserid" UNIQUE="true" FIELDS="userid, requesteduserid"/>
+      </INDEXES>
+    </TABLE>
+    <TABLE NAME="message_users_blocked" COMMENT="Maintains lists of blocked users">
+      <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="blockeduserid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
+        <KEY NAME="blockeduserid" TYPE="foreign" FIELDS="blockeduserid" REFTABLE="user" REFFIELDS="id"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="userid-blockeduserid" UNIQUE="true" FIELDS="userid, blockeduserid"/>
+      </INDEXES>
+    </TABLE>
     <TABLE NAME="modules" COMMENT="modules available in the site">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
index 68ac95f..7a08841 100644 (file)
@@ -105,5 +105,13 @@ $messageproviders = array (
     // User insights.
     'insights' => array (
          'capability'  => 'moodle/analytics:listinsights'
-    )
+    ),
+
+    // Message contact requests.
+    'messagecontactrequests' => [
+        'defaults' => [
+            'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF,
+            'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDOFF,
+        ]
+    ],
 );
index 08195cc..4893b41 100644 (file)
@@ -74,6 +74,15 @@ $functions = array(
         'capabilities'  => 'moodle/badges:viewotherbadges',
         'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
+    'core_blog_get_entries' => array(
+        'classname'   => 'core_blog\external',
+        'methodname'  => 'get_entries',
+        'description' => 'Returns blog entries.',
+        'type'        => 'read',
+        'services'    => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+        'ajax'          => true,
+        'loginrequired' => false,
+    ),
     'core_calendar_get_calendar_monthly_view' => array(
         'classname' => 'core_calendar_external',
         'methodname' => 'get_calendar_monthly_view',
@@ -821,11 +830,21 @@ $functions = array(
         'description' => 'Updates existing groupings',
         'type' => 'write',
     ),
+    'core_message_block_user' => array(
+        'classname' => 'core_message_external',
+        'methodname' => 'block_user',
+        'classpath' => 'message/externallib.php',
+        'description' => 'Blocks a user',
+        'type' => 'write',
+        'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
     'core_message_block_contacts' => array(
         'classname' => 'core_message_external',
         'methodname' => 'block_contacts',
         'classpath' => 'message/externallib.php',
-        'description' => 'Block contacts',
+        'description' => '** DEPRECATED ** Please do not call this function any more.
+                          Block contacts',
         'type' => 'write',
         'ajax' => true,
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
@@ -834,11 +853,47 @@ $functions = array(
         'classname' => 'core_message_external',
         'methodname' => 'create_contacts',
         'classpath' => 'message/externallib.php',
-        'description' => 'Add contacts to the contact list',
+        'description' => '** DEPRECATED ** Please do not call this function any more.
+                          Add contacts to the contact list',
+        'type' => 'write',
+        'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
+    'core_message_get_contact_requests' => array(
+        'classname' => 'core_message_external',
+        'methodname' => 'get_contact_requests',
+        'classpath' => 'message/externallib.php',
+        'description' => 'Returns contact requests for a user',
+        'type' => 'read',
+        'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
+    'core_message_create_contact_request' => array(
+        'classname' => 'core_message_external',
+        'methodname' => 'create_contact_request',
+        'classpath' => 'message/externallib.php',
+        'description' => 'Creates a contact request',
         'type' => 'write',
         'ajax' => true,
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
+    'core_message_confirm_contact_request' => array(
+        'classname' => 'core_message_external',
+        'methodname' => 'confirm_contact_request',
+        'classpath' => 'message/externallib.php',
+        'description' => 'Confirms a contact request',
+        'type' => 'write',
+        'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
+    'core_message_decline_contact_request' => array(
+        'classname' => 'core_message_external',
+        'methodname' => 'decline_contact_request',
+        'classpath' => 'message/externallib.php',
+        'description' => 'Declines a contact request',
+        'type' => 'write',
         'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'core_message_delete_contacts' => array(
         'classname' => 'core_message_external',
@@ -848,7 +903,6 @@ $functions = array(
         'type' => 'write',
         'ajax' => true,
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
-        'ajax' => true,
     ),
     'core_message_delete_conversation' => array(
         'classname' => 'core_message_external',
@@ -1043,11 +1097,21 @@ $functions = array(
         'ajax' => true,
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
+    'core_message_unblock_user' => array(
+        'classname' => 'core_message_external',
+        'methodname' => 'unblock_user',
+        'classpath' => 'message/externallib.php',
+        'description' => 'Unblocks a user',
+        'type' => 'write',
+        'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
     'core_message_unblock_contacts' => array(
         'classname' => 'core_message_external',
         'methodname' => 'unblock_contacts',
         'classpath' => 'message/externallib.php',
-        'description' => 'Unblock contacts',
+        'description' => '** DEPRECATED ** Please do not call this function any more.
+                         Unblock contacts',
         'type' => 'write',
         'ajax' => true,
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
index 7f6a1f1..a6087d3 100644 (file)
@@ -2167,7 +2167,7 @@ function xmldb_main_upgrade($oldversion) {
         $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'));
+        $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);
         }
@@ -2181,7 +2181,7 @@ function xmldb_main_upgrade($oldversion) {
         $table = new xmldb_table('messages');
 
         // Conditionally launch add index.
-        $index = new xmldb_index('conversationid_timecreated', XMLDB_INDEX_NOTUNIQUE, array('conversationidtimecreated'));
+        $index = new xmldb_index('conversationid_timecreated', XMLDB_INDEX_NOTUNIQUE, array('conversationid', 'timecreated'));
         if (!$dbman->index_exists($table, $index)) {
             $dbman->add_index($table, $index);
         }
@@ -2202,7 +2202,7 @@ function xmldb_main_upgrade($oldversion) {
 
     if ($oldversion < 2018040500.01) {
 
-        // Define field indexpriority to be added to search_index_requests. Allow null initially.
+        // Define field cohort to be added to theme. Allow null initially.
         $table = new xmldb_table('cohort');
         $field = new xmldb_field('theme', XMLDB_TYPE_CHAR, '50',
                 null, null, null, null, 'timemodified');
@@ -2324,6 +2324,7 @@ function xmldb_main_upgrade($oldversion) {
                 $DB->delete_records('blog_association', ['contextid' => $module->modcontextid]);
             }
         }
+
         // Main savepoint reached.
         upgrade_main_savepoint(true, 2018083100.01);
     }
@@ -2373,7 +2374,7 @@ function xmldb_main_upgrade($oldversion) {
 
     if ($oldversion < 2018092100.02) {
         $table = new xmldb_table('question');
-        $index = new xmldb_index('categoryidnumber', XMLDB_INDEX_UNIQUE, array('categoryidnumber'));
+        $index = new xmldb_index('categoryidnumber', XMLDB_INDEX_UNIQUE, array('category', 'idnumber'));
         if (!$dbman->index_exists($table, $index)) {
             $dbman->add_index($table, $index);
         }
@@ -2391,7 +2392,7 @@ function xmldb_main_upgrade($oldversion) {
 
     if ($oldversion < 2018092100.04) {
         $table = new xmldb_table('question_categories');
-        $index = new xmldb_index('contextididnumber', XMLDB_INDEX_UNIQUE, array('contextididnumber'));
+        $index = new xmldb_index('contextididnumber', XMLDB_INDEX_UNIQUE, array('contextid', 'idnumber'));
         if (!$dbman->index_exists($table, $index)) {
             $dbman->add_index($table, $index);
         }
@@ -2399,5 +2400,129 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2018092100.04);
     }
 
+    if ($oldversion < 2018092800.00) {
+        // Alter the table 'message_contacts'.
+        $table = new xmldb_table('message_contacts');
+
+        // Remove index so we can alter the fields.
+        $index = new xmldb_index('userid-contactid', XMLDB_INDEX_UNIQUE, ['userid', 'contactid']);
+        if ($dbman->index_exists($table, $index)) {
+            $dbman->drop_index($table, $index);
+        }
+
+        // Remove defaults of '0' from the 'userid' and 'contactid' fields.
+        $field = new xmldb_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'id');
+        $dbman->change_field_default($table, $field);
+
+        $field = new xmldb_field('contactid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'userid');
+        $dbman->change_field_default($table, $field);
+
+        // Add the missing FKs that will now be added to new installs.
+        $key = new xmldb_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']);
+        $dbman->add_key($table, $key);
+
+        $key = new xmldb_key('contactid', XMLDB_KEY_FOREIGN, ['contactid'], 'user', ['id']);
+        $dbman->add_key($table, $key);
+
+        // Re-add the index.
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Add the field 'timecreated'. Allow null, since existing records won't have an accurate value we can use.
+        $field = new xmldb_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'blocked');
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Create new 'message_contact_requests' table.
+        $table = new xmldb_table('message_contact_requests');
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null, null);
+        $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'id');
+        $table->add_field('requesteduserid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'userid');
+        $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'requesteduserid');
+
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id'], null, null);
+        $table->add_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']);
+        $table->add_key('requesteduserid', XMLDB_KEY_FOREIGN, ['requesteduserid'], 'user', ['id']);
+
+        $table->add_index('userid-requesteduserid', XMLDB_INDEX_UNIQUE, ['userid', 'requesteduserid']);
+
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Create new 'message_users_blocked' table.
+        $table = new xmldb_table('message_users_blocked');
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null, null);
+        $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'id');
+        $table->add_field('blockeduserid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'userid');
+        // Allow NULLs in the 'timecreated' field because we will be moving existing data here that has no timestamp.
+        $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'blockeduserid');
+
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id'], null, null);
+        $table->add_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']);
+        $table->add_key('blockeduserid', XMLDB_KEY_FOREIGN, ['blockeduserid'], 'user', ['id']);
+
+        $table->add_index('userid-blockeduserid', XMLDB_INDEX_UNIQUE, ['userid', 'blockeduserid']);
+
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        upgrade_main_savepoint(true, 2018092800.00);
+    }
+
+    if ($oldversion < 2018092800.01) {
+        // Move all the 'blocked' contacts to the new table 'message_users_blocked'.
+        $updatesql = "INSERT INTO {message_users_blocked} (userid, blockeduserid, timecreated)
+                           SELECT userid, contactid, null as timecreated
+                             FROM {message_contacts}
+                            WHERE blocked = :blocked";
+        $DB->execute($updatesql, ['blocked' => 1]);
+
+        // Removed the 'blocked' column from 'message_contacts'.
+        $table = new xmldb_table('message_contacts');
+        $field = new xmldb_field('blocked');
+        $dbman->drop_field($table, $field);
+
+        upgrade_main_savepoint(true, 2018092800.01);
+    }
+
+    if ($oldversion < 2018092800.02) {
+        // Delete any contacts that are not mutual (meaning they both haven't added each other).
+        $sql = "SELECT c1.id
+                  FROM {message_contacts} c1
+             LEFT JOIN {message_contacts} c2
+                    ON c1.userid = c2.contactid
+                   AND c1.contactid = c2.userid
+                 WHERE c2.id IS NULL";
+        if ($contacts = $DB->get_records_sql($sql)) {
+            list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contacts));
+            $DB->delete_records_select('message_contacts', "id $insql", $inparams);
+        }
+
+        upgrade_main_savepoint(true, 2018092800.02);
+    }
+
+    if ($oldversion < 2018092800.03) {
+        // Remove any duplicate rows - from now on adding contacts just requires 1 row.
+        // The person who made the contact request (userid) and the person who approved
+        // it (contactid). Upgrade the table so that the first person to add the contact
+        // was the one who made the request.
+        $sql = "SELECT c1.id
+                  FROM {message_contacts} c1
+            INNER JOIN {message_contacts} c2
+                    ON c1.userid = c2.contactid
+                   AND c1.contactid = c2.userid
+                 WHERE c1.id > c2.id";
+        if ($contacts = $DB->get_records_sql($sql)) {
+            list($insql, $inparams) = $DB->get_in_or_equal(array_keys($contacts));
+            $DB->delete_records_select('message_contacts', "id $insql", $inparams);
+        }
+
+        upgrade_main_savepoint(true, 2018092800.03);
+    }
+
     return true;
 }
index ca248e7..44e8b8b 100644 (file)
@@ -3455,3 +3455,159 @@ function get_roles_with_assignment_on_context(context $context) {
 
     return get_roles_used_in_context($context, false);
 }
+
+/**
+ * Add the selected user as a contact for the current user
+ *
+ * @deprecated since Moodle 3.6
+ * @param int $contactid the ID of the user to add as a contact
+ * @param int $blocked 1 if you wish to block the contact
+ * @param int $userid the user ID of the user we want to add the contact for, defaults to current user if not specified.
+ * @return bool/int false if the $contactid isnt a valid user id. True if no changes made.
+ *                  Otherwise returns the result of update_record() or insert_record()
+ */
+function message_add_contact($contactid, $blocked = 0, $userid = 0) {
+    debugging('message_add_contact() is deprecated. Please use \core_message\api::create_contact_request() instead. ' .
+        'If you wish to block or unblock a user please use \core_message\api::is_blocked() and ' .
+        '\core_message\api::block_user() or \core_message\api::unblock_user() respectively.', DEBUG_DEVELOPER);
+
+    global $USER, $DB;
+
+    if (!$DB->record_exists('user', array('id' => $contactid))) {
+        return false;
+    }
+
+    if (empty($userid)) {
+        $userid = $USER->id;
+    }
+
+    // Check if a record already exists as we may be changing blocking status.
+    if (\core_message\api::is_contact($userid, $contactid)) {
+        $isblocked = \core_message\api::is_blocked($userid, $contactid);
+        // Check if blocking status has been changed.
+        if ($isblocked != $blocked) {
+            if ($blocked == 1) {
+                if (!$isblocked) {
+                    \core_message\api::block_user($userid, $contactid);
+                }
+            } else {
+                \core_message\api::unblock_user($userid, $contactid);
+            }
+
+            return true;
+        } else {
+            // No change to blocking status.
+            return true;
+        }
+    } else {
+        if ($blocked == 1) {
+            if (!\core_message\api::is_blocked($userid, $contactid)) {
+                \core_message\api::block_user($userid, $contactid);
+            }
+        } else {
+            \core_message\api::unblock_user($userid, $contactid);
+            if (!\core_message\api::does_contact_request_exist($userid, $contactid)) {
+                \core_message\api::create_contact_request($userid, $contactid);
+            }
+        }
+
+        return true;
+    }
+}
+
+/**
+ * Remove a contact.
+ *
+ * @deprecated since Moodle 3.6
+ * @param int $contactid the user ID of the contact to remove
+ * @param int $userid the user ID of the user we want to remove the contacts for, defaults to current user if not specified.
+ * @return bool returns the result of delete_records()
+ */
+function message_remove_contact($contactid, $userid = 0) {
+    debugging('message_remove_contact() is deprecated. Please use \core_message\api::remove_contact() instead.',
+        DEBUG_DEVELOPER);
+
+    global $USER;
+
+    if (empty($userid)) {
+        $userid = $USER->id;
+    }
+
+    \core_message\api::remove_contact($userid, $contactid);
+
+    return true;
+}
+
+/**
+ * Unblock a contact.
+ *
+ * @deprecated since Moodle 3.6
+ * @param int $contactid the user ID of the contact to unblock
+ * @param int $userid the user ID of the user we want to unblock the contact for, defaults to current user
+ *  if not specified.
+ * @return bool returns the result of delete_records()
+ */
+function message_unblock_contact($contactid, $userid = 0) {
+    debugging('message_unblock_contact() is deprecated. Please use \core_message\api::unblock_user() instead.',
+        DEBUG_DEVELOPER);
+
+    global $DB, $USER;
+
+    if (!$DB->record_exists('user', array('id' => $contactid))) {
+        return false;
+    }
+
+    if (empty($userid)) {
+        $userid = $USER->id;
+    }
+
+    \core_message\api::unblock_user($userid, $contactid);
+
+    return true;
+}
+
+/**
+ * Block a user.
+ *
+ * @deprecated since Moodle 3.6
+ * @param int $contactid the user ID of the user to block
+ * @param int $userid the user ID of the user we want to unblock the contact for, defaults to current user
+ *  if not specified.
+ * @return bool
+ */
+function message_block_contact($contactid, $userid = 0) {
+    debugging('message_block_contact() is deprecated. Please use \core_message\api::is_blocked() and ' .
+        '\core_message\api::block_user() instead.', DEBUG_DEVELOPER);
+
+    global $DB, $USER;
+
+    if (!$DB->record_exists('user', array('id' => $contactid))) {
+        return false;
+    }
+
+    if (empty($userid)) {
+        $userid = $USER->id;
+    }
+
+    if (!\core_message\api::is_blocked($userid, $contactid)) {
+        \core_message\api::block_user($userid, $contactid);
+    }
+
+    return true;
+}
+
+/**
+ * Load a user's contact record
+ *
+ * @deprecated since Moodle 3.6
+ * @param int $contactid the user ID of the user whose contact record you want
+ * @return array message contacts
+ */
+function message_get_contact($contactid) {
+    debugging('message_get_contact() is deprecated. Please use \core_message\api::get_contact() instead.',
+        DEBUG_DEVELOPER);
+
+    global $USER;
+
+    return \core_message\api::get_contact($USER->id, $contactid);
+}
index 0bcfd47..61c56d8 100644 (file)
@@ -59,28 +59,6 @@ M.editor_tinymce.init_editor = function(Y, editorid, options) {
         }
     }
 
-    // We have to override the editor setup to work around a bug in iOS browsers - MDL-36803.
-    if (Y.UA.ios) {
-        // Retain any setup which is already defined.
-        options.originalSetupFunction = options.setup || function(){};
-        options.setup = function(editor) {
-            options.originalSetupFunction();
-            editor.onPostRender.add(function(ed) {
-                // Whenever there is a keydown event, ensure that the contentWindow still have focus.
-                ed.contentDocument.addEventListener('keydown', function() {
-                    ed.contentWindow.focus();
-                });
-
-                // Whenever a touch event is registered against the content document,
-                // reapply focus. This works around an issue with the location caret not
-                // being focusable without use of the Loupe.
-                ed.contentDocument.addEventListener('touchend', function() {
-                    ed.contentWindow.focus();
-                });
-            });
-        };
-    }
-
     // Retain any setup which is already defined.
     options.originalSetupFunction = options.setup || function(){};
     options.setup = function(editor) {
index d0840bd..e339c5c 100644 (file)
@@ -13,4 +13,6 @@ Description of import of various jQuery libraries into Moodle:
 
 5/ open http://127.0.0.1/lib/tests/other/jquerypage.php
 
+6/ Update the version of jquery in core_privacy\local\request\moodle_content_writer::write_html_data()
+
 Petr Skoda
index d955a17..65923f0 100644 (file)
@@ -2258,6 +2258,35 @@ function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour
     return $calendartype->timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour);
 }
 
+/**
+ * Returns a html "time" tag with both the exact user date with timezone information
+ * as a datetime attribute in the W3C format, and the user readable date and time as text.
+ *
+ * @package core
+ * @category time
+ * @param int $date the timestamp in UTC, as obtained from the database.
+ * @param string $format strftime format. You should probably get this using
+ *        get_string('strftime...', 'langconfig');
+ * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
+ *        not 99 then daylight saving will not be added.
+ *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
+ * @param bool $fixday If true (default) then the leading zero from %d is removed.
+ *        If false then the leading zero is maintained.
+ * @param bool $fixhour If true (default) then the leading zero from %I is removed.
+ * @return string the formatted date/time.
+ */
+function userdate_htmltime($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
+    $userdatestr = userdate($date, $format, $timezone, $fixday, $fixhour);
+    if (CLI_SCRIPT && !PHPUNIT_TEST) {
+        return $userdatestr;
+    }
+    $machinedate = new DateTime();
+    $machinedate->setTimestamp(intval($date));
+    $machinedate->setTimezone(core_date::get_user_timezone_object());
+
+    return html_writer::tag('time', $userdatestr, ['datetime' => $machinedate->format(DateTime::W3C)]);
+}
+
 /**
  * Returns a formatted date ensuring it is UTF-8.
  *
index 454adf3..a074857 100644 (file)
@@ -4182,7 +4182,7 @@ EOD;
 
                 // Check to see if we should be displaying a message button.
                 if (!empty($CFG->messaging) && $USER->id != $user->id && has_capability('moodle/site:sendmessage', $context)) {
-                    $iscontact = !empty(message_get_contact($user->id));
+                    $iscontact = \core_message\api::is_contact($USER->id, $user->id);
                     $contacttitle = $iscontact ? 'removefromyourcontacts' : 'addtoyourcontacts';
                     $contacturlaction = $iscontact ? 'removecontact' : 'addcontact';
                     $contactimage = $iscontact ? 'removecontact' : 'addcontact';
@@ -4388,10 +4388,16 @@ EOD;
      * @return string
      */
     public function render_login(\core_auth\output\login $form) {
+        global $CFG;
+
         $context = $form->export_for_template($this);
 
         // Override because rendering is not supported in template yet.
-        $context->cookieshelpiconformatted = $this->help_icon('cookiesenabled');
+        if ($CFG->rememberusername == 0) {
+            $context->cookieshelpiconformatted = $this->help_icon('cookiesenabledonlysession');
+        } else {
+            $context->cookieshelpiconformatted = $this->help_icon('cookiesenabled');
+        }
         $context->errorformatted = $this->error_text($context->error);
 
         return $this->render_from_template('core/loginform', $context);
index 17ce5e7..8d2a465 100644 (file)
@@ -2,3 +2,4 @@ Description of import into Moodle:
 // Download from https://requirejs.org/docs/download.html
 // Put the require.js and require.min.js and LICENSE file in this folder.
 // Check if MDL-60458 workaround can be removed.
+// Check that core_privacy\local\request\moodle_content_writer::write_html_data() does not need to be updated.
index c429fb8..ef256b4 100644 (file)
@@ -1714,73 +1714,85 @@ class core_moodlelib_testcase extends advanced_testcase {
                 'time' => '1309514400',
                 'usertimezone' => 'America/Moncton',
                 'timezone' => '0.0', // No dst offset.
-                'expectedoutput' => 'Friday, 1 July 2011, 10:00 AM'
+                'expectedoutput' => 'Friday, 1 July 2011, 10:00 AM',
+                'expectedoutputhtml' => '<time datetime="2011-07-01T07:00:00-03:00">Friday, 1 July 2011, 10:00 AM</time>'
             ),
             array(
                 'time' => '1309514400',
                 'usertimezone' => 'America/Moncton',
                 'timezone' => '99', // Dst offset and timezone offset.
-                'expectedoutput' => 'Friday, 1 July 2011, 7:00 AM'
+                'expectedoutput' => 'Friday, 1 July 2011, 7:00 AM',
+                'expectedoutputhtml' => '<time datetime="2011-07-01T07:00:00-03:00">Friday, 1 July 2011, 7:00 AM</time>'
             ),
             array(
                 'time' => '1309514400',
                 'usertimezone' => 'America/Moncton',
                 'timezone' => 'America/Moncton', // Dst offset and timezone offset.
-                'expectedoutput' => 'Friday, 1 July 2011, 7:00 AM'
+                'expectedoutput' => 'Friday, 1 July 2011, 7:00 AM',
+                'expectedoutputhtml' => '<time datetime="2011-07-01t07:00:00-03:00">Friday, 1 July 2011, 7:00 AM</time>'
             ),
             array(
                 'time' => '1293876000 ',
                 'usertimezone' => 'America/Moncton',
                 'timezone' => '0.0', // No dst offset.
-                'expectedoutput' => 'Saturday, 1 January 2011, 10:00 AM'
+                'expectedoutput' => 'Saturday, 1 January 2011, 10:00 AM',
+                'expectedoutputhtml' => '<time datetime="2011-01-01T06:00:00-04:00">Saturday, 1 January 2011, 10:00 AM</time>'
             ),
             array(
                 'time' => '1293876000 ',
                 'usertimezone' => 'America/Moncton',
                 'timezone' => '99', // No dst offset in jan, so just timezone offset.
-                'expectedoutput' => 'Saturday, 1 January 2011, 6:00 AM'
+                'expectedoutput' => 'Saturday, 1 January 2011, 6:00 AM',
+                'expectedoutputhtml' => '<time datetime="2011-01-01T06:00:00-04:00">Saturday, 1 January 2011, 6:00 AM</time>'
             ),
             array(
                 'time' => '1293876000 ',
                 'usertimezone' => 'America/Moncton',
                 'timezone' => 'America/Moncton', // No dst offset in jan.
-                'expectedoutput' => 'Saturday, 1 January 2011, 6:00 AM'
+                'expectedoutput' => 'Saturday, 1 January 2011, 6:00 AM',
+                'expectedoutputhtml' => '<time datetime="2011-01-01T06:00:00-04:00">Saturday, 1 January 2011, 6:00 AM</time>'
             ),
             array(
                 'time' => '1293876000 ',
                 'usertimezone' => '2',
                 'timezone' => '99', // Take user timezone.
-                'expectedoutput' => 'Saturday, 1 January 2011, 12:00 PM'
+                'expectedoutput' => 'Saturday, 1 January 2011, 12:00 PM',
+                'expectedoutputhtml' => '<time datetime="2011-01-01T12:00:00+02:00">Saturday, 1 January 2011, 12:00 PM</time>'
             ),
             array(
                 'time' => '1293876000 ',
                 'usertimezone' => '-2',
                 'timezone' => '99', // Take user timezone.
-                'expectedoutput' => 'Saturday, 1 January 2011, 8:00 AM'
+                'expectedoutput' => 'Saturday, 1 January 2011, 8:00 AM',
+                'expectedoutputhtml' => '<time datetime="2011-01-01T08:00:00-02:00">Saturday, 1 January 2011, 8:00 AM</time>'
             ),
             array(
                 'time' => '1293876000 ',
                 'usertimezone' => '-10',
                 'timezone' => '2', // Take this timezone.
-                'expectedoutput' => 'Saturday, 1 January 2011, 12:00 PM'
+                'expectedoutput' => 'Saturday, 1 January 2011, 12:00 PM',
+                'expectedoutputhtml' => '<time datetime="2011-01-01T00:00:00-10:00">Saturday, 1 January 2011, 12:00 PM</time>'
             ),
             array(
                 'time' => '1293876000 ',
                 'usertimezone' => '-10',
                 'timezone' => '-2', // Take this timezone.
-                'expectedoutput' => 'Saturday, 1 January 2011, 8:00 AM'
+                'expectedoutput' => 'Saturday, 1 January 2011, 8:00 AM',
+                'expectedoutputhtml' => '<time datetime="2011-01-01T00:00:00-10:00">Saturday, 1 January 2011, 8:00 AM</time>'
             ),
             array(
                 'time' => '1293876000 ',
                 'usertimezone' => '-10',
                 'timezone' => 'random/time', // This should show server time.
-                'expectedoutput' => 'Saturday, 1 January 2011, 6:00 PM'
+                'expectedoutput' => 'Saturday, 1 January 2011, 6:00 PM',
+                'expectedoutputhtml' => '<time datetime="2011-01-01T00:00:00-10:00">Saturday, 1 January 2011, 6:00 PM</time>'
             ),
             array(
                 'time' => '1293876000 ',
                 'usertimezone' => '20', // Fallback to server time zone.
                 'timezone' => '99',     // This should show user time.
-                'expectedoutput' => 'Saturday, 1 January 2011, 6:00 PM'
+                'expectedoutput' => 'Saturday, 1 January 2011, 6:00 PM',
+                'expectedoutputhtml' => '<time datetime="2011-01-01T18:00:00+08:00">Saturday, 1 January 2011, 6:00 PM</time>'
             ),
         );
 
@@ -1791,13 +1803,18 @@ class core_moodlelib_testcase extends advanced_testcase {
         foreach ($testvalues as $vals) {
             $USER->timezone = $vals['usertimezone'];
             $actualoutput = userdate($vals['time'], '%A, %d %B %Y, %I:%M %p', $vals['timezone']);
+            $actualoutputhtml = userdate_htmltime($vals['time'], '%A, %d %B %Y, %I:%M %p', $vals['timezone']);
 
             // On different systems case of AM PM changes so compare case insensitive.
             $vals['expectedoutput'] = core_text::strtolower($vals['expectedoutput']);
+            $vals['expectedoutputhtml'] = core_text::strtolower($vals['expectedoutputhtml']);
             $actualoutput = core_text::strtolower($actualoutput);
+            $actualoutputhtml = core_text::strtolower($actualoutputhtml);
 
             $this->assertSame($vals['expectedoutput'], $actualoutput,
                 "Expected: {$vals['expectedoutput']} => Actual: {$actualoutput} \ndata: " . var_export($vals, true));
+            $this->assertSame($vals['expectedoutputhtml'], $actualoutputhtml,
+                "Expected: {$vals['expectedoutputhtml']} => Actual: {$actualoutputhtml} \ndata: " . var_export($vals, true));
         }
     }
 
index 6d6ecec..288ed76 100644 (file)
@@ -130,6 +130,11 @@ any group. Besides, groups_get_members_ids_sql, get_enrolled_sql and get_enrolle
 the groupid field.
 * Added $CFG->conversionattemptlimit setting to config.php allowing a maximum number of retries before giving up conversion
   of a given document by the assignfeedback_editpdf\task\convert_submissions task. Default value: 3.
+* The following events have been deprecated and should not be used any more:
+  - message_contact_blocked
+  - message_contact_unblocked
+  The reason for this is because you can now block/unblock users without them necessarily being a contact. These events
+  have been replaced with message_user_blocked and message_user_unblocked respectively.
 
 === 3.5 ===
 
index 1e38b6b..c99cadb 100644 (file)
Binary files a/message/amd/build/message_area_profile.min.js and b/message/amd/build/message_area_profile.min.js differ
index c4cedfb..eb73dcb 100644 (file)
@@ -153,7 +153,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
          * @private
          */
         Profile.prototype._blockContact = function() {
-            var action = this._performAction('core_message_block_contacts', 'unblockcontact', 'profile-block-contact',
+            var action = this._performAction('core_message_block_user', 'unblockcontact', 'profile-block-contact',
                 'profile-unblock-contact', '');
             return action.then(function() {
                 this.messageArea.trigger(Events.CONTACTBLOCKED, this._getUserId());
@@ -167,7 +167,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
          * @private
          */
         Profile.prototype._unblockContact = function() {
-            var action = this._performAction('core_message_unblock_contacts', 'blockcontact', 'profile-unblock-contact',
+            var action = this._performAction('core_message_unblock_user', 'blockcontact', 'profile-unblock-contact',
                 'profile-block-contact', 'danger');
             return action.then(function() {
                 this.messageArea.trigger(Events.CONTACTUNBLOCKED, this._getUserId());
@@ -181,7 +181,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
          * @private
          */
         Profile.prototype._addContact = function() {
-            var action = this._performAction('core_message_create_contacts', 'removecontact', 'profile-add-contact',
+            var action = this._performAction('core_message_create_contact_request', 'removecontact', 'profile-add-contact',
                 'profile-remove-contact', 'danger');
             return action.then(function() {
                 this.messageArea.trigger(Events.CONTACTADDED, this._getUserId());
@@ -214,14 +214,40 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
          * @private
          */
         Profile.prototype._performAction = function(service, string, oldaction, newaction, newclass) {
+            // This is a temporary hack as we are getting rid of this UI.
+            var userargs = '';
+            switch (service) {
+                case 'core_message_block_user':
+                    userargs = {
+                        userid: this.messageArea.getCurrentUserId(),
+                        blockeduserid: this._getUserId()
+                    };
+                    break;
+                case 'core_message_unblock_user':
+                    userargs = {
+                        userid: this.messageArea.getCurrentUserId(),
+                        unblockeduserid: this._getUserId()
+                    };
+                    break;
+                case 'core_message_create_contact_request':
+                    userargs = {
+                        userid: this.messageArea.getCurrentUserId(),
+                        requesteduserid: this._getUserId()
+                    };
+                    break;
+                default:
+                    userargs = {
+                        userid: this.messageArea.getCurrentUserId(),
+                        userids: [
+                            this._getUserId()
+                        ]
+                    };
+            }
+
+
             var promises = Ajax.call([{
                 methodname: service,
-                args: {
-                    userid: this.messageArea.getCurrentUserId(),
-                    userids: [
-                        this._getUserId()
-                    ]
-                }
+                args: userargs
             }]);
 
             return promises[0].then(function() {
index 51a067a..8bc93a7 100644 (file)
@@ -63,8 +63,8 @@ class api {
         $ufields2 = \user_picture::fields('u2', array('lastaccess'), 'userto_id', 'userto_');
 
         $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
+                       m.smallmessage, m.timecreated, 0 as isread, $ufields, mub.id as userfrom_blocked, $ufields2,
+                       mub2.id as userto_blocked
                   FROM {messages} m
             INNER JOIN {user} u
                     ON u.id = m.useridfrom
@@ -74,10 +74,10 @@ class api {
                     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_users_blocked} mub
+                    ON (mub.blockeduserid = u.id AND mub.userid = ?)
+             LEFT JOIN {message_users_blocked} mub2
+                    ON (mub2.blockeduserid = u2.id AND mub2.userid = ?)
              LEFT JOIN {message_user_actions} mua
                     ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
                  WHERE (m.useridfrom = ? OR mcm.userid = ?)
@@ -101,7 +101,7 @@ class api {
                     $message->isread = true;
                 }
                 $blockedcol = $prefix . 'blocked';
-                $message->blocked = $message->$blockedcol;
+                $message->blocked = $message->$blockedcol ? 1 : 0;
 
                 $message->messageid = $message->id;
                 $conversations[] = helper::create_contact($message, $prefix);
@@ -126,12 +126,12 @@ class api {
 
         // Get all the users in the course.
         list($esql, $params) = get_enrolled_sql(\context_course::instance($courseid), '', 0, true);
-        $sql = "SELECT u.*, mc.blocked
+        $sql = "SELECT u.*, mub.id as isblocked
                   FROM {user} u
                   JOIN ($esql) je
                     ON je.id = u.id
-             LEFT JOIN {message_contacts} mc
-                    ON (mc.contactid = u.id AND mc.userid = :userid)
+             LEFT JOIN {message_users_blocked} mub
+                    ON (mub.blockeduserid = u.id AND mub.userid = :userid)
                  WHERE u.deleted = 0";
         // Add more conditions.
         $fullname = $DB->sql_fullname();
@@ -144,6 +144,7 @@ class api {
         $contacts = array();
         if ($users = $DB->get_records_sql($sql, $params, $limitfrom, $limitnum)) {
             foreach ($users as $user) {
+                $user->blocked = $user->isblocked ? 1 : 0;
                 $contacts[] = helper::create_contact($user);
             }
         }
@@ -172,19 +173,22 @@ class api {
 
         // Ok, let's search for contacts first.
         $contacts = array();
-        $sql = "SELECT $ufields, mc.blocked
+        $sql = "SELECT $ufields, mub.id as isuserblocked
                   FROM {user} u
                   JOIN {message_contacts} mc
                     ON u.id = mc.contactid
+             LEFT JOIN {message_users_blocked} mub
+                    ON (mub.userid = :userid2 AND mub.blockeduserid = u.id)
                  WHERE mc.userid = :userid
                    AND u.deleted = 0
                    AND u.confirmed = 1
                    AND " . $DB->sql_like($fullname, ':search', false) . "
                    AND u.id $exclude
               ORDER BY " . $DB->sql_fullname();
-        if ($users = $DB->get_records_sql($sql, array('userid' => $userid, 'search' => '%' . $search . '%') + $excludeparams,
-            0, $limitnum)) {
+        if ($users = $DB->get_records_sql($sql, array('userid' => $userid, 'userid2' => $userid,
+                'search' => '%' . $search . '%') + $excludeparams, 0, $limitnum)) {
             foreach ($users as $user) {
+                $user->blocked = $user->isuserblocked ? 1 : 0;
                 $contacts[] = helper::create_contact($user);
             }
         }
@@ -225,7 +229,7 @@ class api {
                                      WHERE userid = :userid)
               ORDER BY " . $DB->sql_fullname();
         if ($users = $DB->get_records_sql($sql,  array('userid' => $userid, 'search' => '%' . $search . '%') + $excludeparams,
-            0, $limitnum)) {
+                0, $limitnum)) {
             foreach ($users as $user) {
                 $noncontacts[] = helper::create_contact($user);
             }
@@ -324,7 +328,7 @@ class api {
             return [];
         }
 
-        $contactssql = "SELECT contactid, blocked
+        $contactssql = "SELECT contactid
                           FROM {message_contacts}
                          WHERE userid = ?
                            AND contactid $useridsql";
@@ -401,21 +405,41 @@ class api {
     public static function get_contacts($userid, $limitfrom = 0, $limitnum = 0) {
         global $DB;
 
-        $arrcontacts = array();
-        $sql = "SELECT u.*, mc.blocked
+        $contactids = [];
+        $sql = "SELECT mc.*
                   FROM {message_contacts} mc
-                  JOIN {user} u
-                    ON mc.contactid = u.id
-                 WHERE mc.userid = :userid
-                   AND u.deleted = 0
-              ORDER BY " . $DB->sql_fullname();
-        if ($contacts = $DB->get_records_sql($sql, array('userid' => $userid), $limitfrom, $limitnum)) {
+                 WHERE mc.userid = ? OR mc.contactid = ?
+              ORDER BY timecreated DESC";
+        if ($contacts = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
             foreach ($contacts as $contact) {
-                $arrcontacts[] = helper::create_contact($contact);
+                if ($userid == $contact->userid) {
+                    $contactids[] = $contact->contactid;
+                } else {
+                    $contactids[] = $contact->userid;
+                }
+            }
+        }
+
+        if (!empty($contactids)) {
+            list($insql, $inparams) = $DB->get_in_or_equal($contactids);
+
+            $sql = "SELECT u.*, mub.id as isblocked
+                      FROM {user} u
+                 LEFT JOIN {message_users_blocked} mub
+                        ON u.id = mub.blockeduserid
+                     WHERE u.id $insql";
+            if ($contacts = $DB->get_records_sql($sql, $inparams)) {
+                $arrcontacts = [];
+                foreach ($contacts as $contact) {
+                    $contact->blocked = $contact->isblocked ? 1 : 0;
+                    $arrcontacts[] = helper::create_contact($contact);
+                }
+
+                return $arrcontacts;
             }
         }
 
-        return $arrcontacts;
+        return [];
     }
 
     /**
@@ -434,21 +458,24 @@ class api {
         $unreadcountssql = "SELECT $userfields, count(m.id) as messagecount
                               FROM {message_contacts} mc
                         INNER JOIN {user} u
-                                ON u.id = mc.contactid
+                                ON (u.id = mc.contactid OR u.id = mc.userid)
                          LEFT JOIN {messages} m
-                                ON m.useridfrom = mc.contactid
+                                ON ((m.useridfrom = mc.contactid OR m.useridfrom = mc.userid) AND m.useridfrom != ?)
                          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 = ?)
+                         LEFT JOIN {message_users_blocked} mub
+                                ON (mub.userid = ? AND mub.blockeduserid = u.id)
                              WHERE mua.id is NULL
-                               AND mc.userid = ?
-                               AND mc.blocked = 0
+                               AND mub.id is NULL
+                               AND (mc.userid = ? OR mc.contactid = ?)
+                               AND u.id != ?
                                AND u.deleted = 0
                           GROUP BY $userfields";
 
-        return $DB->get_records_sql($unreadcountssql, [$userid, $userid, self::MESSAGE_ACTION_READ,
-            $userid, $userid], $limitfrom, $limitnum);
+        return $DB->get_records_sql($unreadcountssql, [$userid, $userid, $userid, self::MESSAGE_ACTION_READ,
+            $userid, $userid, $userid, $userid], $limitfrom, $limitnum);
     }
 
     /**
@@ -474,14 +501,17 @@ class api {
                                 ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
                          LEFT JOIN {message_contacts} mc
                                 ON (mc.userid = ? AND mc.contactid = u.id)
+                         LEFT JOIN {message_users_blocked} mub
+                                ON (mub.userid = ? AND mub.blockeduserid = u.id)
                              WHERE mcm.userid = ?
                                AND mcm.userid != m.useridfrom
                                AND mua.id is NULL
+                               AND mub.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],
+        return $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, $userid, $userid, $userid],
             $limitfrom, $limitnum);
     }
 
@@ -550,7 +580,7 @@ class api {
      * @return \stdClass
      */
     public static function get_profile($userid, $otheruserid) {
-        global $CFG, $DB, $PAGE;
+        global $CFG, $PAGE;
 
         require_once($CFG->dirroot . '/user/lib.php');
 
@@ -587,15 +617,8 @@ class api {
             }
         }
 
-        // Check if the contact has been blocked.
-        $contact = $DB->get_record('message_contacts', array('userid' => $userid, 'contactid' => $otheruserid));
-        if ($contact) {
-            $data->isblocked = (bool) $contact->blocked;
-            $data->iscontact = true;
-        } else {
-            $data->isblocked = false;
-            $data->iscontact = false;
-        }
+        $data->isblocked = self::is_blocked($userid, $otheruserid);
+        $data->iscontact = self::is_contact($userid, $otheruserid);
 
         return $data;
     }
@@ -854,9 +877,9 @@ class api {
             $user = $USER;
         }
 
-        $sql = "SELECT count(mc.id)
-                  FROM {message_contacts} mc
-                 WHERE mc.userid = :userid AND mc.blocked = 1";
+        $sql = "SELECT count(mub.id)
+                  FROM {message_users_blocked} mub
+                 WHERE mub.userid = :userid";
         return $DB->count_records_sql($sql, array('userid' => $user->id));
     }
 
@@ -890,8 +913,14 @@ class api {
         if ($sender !== null && isset($sender->id)) {
             $senderid = $sender->id;
         }
+
+        $systemcontext = \context_system::instance();
+        if (has_capability('moodle/site:readallmessages', $systemcontext, $senderid)) {
+            return true;
+        }
+
         // The recipient has specifically blocked this sender.
-        if (self::is_user_blocked($recipient->id, $senderid)) {
+        if (self::is_blocked($recipient->id, $senderid)) {
             return false;
         }
 
@@ -908,7 +937,7 @@ class api {
      * @return bool true if $sender is blocked, false otherwise.
      */
     public static function is_user_non_contact_blocked($recipient, $sender = null) {
-        global $USER, $DB;
+        global $USER;
 
         if (is_null($sender)) {
             // The message is from the logged in user, unless otherwise specified.
@@ -918,8 +947,7 @@ class api {
         $blockednoncontacts = get_user_preferences('message_blocknoncontacts', '', $recipient->id);
         if (!empty($blockednoncontacts)) {
             // Confirm the sender is a contact of the recipient.
-            $exists = $DB->record_exists('message_contacts', array('userid' => $recipient->id, 'contactid' => $sender->id));
-            if ($exists) {
+            if (self::is_contact($sender->id, $recipient->id)) {
                 // All good, the recipient is a contact of the sender.
                 return false;
             } else {
@@ -937,12 +965,16 @@ class api {
      * Note: This function will always return false if the sender has the
      * readallmessages capability at the system context level.
      *
+     * @deprecated since 3.6
      * @param int $recipientid User ID of the recipient.
      * @param int $senderid User ID of the sender.
      * @return bool true if $sender is blocked, false otherwise.
      */
     public static function is_user_blocked($recipientid, $senderid = null) {
-        global $USER, $DB;
+        debugging('\core_message\api::is_user_blocked is deprecated and should not be used.',
+            DEBUG_DEVELOPER);
+
+        global $USER;
 
         if (is_null($senderid)) {
             // The message is from the logged in user, unless otherwise specified.
@@ -954,7 +986,7 @@ class api {
             return false;
         }
 
-        if ($DB->get_field('message_contacts', 'blocked', ['userid' => $recipientid, 'contactid' => $senderid])) {
+        if (self::is_blocked($recipientid, $senderid)) {
             return true;
         }
 
@@ -1079,12 +1111,11 @@ class api {
 
         $userfields = \user_picture::fields('u', array('lastaccess'));
         $blockeduserssql = "SELECT $userfields
-                              FROM {message_contacts} mc
+                              FROM {message_users_blocked} mub
                         INNER JOIN {user} u
-                                ON u.id = mc.contactid
+                                ON u.id = mub.blockeduserid
                              WHERE u.deleted = 0
-                               AND mc.userid = ?
-                               AND mc.blocked = 1
+                               AND mub.userid = ?
                           GROUP BY $userfields
                           ORDER BY u.firstname ASC";
         return $DB->get_records_sql($blockeduserssql, [$userid]);
@@ -1287,4 +1318,298 @@ class api {
 
         return $conversation->id;
     }
+
+    /**
+     * Checks if a user can create a contact request.
+     *
+     * @param int $userid The id of the user who is creating the contact request
+     * @param int $requesteduserid The id of the user being requested
+     * @return bool
+     */
+    public static function can_create_contact(int $userid, int $requesteduserid) : bool {
+        global $CFG;
+
+        // If we can't message at all, then we can't create a contact.
+        if (empty($CFG->messaging)) {
+            return false;
+        }
+
+        // If we can message anyone on the site then we can create a contact.
+        if ($CFG->messagingallusers) {
+            return true;
+        }
+
+        // We need to check if they are in the same course.
+        return enrol_sharing_course($userid, $requesteduserid);
+    }
+
+    /**
+     * Handles creating a contact request.
+     *
+     * @param int $userid The id of the user who is creating the contact request
+     * @param int $requesteduserid The id of the user being requested
+     */
+    public static function create_contact_request(int $userid, int $requesteduserid) {
+        global $DB;
+
+        $request = new \stdClass();
+        $request->userid = $userid;
+        $request->requesteduserid = $requesteduserid;
+        $request->timecreated = time();
+
+        $DB->insert_record('message_contact_requests', $request);
+
+        // Send a notification.
+        $userfrom = \core_user::get_user($userid);
+        $userfromfullname = fullname($userfrom);
+        $userto = \core_user::get_user($requesteduserid);
+        $url = new \moodle_url('/message/pendingcontactrequests.php');
+
+        $subject = get_string('messagecontactrequestsnotificationsubject', 'core_message', $userfromfullname);
+        $fullmessage = get_string('messagecontactrequestsnotification', 'core_message', $userfromfullname);
+
+        $message = new \core\message\message();
+        $message->courseid = SITEID;
+        $message->component = 'moodle';
+        $message->name = 'messagecontactrequests';
+        $message->notification = 1;
+        $message->userfrom = $userfrom;
+        $message->userto = $userto;
+        $message->subject = $subject;
+        $message->fullmessage = text_to_html($fullmessage);
+        $message->fullmessageformat = FORMAT_HTML;
+        $message->fullmessagehtml = $fullmessage;
+        $message->smallmessage = '';
+        $message->contexturl = $url->out(false);
+
+        message_send($message);
+    }
+
+
+    /**
+     * Handles confirming a contact request.
+     *
+     * @param int $userid The id of the user who created the contact request
+     * @param int $requesteduserid The id of the user confirming the request
+     */
+    public static function confirm_contact_request(int $userid, int $requesteduserid) {
+        global $DB;
+
+        if ($request = $DB->get_record('message_contact_requests', ['userid' => $userid,
+                'requesteduserid' => $requesteduserid])) {
+            self::add_contact($userid, $requesteduserid);
+
+            $DB->delete_records('message_contact_requests', ['id' => $request->id]);
+        }
+    }
+
+    /**
+     * Handles declining a contact request.
+     *
+     * @param int $userid The id of the user who created the contact request
+     * @param int $requesteduserid The id of the user declining the request
+     */
+    public static function decline_contact_request(int $userid, int $requesteduserid) {
+        global $DB;
+
+        if ($request = $DB->get_record('message_contact_requests', ['userid' => $userid,
+                'requesteduserid' => $requesteduserid])) {
+            $DB->delete_records('message_contact_requests', ['id' => $request->id]);
+        }
+    }
+
+    /**
+     * Handles returning the contact requests for a user.
+     *
+     * This also includes the user data necessary to display information
+     * about the user.
+     *
+     * It will not include blocked users.
+     *
+     * @param int $userid
+     * @return array The list of contact requests
+     */
+    public static function get_contact_requests(int $userid) : array {
+        global $DB;
+
+        // Used to search for contacts.
+        $ufields = \user_picture::fields('u');
+
+        $sql = "SELECT $ufields, mcr.id as contactrequestid
+                  FROM {user} u
+                  JOIN {message_contact_requests} mcr
+                    ON u.id = mcr.userid
+             LEFT JOIN {message_users_blocked} mub
+                    ON (mub.userid = ? AND mub.blockeduserid = u.id)
+                 WHERE mcr.requesteduserid = ?
+                   AND u.deleted = 0
+                   AND mub.id is NULL
+              ORDER BY mcr.timecreated DESC";
+
+        return $DB->get_records_sql($sql, [$userid, $userid]);
+    }
+
+    /**
+     * Handles adding a contact.
+     *
+     * @param int $userid The id of the user who requested to be a contact
+     * @param int $contactid The id of the contact
+     */
+    public static function add_contact(int $userid, int $contactid) {
+        global $DB;
+
+        $messagecontact = new \stdClass();
+        $messagecontact->userid = $userid;
+        $messagecontact->contactid = $contactid;
+        $messagecontact->timecreated = time();
+        $messagecontact->id = $DB->insert_record('message_contacts', $messagecontact);
+
+        $eventparams = [
+            'objectid' => $messagecontact->id,
+            'userid' => $userid,
+            'relateduserid' => $contactid,
+            'context' => \context_user::instance($userid)
+        ];
+        $event = \core\event\message_contact_added::create($eventparams);
+        $event->add_record_snapshot('message_contacts', $messagecontact);
+        $event->trigger();
+    }
+
+    /**
+     * Handles removing a contact.
+     *
+     * @param int $userid The id of the user who is removing a user as a contact
+     * @param int $contactid The id of the user to be removed as a contact
+     */
+    public static function remove_contact(int $userid, int $contactid) {
+        global $DB;
+
+        if ($contact = self::get_contact($userid, $contactid)) {
+            $DB->delete_records('message_contacts', ['id' => $contact->id]);
+
+            $event = \core\event\message_contact_removed::create(array(
+                'objectid' => $contact->id,
+                'userid' => $userid,
+                'relateduserid' => $contactid,
+                'context' => \context_user::instance($userid)
+            ));
+            $event->add_record_snapshot('message_contacts', $contact);
+            $event->trigger();
+        }
+    }
+
+    /**
+     * Handles blocking a user.
+     *
+     * @param int $userid The id of the user who is blocking
+     * @param int $usertoblockid The id of the user being blocked
+     */
+    public static function block_user(int $userid, int $usertoblockid) {
+        global $DB;
+
+        $blocked = new \stdClass();
+        $blocked->userid = $userid;
+        $blocked->blockeduserid = $usertoblockid;
+        $blocked->timecreated = time();
+        $blocked->id = $DB->insert_record('message_users_blocked', $blocked);
+
+        // Trigger event for blocking a contact.
+        $event = \core\event\message_user_blocked::create(array(
+            'objectid' => $blocked->id,
+            'userid' => $userid,
+            'relateduserid' => $usertoblockid,
+            'context' => \context_user::instance($userid)
+        ));
+        $event->add_record_snapshot('message_users_blocked', $blocked);
+        $event->trigger();
+    }
+
+    /**
+     * Handles unblocking a user.
+     *
+     * @param int $userid The id of the user who is unblocking
+     * @param int $usertounblockid The id of the user being unblocked
+     */
+    public static function unblock_user(int $userid, int $usertounblockid) {
+        global $DB;
+
+        if ($blockeduser = $DB->get_record('message_users_blocked',
+                ['userid' => $userid, 'blockeduserid' => $usertounblockid])) {
+            $DB->delete_records('message_users_blocked', ['id' => $blockeduser->id]);
+
+            // Trigger event for unblocking a contact.
+            $event = \core\event\message_user_unblocked::create(array(
+                'objectid' => $blockeduser->id,
+                'userid' => $userid,
+                'relateduserid' => $usertounblockid,
+                'context' => \context_user::instance($userid)
+            ));
+            $event->add_record_snapshot('message_users_blocked', $blockeduser);
+            $event->trigger();
+        }
+    }
+
+    /**
+     * Checks if users are already contacts.
+     *
+     * @param int $userid The id of one of the users
+     * @param int $contactid The id of the other user
+     * @return bool Returns true if they are a contact, false otherwise
+     */
+    public static function is_contact(int $userid, int $contactid) : bool {
+        global $DB;
+
+        $sql = "SELECT id
+                  FROM {message_contacts} mc
+                 WHERE (mc.userid = ? AND mc.contactid = ?)
+                    OR (mc.userid = ? AND mc.contactid = ?)";
+        return $DB->record_exists_sql($sql, [$userid, $contactid, $contactid, $userid]);
+    }
+
+    /**
+     * Returns the row in the database table message_contacts that represents the contact between two people.
+     *
+     * @param int $userid The id of one of the users
+     * @param int $contactid The id of the other user
+     * @return mixed A fieldset object containing the record, false otherwise
+     */
+    public static function get_contact(int $userid, int $contactid) {
+        global $DB;
+
+        $sql = "SELECT mc.*
+                  FROM {message_contacts} mc
+                 WHERE (mc.userid = ? AND mc.contactid = ?)
+                    OR (mc.userid = ? AND mc.contactid = ?)";
+        return $DB->get_record_sql($sql, [$userid, $contactid, $contactid, $userid]);
+    }
+
+    /**
+     * Checks if a user is already blocked.
+     *
+     * @param int $userid
+     * @param int $blockeduserid
+     * @return bool Returns true if they are a blocked, false otherwise
+     */
+    public static function is_blocked(int $userid, int $blockeduserid) : bool {
+        global $DB;
+
+        return $DB->record_exists('message_users_blocked', ['userid' => $userid, 'blockeduserid' => $blockeduserid]);
+    }
+
+    /**
+     * Checks if a contact request already exists between users.
+     *
+     * @param int $userid The id of the user who is creating the contact request
+     * @param int $requesteduserid The id of the user being requested
+     * @return bool Returns true if a contact request exists, false otherwise
+     */
+    public static function does_contact_request_exist(int $userid, int $requesteduserid) : bool {
+        global $DB;
+
+        $sql = "SELECT id
+                  FROM {message_contact_requests} mcr
+                 WHERE (mcr.userid = ? AND mcr.requesteduserid = ?)
+                    OR (mcr.userid = ? AND mcr.requesteduserid = ?)";
+        return $DB->record_exists_sql($sql, [$userid, $requesteduserid, $requesteduserid, $userid]);
+    }
 }
index a9b53d3..7d98e7b 100644 (file)
@@ -101,7 +101,8 @@ class messages implements templatable, renderable {
             $data->messages[] = $message->export_for_template($output);
         }
 
-        $data->isblocked = api::is_user_blocked($this->currentuserid, $this->otheruserid);
+        $blockeduserid = $this->otheruserid ?? $USER->id;
+        $data->isblocked = api::is_blocked($this->currentuserid, $blockeduserid);
 
         return $data;
     }
index 8551353..b9049e5 100644 (file)
@@ -90,11 +90,31 @@ class provider implements
             [
                 'userid' => 'privacy:metadata:message_contacts:userid',
                 'contactid' => 'privacy:metadata:message_contacts:contactid',
-                'blocked' => 'privacy:metadata:message_contacts:blocked',
+                'timecreated' => 'privacy:metadata:message_contacts:timecreated',
             ],
             'privacy:metadata:message_contacts'
         );
 
+        $items->add_database_table(
+            'message_contact_requests',
+            [
+                'userid' => 'privacy:metadata:message_contact_requests:userid',
+                'requesteduserid' => 'privacy:metadata:message_contact_requests:requesteduserid',
+                'timecreated' => 'privacy:metadata:message_contact_requests:timecreated',
+            ],
+            'privacy:metadata:message_contact_requests'
+        );
+
+        $items->add_database_table(
+            'message_users_blocked',
+            [
+                'userid' => 'privacy:metadata:message_users_blocked:userid',
+                'blockeduserid' => 'privacy:metadata:message_users_blocked:blockeduserid',
+                'timecreated' => 'privacy:metadata:message_users_blocked:timecreated',
+            ],
+            'privacy:metadata:message_users_blocked'
+        );
+
         $items->add_database_table(
             'notifications',
             [
@@ -186,6 +206,12 @@ class provider implements
         // Export the contacts.
         self::export_user_data_contacts($userid);
 
+        // Export the contact requests.
+        self::export_user_data_contact_requests($userid);
+
+        // Export the blocked users.
+        self::export_user_data_blocked_users($userid);
+
         // Export the notifications.
         self::export_user_data_notifications($userid);
 
@@ -209,6 +235,8 @@ class provider implements
         $DB->delete_records('message_user_actions');
         $DB->delete_records('message_conversation_members');
         $DB->delete_records('message_contacts');
+        $DB->delete_records('message_contact_requests');
+        $DB->delete_records('message_users_blocked');
         $DB->delete_records('notifications');
     }
 
@@ -239,6 +267,8 @@ class provider implements
         $DB->delete_records('message_user_actions', ['userid' => $userid]);
         $DB->delete_records('message_conversation_members', ['userid' => $userid]);
         $DB->delete_records_select('message_contacts', 'userid = ? OR contactid = ?', [$userid, $userid]);
+        $DB->delete_records_select('message_contact_requests', 'userid = ? OR requesteduserid = ?', [$userid, $userid]);
+        $DB->delete_records_select('message_users_blocked', 'userid = ? OR blockeduserid = ?', [$userid, $userid]);
         $DB->delete_records_select('notifications', 'useridfrom = ? OR useridto = ?', [$userid, $userid]);
     }
 
@@ -253,18 +283,70 @@ class provider implements
         $context = \context_system::instance();
 
         // Get the user's contacts.
-        if ($contacts = $DB->get_records('message_contacts', ['userid' => $userid], 'id ASC')) {
+        if ($contacts = $DB->get_records_select('message_contacts', 'userid = ? OR contactid = ?', [$userid, $userid], 'id ASC')) {
             $contactdata = [];
             foreach ($contacts as $contact) {
                 $contactdata[] = (object) [
-                    'contact' => transform::user($contact->contactid),
-                    'blocked' => transform::yesno($contact->blocked)
+                    'contact' => transform::user($contact->contactid)
                 ];
             }
             writer::with_context($context)->export_data([get_string('contacts', 'core_message')], (object) $contactdata);
         }
     }
 
+    /**
+     * Export the messaging contact requests data.
+     *
+     * @param int $userid
+     */
+    protected static function export_user_data_contact_requests(int $userid) {
+        global $DB;
+
+        $context = \context_system::instance();
+
+        if ($contactrequests = $DB->get_records_select('message_contact_requests', 'userid = ? OR requesteduserid = ?',
+                [$userid, $userid], 'id ASC')) {
+            $contactrequestsdata = [];
+            foreach ($contactrequests as $contactrequest) {
+                if ($userid == $contactrequest->requesteduserid) {
+                    $maderequest = false;
+                    $contactid = $contactrequest->userid;
+                } else {
+                    $maderequest = true;
+                    $contactid = $contactrequest->requesteduserid;
+                }
+
+                $contactrequestsdata[] = (object) [
+                    'contactrequest' => transform::user($contactid),
+                    'maderequest' => transform::yesno($maderequest)
+                ];
+            }
+            writer::with_context($context)->export_data([get_string('contactrequests', 'core_message')],
+                (object) $contactrequestsdata);
+        }
+    }
+
+    /**
+     * Export the messaging blocked users data.
+     *
+     * @param int $userid
+     */
+    protected static function export_user_data_blocked_users(int $userid) {
+        global $DB;
+
+        $context = \context_system::instance();
+
+        if ($blockedusers = $DB->get_records('message_users_blocked', ['userid' => $userid], 'id ASC')) {
+            $blockedusersdata = [];
+            foreach ($blockedusers as $blockeduser) {
+                $blockedusersdata[] = (object) [
+                    'blockeduser' => transform::user($blockeduser->blockeduserid)
+                ];
+            }
+            writer::with_context($context)->export_data([get_string('blockedusers', 'core_message')], (object) $blockedusersdata);
+        }
+    }
+
     /**
      * Export the messaging data.
      *
index 5151adb..f678c2f 100644 (file)
@@ -90,23 +90,30 @@ class core_message_external extends external_api {
         foreach($params['messages'] as $message) {
             $receivers[] = $message['touserid'];
         }
-        list($sqluserids, $sqlparams) = $DB->get_in_or_equal($receivers, SQL_PARAMS_NAMED, 'userid_');
+        list($sqluserids, $sqlparams) = $DB->get_in_or_equal($receivers);
         $tousers = $DB->get_records_select("user", "id " . $sqluserids . " AND deleted = 0", $sqlparams);
         $blocklist   = array();
         $contactlist = array();
-        $sqlparams['contactid'] = $USER->id;
+        $contactsqlparams = array_merge($sqlparams, [$USER->id], [$USER->id], $sqlparams);
         $rs = $DB->get_recordset_sql("SELECT *
                                         FROM {message_contacts}
-                                       WHERE userid $sqluserids
-                                             AND contactid = :contactid", $sqlparams);
+                                       WHERE (userid $sqluserids AND contactid = ?)
+                                          OR (userid = ? AND contactid $sqluserids)", $contactsqlparams);
         foreach ($rs as $record) {
-            if ($record->blocked) {
-                // $record->userid is blocking current user
-                $blocklist[$record->userid] = true;
-            } else {
-                // $record->userid have current user as contact
-                $contactlist[$record->userid] = true;
+            $useridtouse = $record->userid;
+            if ($record->userid == $USER->id) {
+                $useridtouse = $record->contactid;
             }
+            $contactlist[$useridtouse] = true;
+        }
+        $rs->close();
+        $blocksqlparams = array_merge($sqlparams, [$USER->id]);
+        $rs = $DB->get_recordset_sql("SELECT *
+                                        FROM {message_users_blocked}
+                                       WHERE userid $sqluserids
+                                         AND blockeduserid = ?", $blocksqlparams);
+        foreach ($rs as $record) {
+            $blocklist[$record->userid] = true;
         }
         $rs->close();
 
@@ -191,6 +198,7 @@ class core_message_external extends external_api {
     /**
      * Create contacts parameters description.
      *
+     * @deprecated since Moodle 3.6
      * @return external_function_parameters
      * @since Moodle 2.5
      */
@@ -210,6 +218,7 @@ class core_message_external extends external_api {
     /**
      * Create contacts.
      *
+     * @deprecated since Moodle 3.6
      * @param array $userids array of user IDs.
      * @param int $userid The id of the user we are creating the contacts for
      * @return external_description
@@ -256,6 +265,7 @@ class core_message_external extends external_api {
     /**
      * Create contacts return description.
      *
+     * @deprecated since Moodle 3.6
      * @return external_description
      * @since Moodle 2.5
      */
@@ -263,6 +273,15 @@ class core_message_external extends external_api {
         return new external_warnings();
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function create_contacts_is_deprecated() {
+        return true;
+    }
+
     /**
      * Delete contacts parameters description.
      *
@@ -315,7 +334,7 @@ class core_message_external extends external_api {
         $params = self::validate_parameters(self::delete_contacts_parameters(), $params);
 
         foreach ($params['userids'] as $id) {
-            message_remove_contact($id, $userid);
+            \core_message\api::remove_contact($userid, $id);
         }
 
         return null;
@@ -331,9 +350,121 @@ class core_message_external extends external_api {
         return null;
     }
 
+    /**
+     * Block user parameters description.
+     *
+     * @return external_function_parameters
+     */
+    public static function block_user_parameters() {
+        return new external_function_parameters(
+            [
+                'userid' => new external_value(PARAM_INT, 'The id of the user who is blocking'),
+                'blockeduserid' => new external_value(PARAM_INT, 'The id of the user being blocked'),
+            ]
+        );
+    }
+
+    /**
+     * Blocks a user.
+     *
+     * @param int $userid The id of the user who is blocking
+     * @param int $blockeduserid The id of the user being blocked
+     * @return external_description
+     */
+    public static function block_user(int $userid, int $blockeduserid) {
+        global $CFG, $USER;
+
+        // Check if messaging is enabled.
+        if (empty($CFG->messaging)) {
+            throw new moodle_exception('disabled', 'message');
+        }
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $capability = 'moodle/site:manageallmessaging';
+        if (($USER->id != $userid) && !has_capability($capability, $context)) {
+            throw new required_capability_exception($context, $capability, 'nopermissions', '');
+        }
+
+        $params = ['userid' => $userid, 'blockeduserid' => $blockeduserid];
+        $params = self::validate_parameters(self::block_user_parameters(), $params);
+
+        if (!\core_message\api::is_blocked($params['userid'], $params['blockeduserid'])) {
+            \core_message\api::block_user($params['userid'], $params['blockeduserid']);
+        }
+
+        return [];
+    }
+
+    /**
+     * Block user return description.
+     *
+     * @return external_description
+     */
+    public static function block_user_returns() {
+        return new external_warnings();
+    }
+
+    /**
+     * Unblock user parameters description.
+     *
+     * @return external_function_parameters
+     */
+    public static function unblock_user_parameters() {
+        return new external_function_parameters(
+            [
+                'userid' => new external_value(PARAM_INT, 'The id of the user who is unblocking'),
+                'unblockeduserid' => new external_value(PARAM_INT, 'The id of the user being unblocked'),
+            ]
+        );
+    }
+
+    /**
+     * Unblock user.
+     *
+     * @param int $userid The id of the user who is unblocking
+     * @param int $unblockeduserid The id of the user being unblocked
+     */
+    public static function unblock_user(int $userid, int $unblockeduserid) {
+        global $CFG, $USER;
+
+        // Check if messaging is enabled.
+        if (empty($CFG->messaging)) {
+            throw new moodle_exception('disabled', 'message');
+        }
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $capability = 'moodle/site:manageallmessaging';
+        if (($USER->id != $userid) && !has_capability($capability, $context)) {
+            throw new required_capability_exception($context, $capability, 'nopermissions', '');
+        }
+
+        $params = ['userid' => $userid, 'unblockeduserid' => $unblockeduserid];
+        $params = self::validate_parameters(self::unblock_user_parameters(), $params);
+
+        \core_message\api::unblock_user($params['userid'], $params['unblockeduserid']);
+
+        return [];
+    }
+
+    /**
+     * Unblock user return description.
+     *
+     * @return external_description
+     */
+    public static function unblock_user_returns() {
+        return new external_warnings();
+    }
+
     /**
      * Block contacts parameters description.
      *
+     * @deprecated since Moodle 3.6
      * @return external_function_parameters
      * @since Moodle 2.5
      */
@@ -353,6 +484,7 @@ class core_message_external extends external_api {
     /**
      * Block contacts.
      *
+     * @deprecated since Moodle 3.6
      * @param array $userids array of user IDs.
      * @param int $userid The id of the user we are blocking the contacts for
      * @return external_description
@@ -399,6 +531,7 @@ class core_message_external extends external_api {
     /**
      * Block contacts return description.
      *
+     * @deprecated since Moodle 3.6
      * @return external_description
      * @since Moodle 2.5
      */
@@ -406,9 +539,19 @@ class core_message_external extends external_api {
         return new external_warnings();
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function block_contacts_is_deprecated() {
+        return true;
+    }
+
     /**
      * Unblock contacts parameters description.
      *
+     * @deprecated since Moodle 3.6
      * @return external_function_parameters
      * @since Moodle 2.5
      */
@@ -428,6 +571,7 @@ class core_message_external extends external_api {
     /**
      * Unblock contacts.
      *
+     * @deprecated since Moodle 3.6
      * @param array $userids array of user IDs.
      * @param int $userid The id of the user we are unblocking the contacts for
      * @return null
@@ -467,6 +611,7 @@ class core_message_external extends external_api {
     /**
      * Unblock contacts return description.
      *
+     * @deprecated since Moodle 3.6
      * @return external_description
      * @since Moodle 2.5
      */
@@ -474,6 +619,265 @@ class core_message_external extends external_api {
         return null;
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function unblock_contacts_is_deprecated() {
+        return true;
+    }
+
+    /**
+     * Returns contact requests parameters description.
+     *
+     * @return external_function_parameters
+     */
+    public static function get_contact_requests_parameters() {
+        return new external_function_parameters(
+            [
+                'userid' => new external_value(PARAM_INT, 'The id of the user we want the requests for')
+            ]
+        );
+    }
+
+    /**
+     * Handles returning the contact requests for a user.
+     *
+     * This also includes the user data necessary to display information
+     * about the user.
+     *
+     * It will not include blocked users.
+     *
+     * @param int $userid The id of the user we want to get the contact requests for
+     */
+    public static function get_contact_requests(int $userid) {
+        global $CFG, $USER;
+
+        // Check if messaging is enabled.
+        if (empty($CFG->messaging)) {
+            throw new moodle_exception('disabled', 'message');
+        }
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $capability = 'moodle/site:manageallmessaging';
+        if (($USER->id != $userid) && !has_capability($capability, $context)) {
+            throw new required_capability_exception($context, $capability, 'nopermissions', '');
+        }
+
+        $params = ['userid' => $userid];
+        $params = self::validate_parameters(self::get_contact_requests_parameters(), $params);
+
+        return \core_message\api::get_contact_requests($params['userid']);
+    }
+
+    /**
+     * Returns the contact requests return description.
+     *
+     * @return external_description
+     */
+    public static function get_contact_requests_returns() {
+        return new external_multiple_structure(
+            new external_single_structure(
+                [
+                    'id' => new external_value(core_user::get_property_type('id'), 'ID of the user'),
+                    'contactrequestid' => new external_value(PARAM_INT, 'The ID of the contact request'),
+                    'picture' => new external_value(core_user::get_property_type('picture'), 'The picture'),
+                    'firstname' => new external_value(core_user::get_property_type('firstname'),
+                        'The first name(s) of the user'),
+                    'lastname' => new external_value(core_user::get_property_type('lastname'),
+                        'The family name of the user'),
+                    'firstnamephonetic' => new external_value(core_user::get_property_type('firstnamephonetic'),
+                        'The phonetic first name of the user'),
+                    'lastnamephonetic' => new external_value(core_user::get_property_type('lastnamephonetic'),
+                        'The phonetic last name of the user'),
+                    'middlename' => new external_value(core_user::get_property_type('middlename'),
+                        'The middle name of the user'),
+                    'alternatename' => new external_value(core_user::get_property_type('alternatename'),
+                        'The alternate name of the user'),
+                    'email' => new external_value(core_user::get_property_type('email'), 'An email address')
+                ]
+            )
+        );
+    }
+
+    /**
+     * Creates a contact request parameters description.
+     *
+     * @return external_function_parameters
+     */
+    public static function create_contact_request_parameters() {
+        return new external_function_parameters(
+            [
+                'userid' => new external_value(PARAM_INT, 'The id of the user making the request'),
+                'requesteduserid' => new external_value(PARAM_INT, 'The id of the user being requested')
+            ]
+        );
+    }
+
+    /**
+     * Creates a contact request.
+     *
+     * @param int $userid The id of the user who is creating the contact request
+     * @param int $requesteduserid The id of the user being requested
+     */
+    public static function create_contact_request(int $userid, int $requesteduserid) {
+        global $CFG, $USER;
+
+        // Check if messaging is enabled.
+        if (empty($CFG->messaging)) {
+            throw new moodle_exception('disabled', 'message');
+        }
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $capability = 'moodle/site:manageallmessaging';
+        if (($USER->id != $userid) && !has_capability($capability, $context)) {
+            throw new required_capability_exception($context, $capability, 'nopermissions', '');
+        }
+
+        $params = ['userid' => $userid, 'requesteduserid' => $requesteduserid];
+        $params = self::validate_parameters(self::create_contact_request_parameters(), $params);
+
+        if (!\core_message\api::can_create_contact($params['userid'], $params['requesteduserid'])) {
+            $warning[] = [
+                'item' => 'user',
+                'itemid' => $params['requesteduserid'],
+                'warningcode' => 'cannotcreatecontactrequest',
+                'message' => 'You are unable to create a contact request for this user'
+            ];
+            return $warning;
+        }
+
+        if (!\core_message\api::does_contact_request_exist($params['userid'], $params['requesteduserid'])) {
+            \core_message\api::create_contact_request($params['userid'], $params['requesteduserid']);
+        }
+
+        return [];
+    }
+
+    /**
+     * Creates a contact request return description.
+     *
+     * @return external_description
+     */
+    public static function create_contact_request_returns() {
+        return new external_warnings();
+    }
+
+    /**
+     * Confirm a contact request parameters description.
+     *
+     * @return external_function_parameters
+     */
+    public static function confirm_contact_request_parameters() {
+        return new external_function_parameters(
+            [
+                'userid' => new external_value(PARAM_INT, 'The id of the user making the request'),
+                'requesteduserid' => new external_value(PARAM_INT, 'The id of the user being requested')
+            ]
+        );
+    }
+
+    /**
+     * Confirm a contact request.
+     *
+     * @param int $userid The id of the user who is creating the contact request
+     * @param int $requesteduserid The id of the user being requested
+     */
+    public static function confirm_contact_request(int $userid, int $requesteduserid) {
+        global $CFG, $USER;
+
+        // Check if messaging is enabled.
+        if (empty($CFG->messaging)) {
+            throw new moodle_exception('disabled', 'message');
+        }
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $capability = 'moodle/site:manageallmessaging';
+        if (($USER->id != $requesteduserid) && !has_capability($capability, $context)) {
+            throw new required_capability_exception($context, $capability, 'nopermissions', '');
+        }
+
+        $params = ['userid' => $userid, 'requesteduserid' => $requesteduserid];
+        $params = self::validate_parameters(self::confirm_contact_request_parameters(), $params);
+
+        \core_message\api::confirm_contact_request($params['userid'], $params['requesteduserid']);
+
+        return [];
+    }
+
+    /**
+     * Confirm a contact request return description.
+     *
+     * @return external_description
+     */
+    public static function confirm_contact_request_returns() {
+        return new external_warnings();
+    }
+
+    /**
+     * Declines a contact request parameters description.
+     *
+     * @return external_function_parameters
+     */
+    public static function decline_contact_request_parameters() {
+        return new external_function_parameters(
+            [
+                'userid' => new external_value(PARAM_INT, 'The id of the user making the request'),
+                'requesteduserid' => new external_value(PARAM_INT, 'The id of the user being requested')
+            ]
+        );
+    }
+
+    /**
+     * Declines a contact request.
+     *
+     * @param int $userid The id of the user who is creating the contact request
+     * @param int $requesteduserid The id of the user being requested
+     */
+    public static function decline_contact_request(int $userid, int $requesteduserid) {
+        global $CFG, $USER;
+
+        // Check if messaging is enabled.
+        if (empty($CFG->messaging)) {
+            throw new moodle_exception('disabled', 'message');
+        }
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $capability = 'moodle/site:manageallmessaging';
+        if (($USER->id != $requesteduserid) && !has_capability($capability, $context)) {
+            throw new required_capability_exception($context, $capability, 'nopermissions', '');
+        }
+
+        $params = ['userid' => $userid, 'requesteduserid' => $requesteduserid];
+        $params = self::validate_parameters(self::decline_contact_request_parameters(), $params);
+
+        \core_message\api::decline_contact_request($params['userid'], $params['requesteduserid']);
+
+        return [];
+    }
+
+    /**
+     * Declines a contact request return description.
+     *
+     * @return external_description
+     */
+    public static function decline_contact_request_returns() {
+        return new external_warnings();
+    }
+
     /**
      * Return the structure of a message area contact.
      *
index a3f99fc..1fbe61c 100644 (file)
@@ -151,156 +151,6 @@ function message_format_message_text($message, $forcetexttohtml = false) {
     return format_text($messagetext, $format, $options);
 }
 
-/**
- * Add the selected user as a contact for the current user
- *
- * @param int $contactid the ID of the user to add as a contact
- * @param int $blocked 1 if you wish to block the contact
- * @param int $userid the user ID of the user we want to add the contact for, defaults to current user if not specified.
- * @return bool/int false if the $contactid isnt a valid user id. True if no changes made.
- *                  Otherwise returns the result of update_record() or insert_record()
- */
-function message_add_contact($contactid, $blocked = 0, $userid = 0) {
-    global $USER, $DB;
-
-    if (!$DB->record_exists('user', array('id' => $contactid))) { // invalid userid
-        return false;
-    }
-
-    if (empty($userid)) {
-        $userid = $USER->id;
-    }
-
-    // Check if a record already exists as we may be changing blocking status.
-    if (($contact = $DB->get_record('message_contacts', array('userid' => $userid, 'contactid' => $contactid))) !== false) {
-        // Check if blocking status has been changed.
-        if ($contact->blocked != $blocked) {
-            $contact->blocked = $blocked;
-            $DB->update_record('message_contacts', $contact);
-
-            if ($blocked == 1) {
-                // Trigger event for blocking a contact.
-                $event = \core\event\message_contact_blocked::create(array(
-                    'objectid' => $contact->id,
-                    'userid' => $contact->userid,
-                    'relateduserid' => $contact->contactid,
-                    'context'  => context_user::instance($contact->userid)
-                ));
-                $event->add_record_snapshot('message_contacts', $contact);
-                $event->trigger();
-            } else {
-                // Trigger event for unblocking a contact.
-                $event = \core\event\message_contact_unblocked::create(array(
-                    'objectid' => $contact->id,
-                    'userid' => $contact->userid,
-                    'relateduserid' => $contact->contactid,
-                    'context'  => context_user::instance($contact->userid)
-                ));
-                $event->add_record_snapshot('message_contacts', $contact);
-                $event->trigger();
-            }
-
-            return true;
-        } else {
-            // No change to blocking status.
-            return true;
-        }
-
-    } else {
-        // New contact record.
-        $contact = new stdClass();
-        $contact->userid = $userid;
-        $contact->contactid = $contactid;
-        $contact->blocked = $blocked;
-        $contact->id = $DB->insert_record('message_contacts', $contact);
-
-        $eventparams = array(
-            'objectid' => $contact->id,
-            'userid' => $contact->userid,
-            'relateduserid' => $contact->contactid,
-            'context'  => context_user::instance($contact->userid)
-        );
-
-        if ($blocked) {
-            $event = \core\event\message_contact_blocked::create($eventparams);
-        } else {
-            $event = \core\event\message_contact_added::create($eventparams);
-        }
-        // Trigger event.
-        $event->trigger();
-
-        return true;
-    }
-}
-
-/**
- * remove a contact
- *
- * @param int $contactid the user ID of the contact to remove
- * @param int $userid the user ID of the user we want to remove the contacts for, defaults to current user if not specified.
- * @return bool returns the result of delete_records()
- */
-function message_remove_contact($contactid, $userid = 0) {
-    global $USER, $DB;
-
-    if (empty($userid)) {
-        $userid = $USER->id;
-    }
-
-    if ($contact = $DB->get_record('message_contacts', array('userid' => $userid, 'contactid' => $contactid))) {
-        $DB->delete_records('message_contacts', array('id' => $contact->id));
-
-        // Trigger event for removing a contact.
-        $event = \core\event\message_contact_removed::create(array(
-            'objectid' => $contact->id,
-            'userid' => $contact->userid,
-            'relateduserid' => $contact->contactid,
-            'context'  => context_user::instance($contact->userid)
-        ));
-        $event->add_record_snapshot('message_contacts', $contact);
-        $event->trigger();
-
-        return true;
-    }
-
-    return false;
-}
-
-/**
- * Unblock a contact. Note that this reverts the previously blocked user back to a non-contact.
- *
- * @param int $contactid the user ID of the contact to unblock
- * @param int $userid the user ID of the user we want to unblock the contact for, defaults to current user
- *  if not specified.
- * @return bool returns the result of delete_records()
- */
-function message_unblock_contact($contactid, $userid = 0) {
-    return message_add_contact($contactid, 0, $userid);
-}
-
-/**
- * Block a user.
- *
- * @param int $contactid the user ID of the user to block
- * @param int $userid the user ID of the user we want to unblock the contact for, defaults to current user
- *  if not specified.
- * @return bool
- */
-function message_block_contact($contactid, $userid = 0) {
-    return message_add_contact($contactid, 1, $userid);
-}
-
-/**
- * Load a user's contact record
- *
- * @param int $contactid the user ID of the user whose contact record you want
- * @return array message contacts
- */
-function message_get_contact($contactid) {
-    global $USER, $DB;
-    return $DB->get_record('message_contacts', array('userid' => $USER->id, 'contactid' => $contactid));
-}
-
 /**
  * Search through course users.
  *
@@ -337,6 +187,7 @@ function message_search_users($courseids, $searchtext, $sort='', $exceptions='')
 
     $params = array(
         'userid' => $USER->id,
+        'userid2' => $USER->id,
         'query' => "%$searchtext%"
     );
 
@@ -357,10 +208,12 @@ function message_search_users($courseids, $searchtext, $sort='', $exceptions='')
 
     if (in_array(SITEID, $courseids)) {
         // Search on site level.
-        return $DB->get_records_sql("SELECT $ufields, mc.id as contactlistid, mc.blocked
+        return $DB->get_records_sql("SELECT $ufields, mc.id as contactlistid, mub.id as userblockedid
                                        FROM {user} u
                                        LEFT JOIN {message_contacts} mc
                                             ON mc.contactid = u.id AND mc.userid = :userid
+                                       LEFT JOIN {message_users_blocked} mub
+                                            ON mub.userid = :userid2 AND mub.blockeduserid = u.id
                                       WHERE u.deleted = '0' AND u.confirmed = '1'
                                             AND (".$DB->sql_like($fullname, ':query', false).")
                                             $except
@@ -379,11 +232,13 @@ function message_search_users($courseids, $searchtext, $sort='', $exceptions='')
 
         // Everyone who has a role assignment in this course or higher.
         // TODO: add enabled enrolment join here (skodak)
-        $users = $DB->get_records_sql("SELECT DISTINCT $ufields, mc.id as contactlistid, mc.blocked
+        $users = $DB->get_records_sql("SELECT DISTINCT $ufields, mc.id as contactlistid, mub.id as userblockedid
                                          FROM {user} u
                                          JOIN {role_assignments} ra ON ra.userid = u.id
                                          LEFT JOIN {message_contacts} mc
                                               ON mc.contactid = u.id AND mc.userid = :userid
+                                         LEFT JOIN {message_users_blocked} mub
+                                              ON mub.userid = :userid2 AND mub.blockeduserid = u.id
                                         WHERE u.deleted = '0' AND u.confirmed = '1'
                                               AND (".$DB->sql_like($fullname, ':query', false).")
                                               AND ra.contextid $contextwhere
diff --git a/message/pendingcontactrequests.php b/message/pendingcontactrequests.php
new file mode 100644 (file)
index 0000000..cab15e9
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * A page displaying the user's contact requests.
+ *
+ * This is a temporary (well, should be) page until the new UI is introduced for 3.6.
+ *
+ * @package    core_message
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once('../config.php');
+require_once($CFG->dirroot . '/message/externallib.php');
+
+require_login(null, false);
+
+if (isguestuser()) {
+    redirect($CFG->wwwroot);
+}
+
+if (empty($CFG->messaging)) {
+    print_error('disabled', 'message');
+}
+
+$id = optional_param('id', '', PARAM_INT); // The id of the request.
+$action = optional_param('action', '', PARAM_ALPHA);
+
+// Confirm the request is able to be approved/disapproved.
+if ($id) {
+    $request = $DB->get_record('message_contact_requests', ['id' => $id, 'requesteduserid' => $USER->id], '*', MUST_EXIST);
+}
+
+// Use external functions as these are what we will be using in the new UI.
+if ($id && $action && confirm_sesskey()) {
+    if ($action == 'approve') {
+        core_message_external::confirm_contact_request($request->userid, $USER->id);
+    } else if ($action == 'decline') {
+        core_message_external::decline_contact_request($request->userid, $USER->id);
+    }
+
+    redirect(new moodle_url('/message/pendingcontactrequests.php'));
+}
+
+$table = new html_table();
+
+$headers = [];
+$headers[] = '';
+$headers[] = '';
+
+$table->head = $headers;
+
+// Use external functions as these are what we will be using in the new UI.
+if ($contactrequests = core_message_external::get_contact_requests($USER->id)) {
+    foreach ($contactrequests as $contactrequest) {
+        $approvelink = new moodle_url('/message/pendingcontactrequests.php', ['id' => $contactrequest->contactrequestid,
+            'action' => 'approve', 'sesskey' => sesskey()]);
+        $declinelink = new moodle_url('/message/pendingcontactrequests.php', ['id' => $contactrequest->contactrequestid,
+            'action' => 'decline', 'sesskey' => sesskey()]);
+
+        $cells = array();
+        $cells[] = fullname($contactrequest);
+        $cells[] = html_writer::link($approvelink, get_string('approve')) . " | " .
+            html_writer::link($declinelink, get_string('cancel'));
+        $table->data[] = new html_table_row($cells);
+    }
+}
+
+$url = new moodle_url('/message/pendingcontactrequests.php');
+$PAGE->set_url($url);
+
+$PAGE->set_context(context_user::instance($USER->id));
+$PAGE->set_pagelayout('standard');
+$PAGE->set_title('Pending contact requests');
+$PAGE->set_heading('Pending contact requests');
+
+echo $OUTPUT->header();
+echo html_writer::table($table);
+echo $OUTPUT->footer();
index 42f8192..0278d67 100644 (file)
@@ -102,20 +102,20 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
      * Test count_blocked_users.
      */
     public function test_count_blocked_users() {
+        global $USER;
+
         // Set this user as the admin.
         $this->setAdminUser();
 
-        // Create users to add to the admin's contact list.
+        // Create user to add to the admin's block list.
         $user1 = $this->getDataGenerator()->create_user();
-        $user2 = $this->getDataGenerator()->create_user();
 
         $this->assertEquals(0, \core_message\api::count_blocked_users());
 
-        // Add 1 blocked and 1 normal contact to admin's contact list.
-        message_add_contact($user1->id);
-        message_add_contact($user2->id, 1);
+        // Add 1 blocked user to admin's blocked user list.
+        \core_message\api::block_user($USER->id, $user1->id);
 
-        $this->assertEquals(0, \core_message\api::count_blocked_users($user2));
+        $this->assertEquals(0, \core_message\api::count_blocked_users($user1));
         $this->assertEquals(1, \core_message\api::count_blocked_users());
     }
 
@@ -140,7 +140,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $user2 = self::getDataGenerator()->create_user($user2);
 
         // Block the second user.
-        message_block_contact($user2->id, $user1->id);
+        \core_message\api::block_user($user1->id, $user2->id);
 
         $user3 = new stdClass();
         $user3->firstname = 'User';
@@ -247,9 +247,9 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->getDataGenerator()->enrol_user($user1->id, $course5->id, $role->id);
 
         // Add some users as contacts.
-        message_add_contact($user2->id, 0, $user1->id);
-        message_add_contact($user3->id, 0, $user1->id);
-        message_add_contact($user4->id, 0, $user1->id);
+        \core_message\api::add_contact($user1->id, $user2->id);
+        \core_message\api::add_contact($user1->id, $user3->id);
+        \core_message\api::add_contact($user1->id, $user4->id);
 
         // Remove the viewparticipants capability from one of the courses.
         $course5context = context_course::instance($course5->id);
@@ -280,25 +280,31 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         // Create some users.
         $user1 = self::getDataGenerator()->create_user();
         $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
 
         // The person doing the search.
         $this->setUser($user1);
 
         // Send some messages back and forth.
         $time = 1;
-        $this->send_fake_message($user1, $user2, 'Yo!', 0, $time);
-        $this->send_fake_message($user2, $user1, 'Sup mang?', 0, $time + 1);
-        $this->send_fake_message($user1, $user2, 'Writing PHPUnit tests!', 0, $time + 2);
-        $this->send_fake_message($user2, $user1, 'Word.', 0, $time + 3);
+        $this->send_fake_message($user3, $user1, 'Don\'t block me.', 0, $time);
+        $this->send_fake_message($user1, $user2, 'Yo!', 0, $time + 1);
+        $this->send_fake_message($user2, $user1, 'Sup mang?', 0, $time + 2);
+        $this->send_fake_message($user1, $user2, 'Writing PHPUnit tests!', 0, $time + 3);
+        $this->send_fake_message($user2, $user1, 'Word.', 0, $time + 4);
+
+        // Block user 3.
+        \core_message\api::block_user($user1->id, $user3->id);
 
         // Perform a search.
         $messages = \core_message\api::search_messages($user1->id, 'o');
 
         // Confirm the data is correct.
-        $this->assertEquals(2, count($messages));
+        $this->assertEquals(3, count($messages));
 
         $message1 = $messages[0];
         $message2 = $messages[1];
+        $message3 = $messages[2];
 
         $this->assertEquals($user2->id, $message1->userid);
         $this->assertEquals($user2->id, $message1->useridfrom);
@@ -321,6 +327,17 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertTrue($message2->isread);
         $this->assertFalse($message2->isblocked);
         $this->assertNull($message2->unreadcount);
+
+        $this->assertEquals($user3->id, $message3->userid);
+        $this->assertEquals($user3->id, $message3->useridfrom);
+        $this->assertEquals(fullname($user3), $message3->fullname);
+        $this->assertTrue($message3->ismessaging);
+        $this->assertEquals('Don\'t block me.', $message3->lastmessage);
+        $this->assertNotEmpty($message3->messageid);
+        $this->assertNull($message3->isonline);
+        $this->assertFalse($message3->isread);
+        $this->assertTrue($message3->isblocked);
+        $this->assertNull($message3->unreadcount);
     }
 
     /**
@@ -939,15 +956,16 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $user5 = self::getDataGenerator()->create_user($user5);
 
         // Add some users as contacts.
-        message_add_contact($user2->id, 0, $user1->id);
-        message_add_contact($user3->id, 0, $user1->id);
-        message_add_contact($user4->id, 0, $user1->id);
+        \core_message\api::add_contact($user1->id, $user2->id);
+        \core_message\api::add_contact($user1->id, $user3->id);
+        \core_message\api::add_contact($user1->id, $user4->id);
 
         // Retrieve the contacts.
         $contacts = \core_message\api::get_contacts($user1->id);
 
         // Confirm the data is correct.
         $this->assertEquals(3, count($contacts));
+        usort($contacts, ['static', 'sort_contacts']);
 
         $contact1 = $contacts[0];
         $contact2 = $contacts[1];
@@ -1315,7 +1333,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->setUser($user1);
 
         // Block the second user.
-        message_block_contact($user2->id);
+        \core_message\api::block_user($user1->id, $user2->id);
 
         // Check that the second user can no longer send the first user a message.
         $this->assertFalse(\core_message\api::can_post_message($user1, $user2));
@@ -1343,10 +1361,17 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertTrue(\core_message\api::is_user_non_contact_blocked($user2));
 
         // Add the first user as a contact for the second user.
-        message_add_contact($user1->id, 0, $user2->id);
+        \core_message\api::add_contact($user2->id, $user1->id);
 
         // Check that the return result is now false.
         $this->assertFalse(\core_message\api::is_user_non_contact_blocked($user2));
+
+        // Set the first user's preference to not receive messages from non-contacts.
+        set_user_preference('message_blocknoncontacts', 1, $user1->id);
+        $this->setUser($user2);
+        // Confirm it is still false. We want to ensure a contact request works both ways
+        // as it is now an agreement between users.
+        $this->assertFalse(\core_message\api::is_user_non_contact_blocked($user1));
     }
 
     /**
@@ -1363,16 +1388,19 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
 
         // User shouldn't be blocked.
         $this->assertFalse(\core_message\api::is_user_blocked($user1->id, $user2->id));
+        $this->assertDebuggingCalled();
 
         // Block the user.
-        message_block_contact($user2->id);
+        \core_message\api::block_user($user1->id, $user2->id);
 
         // User should be blocked.
         $this->assertTrue(\core_message\api::is_user_blocked($user1->id, $user2->id));
+        $this->assertDebuggingCalled();
 
         // Unblock the user.
-        message_unblock_contact($user2->id);
+        \core_message\api::unblock_user($user1->id, $user2->id);
         $this->assertFalse(\core_message\api::is_user_blocked($user1->id, $user2->id));
+        $this->assertDebuggingCalled();
     }
 
     /**
@@ -1386,13 +1414,14 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->setUser($user1);
 
         // Block the admin user.
-        message_block_contact(2);
+        \core_message\api::block_user($user1->id, 2);
 
         // Now change to the admin user.
         $this->setAdminUser();
 
         // As the admin you should still be able to send messages to the user.
         $this->assertFalse(\core_message\api::is_user_blocked($user1->id));
+        $this->assertDebuggingCalled();
     }
 
     /*
@@ -1623,13 +1652,12 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $user2 = $this->getDataGenerator()->create_user();
 
         // Add users to the admin's contact list.
-        message_add_contact($user1->id);
-        message_add_contact($user2->id, 1);
+        \core_message\api::block_user($USER->id, $user2->id);
 
         $this->assertCount(1, \core_message\api::get_blocked_users($USER->id));
 
         // Block other user.
-        message_block_contact($user1->id);
+        \core_message\api::block_user($USER->id, $user1->id);
         $this->assertCount(2, \core_message\api::get_blocked_users($USER->id));
 
         // Test deleting users.
@@ -1649,9 +1677,8 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $user4 = self::getDataGenerator()->create_user();
 
         // Add the users to each of their contacts.
-        message_add_contact($user1->id, 0, $user2->id);
-        message_add_contact($user2->id, 0, $user1->id);
-        message_add_contact($user3->id, 0, $user2->id);
+        \core_message\api::add_contact($user1->id, $user2->id);
+        \core_message\api::add_contact($user2->id, $user3->id);
 
         $this->send_fake_message($user1, $user2);
         $this->send_fake_message($user1, $user2);
@@ -1669,13 +1696,13 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
 
         // Get the contacts and the unread message count.
         $messages = \core_message\api::get_contacts_with_unread_message_count($user2->id);
-
         // Confirm the size is correct.
         $this->assertCount(2, $messages);
         ksort($messages);
 
         $messageinfo1 = array_shift($messages);
         $messageinfo2 = array_shift($messages);
+
         $this->assertEquals($user1->id, $messageinfo1->id);
         $this->assertEquals(4, $messageinfo1->messagecount);
         $this->assertEquals($user3->id, $messageinfo2->id);
@@ -1742,7 +1769,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $user2 = self::getDataGenerator()->create_user();
 
         // Add the users to each of their contacts.
-        message_add_contact($user1->id, 0, $user2->id);
+        \core_message\api::add_contact($user2->id, $user1->id);
 
         // Check we get the correct message count.
         $messages = \core_message\api::get_contacts_with_unread_message_count($user2->id);
@@ -1768,8 +1795,8 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $user4 = self::getDataGenerator()->create_user();
 
         // Add a user to the contact list of the users we are testing this function with.
-        message_add_contact($user4->id, 0, $user1->id);
-        message_add_contact($user4->id, 0, $user2->id);
+        \core_message\api::add_contact($user1->id, $user4->id);
+        \core_message\api::add_contact($user2->id, $user4->id);
 
         $this->send_fake_message($user1, $user2);
         $this->send_fake_message($user1, $user2);
@@ -1939,4 +1966,274 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertEquals($conversationid,
             \core_message\api::get_conversation_between_users([$user1->id, $user2->id]));
     }
+
+    /**
+     * Test can create a contact request.
+     */
+    public function test_can_create_contact_request() {
+        global $CFG;
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        // Disable messaging.
+        $CFG->messaging = 0;
+        $this->assertFalse(\core_message\api::can_create_contact($user1->id, $user2->id));
+
+        // Re-enable messaging.
+        $CFG->messaging = 1;
+
+        // Allow users to message anyone site-wide.
+        $CFG->messagingallusers = 1;
+        $this->assertTrue(\core_message\api::can_create_contact($user1->id, $user2->id));
+
+        // Disallow users from messaging anyone site-wide.
+        $CFG->messagingallusers = 0;
+        $this->assertFalse(\core_message\api::can_create_contact($user1->id, $user2->id));
+
+        // Put the users in the same course so a contact request should be possible.
+        $course = self::getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id);
+        $this->assertTrue(\core_message\api::can_create_contact($user1->id, $user2->id));
+    }
+
+    /**
+     * Test creating a contact request.
+     */
+    public function test_create_contact_request() {
+        global $DB;
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        \core_message\api::create_contact_request($user1->id, $user2->id);
+
+        $request = $DB->get_records('message_contact_requests');
+
+        $this->assertCount(1, $request);
+
+        $request = reset($request);
+
+        $this->assertEquals($user1->id, $request->userid);
+        $this->assertEquals($user2->id, $request->requesteduserid);
+    }
+
+    /**
+     * Test confirming a contact request.
+     */
+    public function test_confirm_contact_request() {
+        global $DB;
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        \core_message\api::create_contact_request($user1->id, $user2->id);
+
+        \core_message\api::confirm_contact_request($user1->id, $user2->id);
+
+        $this->assertEquals(0, $DB->count_records('message_contact_requests'));
+
+        $contact = $DB->get_records('message_contacts');
+
+        $this->assertCount(1, $contact);
+
+        $contact = reset($contact);
+
+        $this->assertEquals($user1->id, $contact->userid);
+        $this->assertEquals($user2->id, $contact->contactid);
+    }
+
+    /**
+     * Test declining a contact request.
+     */
+    public function test_decline_contact_request() {
+        global $DB;
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        \core_message\api::create_contact_request($user1->id, $user2->id);
+
+        \core_message\api::decline_contact_request($user1->id, $user2->id);
+
+        $this->assertEquals(0, $DB->count_records('message_contact_requests'));
+        $this->assertEquals(0, $DB->count_records('message_contacts'));
+    }
+
+    /**
+     * Test retrieving contact requests.
+     */
+    public function test_get_contact_requests() {
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        // Block one user, their request should not show up.
+        \core_message\api::block_user($user1->id, $user3->id);
+
+        \core_message\api::create_contact_request($user2->id, $user1->id);
+        \core_message\api::create_contact_request($user3->id, $user1->id);
+
+        $requests = \core_message\api::get_contact_requests($user1->id);
+
+        $this->assertCount(1, $requests);
+
+        $request = reset($requests);
+
+        $this->assertEquals($user2->id, $request->id);
+        $this->assertEquals($user2->picture, $request->picture);
+        $this->assertEquals($user2->firstname, $request->firstname);
+        $this->assertEquals($user2->lastname, $request->lastname);
+        $this->assertEquals($user2->firstnamephonetic, $request->firstnamephonetic);
+        $this->assertEquals($user2->lastnamephonetic, $request->lastnamephonetic);
+        $this->assertEquals($user2->middlename, $request->middlename);
+        $this->assertEquals($user2->alternatename, $request->alternatename);
+        $this->assertEquals($user2->email, $request->email);
+    }
+
+    /**
+     * Test adding contacts.
+     */
+    public function test_add_contact() {
+        global $DB;
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        \core_message\api::add_contact($user1->id, $user2->id);
+
+        $contact = $DB->get_records('message_contacts');
+
+        $this->assertCount(1, $contact);
+
+        $contact = reset($contact);
+
+        $this->assertEquals($user1->id, $contact->userid);
+        $this->assertEquals($user2->id, $contact->contactid);
+    }
+
+    /**
+     * Test removing contacts.
+     */
+    public function test_remove_contact() {
+        global $DB;
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        \core_message\api::add_contact($user1->id, $user2->id);
+        \core_message\api::remove_contact($user1->id, $user2->id);
+
+        $this->assertEquals(0, $DB->count_records('message_contacts'));
+    }
+
+    /**
+     * Test blocking users.
+     */
+    public function test_block_user() {
+        global $DB;
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        \core_message\api::block_user($user1->id, $user2->id);
+
+        $blockedusers = $DB->get_records('message_users_blocked');
+
+        $this->assertCount(1, $blockedusers);
+
+        $blockeduser = reset($blockedusers);
+
+        $this->assertEquals($user1->id, $blockeduser->userid);
+        $this->assertEquals($user2->id, $blockeduser->blockeduserid);
+    }
+
+    /**
+     * Test unblocking users.
+     */
+    public function test_unblock_user() {
+        global $DB;
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        \core_message\api::block_user($user1->id, $user2->id);
+        \core_message\api::unblock_user($user1->id, $user2->id);
+
+        $this->assertEquals(0, $DB->count_records('message_users_blocked'));
+    }
+
+    /**
+     * Test is contact check.
+     */
+    public function test_is_contact() {
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        \core_message\api::add_contact($user1->id, $user2->id);
+
+        $this->assertTrue(\core_message\api::is_contact($user1->id, $user2->id));
+        $this->assertTrue(\core_message\api::is_contact($user2->id, $user1->id));
+        $this->assertFalse(\core_message\api::is_contact($user2->id, $user3->id));
+    }
+
+    /**
+     * Test get contact.
+     */
+    public function test_get_contact() {
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        \core_message\api::add_contact($user1->id, $user2->id);
+
+        $contact = \core_message\api::get_contact($user1->id, $user2->id);
+
+        $this->assertEquals($user1->id, $contact->userid);
+        $this->assertEquals($user2->id, $contact->contactid);
+    }
+
+    /**
+     * Test is blocked checked.
+     */
+    public function test_is_blocked() {
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->assertFalse(\core_message\api::is_blocked($user1->id, $user2->id));
+        $this->assertFalse(\core_message\api::is_blocked($user2->id, $user1->id));
+
+        \core_message\api::block_user($user1->id, $user2->id);
+
+        $this->assertTrue(\core_message\api::is_blocked($user1->id, $user2->id));
+        $this->assertFalse(\core_message\api::is_blocked($user2->id, $user1->id));
+    }
+
+    /**
+     * Test the contact request exist check.
+     */
+    public function test_does_contact_request_exist() {
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->assertFalse(\core_message\api::does_contact_request_exist($user1->id, $user2->id));
+        $this->assertFalse(\core_message\api::does_contact_request_exist($user2->id, $user1->id));
+
+        \core_message\api::create_contact_request($user1->id, $user2->id);
+
+        $this->assertTrue(\core_message\api::does_contact_request_exist($user1->id, $user2->id));
+        $this->assertTrue(\core_message\api::does_contact_request_exist($user2->id, $user1->id));
+    }
+
+    /**
+     * Comparison function for sorting contacts.
+     *
+     * @param stdClass $a
+     * @param stdClass $b
+     * @return bool
+     */
+    protected static function sort_contacts($a, $b) {
+        return $a->userid > $b->userid;
+    }
 }
index 062ed01..135e154 100644 (file)
@@ -6,16 +6,34 @@ Feature: Manage contacts
 
   Background:
     Given the following "users" exist:
-      | username | firstname | lastname | email            |
+      | username | firstname | lastname | email                |
       | user1    | User      | 1        | user1@example.com    |
       | user2    | User      | 2        | user2@example.com    |
       | user3    | User      | 3        | user3@example.com    |
+    And I log in as "admin"
+    And I set the following administration settings values:
+      | messagingallusers | 1 |
+    And I log out
     And I log in as "user1"
     And I view the "User 2" contact in the message area
     And I click on "Add contact" "link"
     And I view the "User 3" contact in the message area
     And I click on "Add contact" "link"
     And I log out
+    # Approve the contact request for user2
+    And I log in as "user2"
+    And I open the notification popover
+    And I click on "View full notification" "link" in the "#nav-notification-popover-container" "css_element"
+    And I click on "Go to" "link" in the "[data-region=footer]" "css_element"
+    And I click on "Approve" "link" in the "User 1" "table_row"
+    And I log out
+    # Approve the contact request for user3
+    And I log in as "user3"
+    And I open the notification popover
+    And I click on "View full notification" "link" in the "#nav-notification-popover-container" "css_element"
+    And I click on "Go to" "link" in the "[data-region=footer]" "css_element"
+    And I click on "Approve" "link" in the "User 1" "table_row"
+    And I log out
 
   Scenario: Add contact shows in contacts tab
     When I log in as "user1"
index 3f63ed0..25660ce 100644 (file)
@@ -52,6 +52,8 @@ class core_message_events_testcase extends core_message_messagelib_testcase {
      * Test the message contact added event.
      */
     public function test_message_contact_added() {
+        global $USER;
+
         // Set this user as the admin.
         $this->setAdminUser();
 
@@ -60,7 +62,7 @@ class core_message_events_testcase extends core_message_messagelib_testcase {
 
         // Trigger and capture the event when adding a contact.
         $sink = $this->redirectEvents();
-        message_add_contact($user->id);
+        \core_message\api::add_contact($USER->id, $user->id);
         $events = $sink->get_events();
         $event = reset($events);
 
@@ -78,6 +80,8 @@ class core_message_events_testcase extends core_message_messagelib_testcase {
      * Test the message contact removed event.
      */
     public function test_message_contact_removed() {
+        global $USER;
+
         // Set this user as the admin.
         $this->setAdminUser();
 
@@ -85,11 +89,11 @@ class core_message_events_testcase extends core_message_messagelib_testcase {
         $user = $this->getDataGenerator()->create_user();
 
         // Add the user to the admin's contact list.
-        message_add_contact($user->id);
+        \core_message\api::add_contact($USER->id, $user->id);
 
         // Trigger and capture the event when adding a contact.
         $sink = $this->redirectEvents();
-        message_remove_contact($user->id);
+        \core_message\api::remove_contact($USER->id, $user->id);
         $events = $sink->get_events();
         $event = reset($events);
 
@@ -104,61 +108,37 @@ class core_message_events_testcase extends core_message_messagelib_testcase {
     }
 
     /**
-     * Test the message contact blocked event.
+     * Test the message user blocked event.
      */
-    public function test_message_contact_blocked() {
+    public function test_message_user_blocked() {
+        global $USER;
+
         // Set this user as the admin.
         $this->setAdminUser();
 
         // Create a user to add to the admin's contact list.
         $user = $this->getDataGenerator()->create_user();
-        $user2 = $this->getDataGenerator()->create_user();
 
         // Add the user to the admin's contact list.
-        message_add_contact($user->id);
+        \core_message\api::add_contact($USER->id, $user->id);
 
         // Trigger and capture the event when blocking a contact.
         $sink = $this->redirectEvents();
-        message_block_contact($user->id);
+        \core_message\api::block_user($USER->id, $user->id);
         $events = $sink->get_events();
         $event = reset($events);
 
         // Check that the event data is valid.
-        $this->assertInstanceOf('\core\event\message_contact_blocked', $event);
+        $this->assertInstanceOf('\core\event\message_user_blocked', $event);
         $this->assertEquals(context_user::instance(2), $event->get_context());
-        $expected = array(SITEID, 'message', 'block contact', 'index.php?user1=' . $user->id . '&amp;user2=2', $user->id);
-        $this->assertEventLegacyLogData($expected, $event);
-        $url = new moodle_url('/message/index.php', array('user1' => $event->userid, 'user2' => $event->relateduserid));
-        $this->assertEquals($url, $event->get_url());
-
-        // Make sure that the contact blocked event is not triggered again.
-        $sink->clear();
-        message_block_contact($user->id);
-        $events = $sink->get_events();
-        $event = reset($events);
-        $this->assertEmpty($event);
-        // Make sure that we still have 1 blocked user.
-        $this->assertEquals(1, \core_message\api::count_blocked_users());
-
-        // Now blocking a user that is not a contact.
-        $sink->clear();
-        message_block_contact($user2->id);
-        $events = $sink->get_events();
-        $event = reset($events);
-
-        // Check that the event data is valid.
-        $this->assertInstanceOf('\core\event\message_contact_blocked', $event);
-        $this->assertEquals(context_user::instance(2), $event->get_context());
-        $expected = array(SITEID, 'message', 'block contact', 'index.php?user1=' . $user2->id . '&amp;user2=2', $user2->id);
-        $this->assertEventLegacyLogData($expected, $event);
-        $url = new moodle_url('/message/index.php', array('user1' => $event->userid, 'user2' => $event->relateduserid));
-        $this->assertEquals($url, $event->get_url());
     }
 
     /**
-     * Test the message contact unblocked event.
+     * Test the message user unblocked event.
      */
-    public function test_message_contact_unblocked() {
+    public function test_message_user_unblocked() {
+        global $USER;
+
         // Set this user as the admin.
         $this->setAdminUser();
 
@@ -166,39 +146,25 @@ class core_message_events_testcase extends core_message_messagelib_testcase {
         $user = $this->getDataGenerator()->create_user();
 
         // Add the user to the admin's contact list.
-        message_add_contact($user->id);
+        \core_message\api::add_contact($USER->id, $user->id);
 
         // Block the user.
-        message_block_contact($user->id);
+        \core_message\api::block_user($USER->id, $user->id);
         // Make sure that we have 1 blocked user.
         $this->assertEquals(1, \core_message\api::count_blocked_users());
 
         // Trigger and capture the event when unblocking a contact.
         $sink = $this->redirectEvents();
-        message_unblock_contact($user->id);
+        \core_message\api::unblock_user($USER->id, $user->id);
         $events = $sink->get_events();
         $event = reset($events);
 
         // Check that the event data is valid.
-        $this->assertInstanceOf('\core\event\message_contact_unblocked', $event);
+        $this->assertInstanceOf('\core\event\message_user_unblocked', $event);
         $this->assertEquals(context_user::instance(2), $event->get_context());
-        $expected = array(SITEID, 'message', 'unblock contact', 'index.php?user1=' . $user->id . '&amp;user2=2', $user->id);
-        $this->assertEventLegacyLogData($expected, $event);
-        $url = new moodle_url('/message/index.php', array('user1' => $event->userid, 'user2' => $event->relateduserid));
-        $this->assertEquals($url, $event->get_url());
 
         // Make sure that we have no blocked users.
         $this->assertEmpty(\core_message\api::count_blocked_users());
-
-        // Make sure that the contact unblocked event is not triggered again.
-        $sink->clear();
-        message_unblock_contact($user->id);
-        $events = $sink->get_events();
-        $event = reset($events);
-        $this->assertEmpty($event);
-
-        // Make sure that we still have no blocked users.
-        $this->assertEmpty(\core_message\api::count_blocked_users());
     }
 
     /**
index f78cc46..784ad14 100644 (file)
@@ -91,42 +91,154 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
     }
 
     /**
-     * Test send_instant_messages
+     * Test send_instant_messages.
      */
     public function test_send_instant_messages() {
+        global $DB, $USER;
 
-        global $DB, $USER, $CFG;
+        $this->resetAfterTest();
 
-        $this->resetAfterTest(true);
         // Transactions used in tests, tell phpunit use alternative reset method.
         $this->preventResetByRollback();
 
-        // Turn off all message processors (so nothing is really sent)
-        require_once($CFG->dirroot . '/message/lib.php');
-        $messageprocessors = get_message_processors();
-        foreach($messageprocessors as $messageprocessor) {
-            $messageprocessor->enabled = 0;
-            $DB->update_record('message_processors', $messageprocessor);
-        }
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
 
-        // Set the required capabilities by the external function
-        $contextid = context_system::instance()->id;
-        $roleid = $this->assignUserCapability('moodle/site:sendmessage', $contextid);
+        $this->setUser($user1);
+
+        // Create test message data.
+        $message1 = array();
+        $message1['touserid'] = $user2->id;
+        $message1['text'] = 'the message.';
+        $message1['clientmsgid'] = 4;
+        $messages = array($message1);
+
+        $sentmessages = core_message_external::send_instant_messages($messages);
+        $sentmessages = external_api::clean_returnvalue(core_message_external::send_instant_messages_returns(), $sentmessages);
+
+        $sentmessage = reset($sentmessages);
+
+        $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 != ?
+                  AND m.id = ?";
+        $themessage = $DB->get_record_sql($sql, [$USER->id, $sentmessage['msgid']]);
+
+        // Confirm that the message was inserted correctly.
+        $this->assertEquals($themessage->useridfrom, $user1->id);
+        $this->assertEquals($themessage->useridto, $message1['touserid']);
+        $this->assertEquals($themessage->smallmessage, $message1['text']);
+        $this->assertEquals($sentmessage['clientmsgid'], $message1['clientmsgid']);
+    }
+
+    /**
+     * Test send_instant_messages to a user who has blocked you.
+     */
+    public function test_send_instant_messages_blocked_user() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Transactions used in tests, tell phpunit use alternative reset method.
+        $this->preventResetByRollback();
 
         $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+
+        \core_message\api::block_user($user2->id, $user1->id);
 
         // Create test message data.
         $message1 = array();
-        $message1['touserid'] = $user1->id;
+        $message1['touserid'] = $user2->id;
         $message1['text'] = 'the message.';
         $message1['clientmsgid'] = 4;
         $messages = array($message1);
 
         $sentmessages = core_message_external::send_instant_messages($messages);
+        $sentmessages = external_api::clean_returnvalue(core_message_external::send_instant_messages_returns(), $sentmessages);
 
-        // We need to execute the return values cleaning process to simulate the web service server.
+        $sentmessage = reset($sentmessages);
+
+        $this->assertEquals(get_string('userisblockingyou', 'message'), $sentmessage['errormessage']);
+
+        $this->assertEquals(0, $DB->count_records('messages'));
+    }
+
+    /**
+     * Test send_instant_messages when sending a message to a non-contact who has blocked non-contacts.
+     */
+    public function test_send_instant_messages_block_non_contacts() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        // Transactions used in tests, tell phpunit use alternative reset method.
+        $this->preventResetByRollback();
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+
+        // Set the user preference so user 2 does not accept messages from non-contacts.
+        set_user_preference('message_blocknoncontacts', 1, $user2);
+
+        // Create test message data.
+        $message1 = array();
+        $message1['touserid'] = $user2->id;
+        $message1['text'] = 'the message.';
+        $message1['clientmsgid'] = 4;
+        $messages = array($message1);
+
+        $sentmessages = core_message_external::send_instant_messages($messages);
+        $sentmessages = external_api::clean_returnvalue(core_message_external::send_instant_messages_returns(), $sentmessages);
+
+        $sentmessage = reset($sentmessages);
+
+        $this->assertEquals(get_string('userisblockingyounoncontact', 'message', fullname($user2)), $sentmessage['errormessage']);
+
+        $this->assertEquals(0, $DB->count_records('messages'));
+    }
+
+    /**
+     * Test send_instant_messages when sending a message to a contact who has blocked non-contacts.
+     */
+    public function test_send_instant_messages_block_non_contacts_but_am_contact() {
+        global $DB, $USER;
+
+        $this->resetAfterTest(true);
+
+        // Transactions used in tests, tell phpunit use alternative reset method.
+        $this->preventResetByRollback();
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+
+        // Set the user preference so user 2 does not accept messages from non-contacts.
+        set_user_preference('message_blocknoncontacts', 1, $user2);
+
+        \core_message\api::add_contact($user1->id, $user2->id);
+
+        // Create test message data.
+        $message1 = array();
+        $message1['touserid'] = $user2->id;
+        $message1['text'] = 'the message.';
+        $message1['clientmsgid'] = 4;
+        $messages = array($message1);
+
+        $sentmessages = core_message_external::send_instant_messages($messages);
         $sentmessages = external_api::clean_returnvalue(core_message_external::send_instant_messages_returns(), $sentmessages);
 
+        $sentmessage = reset($sentmessages);
+
         $sql = "SELECT m.*, mcm.userid as useridto
                  FROM {messages} m
            INNER JOIN {message_conversations} mc
@@ -135,13 +247,75 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                    ON mcm.conversationid = mc.id
                 WHERE mcm.userid != ?
                   AND m.id = ?";
-        $themessage = $DB->get_record_sql($sql, [$USER->id, $sentmessages[0]['msgid']]);
+        $themessage = $DB->get_record_sql($sql, [$USER->id, $sentmessage['msgid']]);
 
         // Confirm that the message was inserted correctly.
-        $this->assertEquals($themessage->useridfrom, $USER->id);
+        $this->assertEquals($themessage->useridfrom, $user1->id);
         $this->assertEquals($themessage->useridto, $message1['touserid']);
         $this->assertEquals($themessage->smallmessage, $message1['text']);
-        $this->assertEquals($sentmessages[0]['clientmsgid'], $message1['clientmsgid']);
+        $this->assertEquals($sentmessage['clientmsgid'], $message1['clientmsgid']);
+    }
+
+    /**
+     * Test send_instant_messages with no capabilities
+     */
+    public function test_send_instant_messages_no_capability() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        // Transactions used in tests, tell phpunit use alternative reset method.
+        $this->preventResetByRollback();
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+
+        // Unset the required capabilities by the external function.
+        $contextid = context_system::instance()->id;
+        $userrole = $DB->get_record('role', array('shortname' => 'user'));
+        $this->unassignUserCapability('moodle/site:sendmessage', $contextid, $userrole->id);
+
+        // Create test message data.
+        $message1 = array();
+        $message1['touserid'] = $user2->id;
+        $message1['text'] = 'the message.';
+        $message1['clientmsgid'] = 4;
+        $messages = array($message1);
+
+        $this->expectException('required_capability_exception');
+        core_message_external::send_instant_messages($messages);
+    }
+
+    /**
+     * Test send_instant_messages when messaging is disabled.
+     */
+    public function test_send_instant_messages_messaging_disabled() {
+        global $CFG;
+
+        $this->resetAfterTest(true);
+
+        // Transactions used in tests, tell phpunit use alternative reset method.
+        $this->preventResetByRollback();
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+
+        // Disable messaging.
+        $CFG->messaging = 0;
+
+        // Create test message data.
+        $message1 = array();
+        $message1['touserid'] = $user2->id;
+        $message1['text'] = 'the message.';
+        $message1['clientmsgid'] = 4;
+        $messages = array($message1);
+
+        $this->expectException('moodle_exception');
+        core_message_external::send_instant_messages($messages);
     }
 
     /**
@@ -159,21 +333,25 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
 
         // Adding a contact.
         $return = core_message_external::create_contacts(array($user2->id));
+        $this->assertDebuggingCalled();
         $return = external_api::clean_returnvalue(core_message_external::create_contacts_returns(), $return);
         $this->assertEquals(array(), $return);
 
         // Adding a contact who is already a contact.
         $return = core_message_external::create_contacts(array($user2->id));
+        $this->assertDebuggingCalled();
         $return = external_api::clean_returnvalue(core_message_external::create_contacts_returns(), $return);
         $this->assertEquals(array(), $return);
 
         // Adding multiple contacts.
         $return = core_message_external::create_contacts(array($user3->id, $user4->id));
+        $this->assertDebuggingCalledCount(2);
         $return = external_api::clean_returnvalue(core_message_external::create_contacts_returns(), $return);
         $this->assertEquals(array(), $return);
 
         // Adding a non-existing user.
         $return = core_message_external::create_contacts(array(99999));
+        $this->assertDebuggingCalled();
         $return = external_api::clean_returnvalue(core_message_external::create_contacts_returns(), $return);
         $this->assertCount(1, $return);
         $return = array_pop($return);
@@ -182,6 +360,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
 
         // Adding contacts with valid and invalid parameters.
         $return = core_message_external::create_contacts(array($user5->id, 99999));
+        $this->assertDebuggingCalledCount(2);
         $return = external_api::clean_returnvalue(core_message_external::create_contacts_returns(), $return);
         $this->assertCount(1, $return);
         $return = array_pop($return);
@@ -207,8 +386,11 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $user5 = self::getDataGenerator()->create_user();
         $user6 = self::getDataGenerator()->create_user();
         $this->setUser($user1);
-        $this->assertEquals(array(), core_message_external::create_contacts(
-            array($user3->id, $user4->id, $user5->id, $user6->id)));
+
+        \core_message\api::add_contact($user1->id, $user3->id);
+        \core_message\api::add_contact($user1->id, $user4->id);
+        \core_message\api::add_contact($user1->id, $user5->id);
+        \core_message\api::add_contact($user1->id, $user6->id);
 
         // Removing a non-contact.
         $return = core_message_external::delete_contacts(array($user2->id));
@@ -248,25 +430,32 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $user4 = self::getDataGenerator()->create_user();
         $user5 = self::getDataGenerator()->create_user();
         $this->setUser($user1);
-        $this->assertEquals(array(), core_message_external::create_contacts(array($user3->id, $user4->id, $user5->id)));
+
+        \core_message\api::add_contact($user1->id, $user3->id);
+        \core_message\api::add_contact($user1->id, $user4->id);
+        \core_message\api::add_contact($user1->id, $user5->id);
 
         // Blocking a contact.
         $return = core_message_external::block_contacts(array($user2->id));
+        $this->assertDebuggingCalled();
         $return = external_api::clean_returnvalue(core_message_external::block_contacts_returns(), $return);
         $this->assertEquals(array(), $return);
 
         // Blocking a contact who is already a contact.
         $return = core_message_external::block_contacts(array($user2->id));
+        $this->assertDebuggingCalled();
         $return = external_api::clean_returnvalue(core_message_external::block_contacts_returns(), $return);
         $this->assertEquals(array(), $return);
 
         // Blocking multiple contacts.
         $return = core_message_external::block_contacts(array($user3->id, $user4->id));
+        $this->assertDebuggingCalledCount(2);
         $return = external_api::clean_returnvalue(core_message_external::block_contacts_returns(), $return);
         $this->assertEquals(array(), $return);
 
         // Blocking a non-existing user.
         $return = core_message_external::block_contacts(array(99999));
+        $this->assertDebuggingCalled();
         $return = external_api::clean_returnvalue(core_message_external::block_contacts_returns(), $return);
         $this->assertCount(1, $return);
         $return = array_pop($return);
@@ -275,6 +464,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
 
         // Blocking contacts with valid and invalid parameters.
         $return = core_message_external::block_contacts(array($user5->id, 99999));
+        $this->assertDebuggingCalledCount(2);
         $return = external_api::clean_returnvalue(core_message_external::block_contacts_returns(), $return);
         $this->assertCount(1, $return);
         $return = array_pop($return);
@@ -300,33 +490,494 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $user5 = self::getDataGenerator()->create_user();
         $user6 = self::getDataGenerator()->create_user();
         $this->setUser($user1);
-        $this->assertEquals(array(), core_message_external::create_contacts(
-