Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Contains class used to return information to display for the message area.
  19   *
  20   * @package    core_message
  21   * @copyright  2016 Mark Nelson <markn@moodle.com>
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace core_message;
  26  
  27  use core_favourites\local\entity\favourite;
  28  
  29  defined('MOODLE_INTERNAL') || die();
  30  
  31  require_once($CFG->dirroot . '/lib/messagelib.php');
  32  
  33  /**
  34   * Class used to return information to display for the message area.
  35   *
  36   * @copyright  2016 Mark Nelson <markn@moodle.com>
  37   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  38   */
  39  class api {
  40  
  41      /**
  42       * The action for reading a message.
  43       */
  44      const MESSAGE_ACTION_READ = 1;
  45  
  46      /**
  47       * The action for deleting a message.
  48       */
  49      const MESSAGE_ACTION_DELETED = 2;
  50  
  51      /**
  52       * The action for reading a message.
  53       */
  54      const CONVERSATION_ACTION_MUTED = 1;
  55  
  56      /**
  57       * The privacy setting for being messaged by anyone within courses user is member of.
  58       */
  59      const MESSAGE_PRIVACY_COURSEMEMBER = 0;
  60  
  61      /**
  62       * The privacy setting for being messaged only by contacts.
  63       */
  64      const MESSAGE_PRIVACY_ONLYCONTACTS = 1;
  65  
  66      /**
  67       * The privacy setting for being messaged by anyone on the site.
  68       */
  69      const MESSAGE_PRIVACY_SITE = 2;
  70  
  71      /**
  72       * An individual conversation.
  73       */
  74      const MESSAGE_CONVERSATION_TYPE_INDIVIDUAL = 1;
  75  
  76      /**
  77       * A group conversation.
  78       */
  79      const MESSAGE_CONVERSATION_TYPE_GROUP = 2;
  80  
  81      /**
  82       * A self conversation.
  83       */
  84      const MESSAGE_CONVERSATION_TYPE_SELF = 3;
  85  
  86      /**
  87       * The state for an enabled conversation area.
  88       */
  89      const MESSAGE_CONVERSATION_ENABLED = 1;
  90  
  91      /**
  92       * The state for a disabled conversation area.
  93       */
  94      const MESSAGE_CONVERSATION_DISABLED = 0;
  95  
  96      /**
  97       * The max message length.
  98       */
  99      const MESSAGE_MAX_LENGTH = 4096;
 100  
 101      /**
 102       * Handles searching for messages in the message area.
 103       *
 104       * @param int $userid The user id doing the searching
 105       * @param string $search The string the user is searching
 106       * @param int $limitfrom
 107       * @param int $limitnum
 108       * @return array
 109       */
 110      public static function search_messages($userid, $search, $limitfrom = 0, $limitnum = 0) {
 111          global $DB;
 112  
 113          // Get the user fields we want.
 114          $ufields = \user_picture::fields('u', array('lastaccess'), 'userfrom_id', 'userfrom_');
 115          $ufields2 = \user_picture::fields('u2', array('lastaccess'), 'userto_id', 'userto_');
 116          // Add the uniqueid column to make each row unique and avoid SQL errors.
 117          $uniqueidsql = $DB->sql_concat('m.id', "'_'", 'm.useridfrom', "'_'", 'mcm.userid');
 118  
 119          $sql = "SELECT $uniqueidsql AS uniqueid, m.id, m.useridfrom, mcm.userid as useridto, m.subject, m.fullmessage,
 120                         m.fullmessagehtml, m.fullmessageformat, m.smallmessage, m.conversationid, m.timecreated, 0 as isread,
 121                         $ufields, mub.id as userfrom_blocked, $ufields2, mub2.id as userto_blocked
 122                    FROM {messages} m
 123              INNER JOIN {user} u
 124                      ON u.id = m.useridfrom
 125              INNER JOIN {message_conversations} mc
 126                      ON mc.id = m.conversationid
 127              INNER JOIN {message_conversation_members} mcm
 128                      ON mcm.conversationid = m.conversationid
 129              INNER JOIN {user} u2
 130                      ON u2.id = mcm.userid
 131               LEFT JOIN {message_users_blocked} mub
 132                      ON (mub.blockeduserid = u.id AND mub.userid = ?)
 133               LEFT JOIN {message_users_blocked} mub2
 134                      ON (mub2.blockeduserid = u2.id AND mub2.userid = ?)
 135               LEFT JOIN {message_user_actions} mua
 136                      ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
 137                   WHERE (m.useridfrom = ? OR mcm.userid = ?)
 138                     AND (m.useridfrom != mcm.userid OR mc.type = ?)
 139                     AND u.deleted = 0
 140                     AND u2.deleted = 0
 141                     AND mua.id is NULL
 142                     AND " . $DB->sql_like('smallmessage', '?', false) . "
 143                ORDER BY timecreated DESC";
 144  
 145          $params = array($userid, $userid, $userid, self::MESSAGE_ACTION_DELETED, $userid, $userid,
 146              self::MESSAGE_CONVERSATION_TYPE_SELF, '%' . $search . '%');
 147  
 148          // Convert the messages into searchable contacts with their last message being the message that was searched.
 149          $conversations = array();
 150          if ($messages = $DB->get_records_sql($sql, $params, $limitfrom, $limitnum)) {
 151              foreach ($messages as $message) {
 152                  $prefix = 'userfrom_';
 153                  if ($userid == $message->useridfrom) {
 154                      $prefix = 'userto_';
 155                      // If it from the user, then mark it as read, even if it wasn't by the receiver.
 156                      $message->isread = true;
 157                  }
 158                  $blockedcol = $prefix . 'blocked';
 159                  $message->blocked = $message->$blockedcol ? 1 : 0;
 160  
 161                  $message->messageid = $message->id;
 162                  // To avoid duplicate messages, only add the message if it hasn't been added previously.
 163                  if (!array_key_exists($message->messageid, $conversations)) {
 164                      $conversations[$message->messageid] = helper::create_contact($message, $prefix);
 165                  }
 166              }
 167              // Remove the messageid keys (to preserve the expected type).
 168              $conversations = array_values($conversations);
 169          }
 170  
 171          return $conversations;
 172      }
 173  
 174      /**
 175       * Handles searching for user in a particular course in the message area.
 176       *
 177       * TODO: This function should be removed once the related web service goes through final deprecation.
 178       * The related web service is data_for_messagearea_search_users_in_course.
 179       * Followup: MDL-63261
 180       *
 181       * @param int $userid The user id doing the searching
 182       * @param int $courseid The id of the course we are searching in
 183       * @param string $search The string the user is searching
 184       * @param int $limitfrom
 185       * @param int $limitnum
 186       * @return array
 187       */
 188      public static function search_users_in_course($userid, $courseid, $search, $limitfrom = 0, $limitnum = 0) {
 189          global $DB;
 190  
 191          // Get all the users in the course.
 192          list($esql, $params) = get_enrolled_sql(\context_course::instance($courseid), '', 0, true);
 193          $sql = "SELECT u.*, mub.id as isblocked
 194                    FROM {user} u
 195                    JOIN ($esql) je
 196                      ON je.id = u.id
 197               LEFT JOIN {message_users_blocked} mub
 198                      ON (mub.blockeduserid = u.id AND mub.userid = :userid)
 199                   WHERE u.deleted = 0";
 200          // Add more conditions.
 201          $fullname = $DB->sql_fullname();
 202          $sql .= " AND u.id != :userid2
 203                    AND " . $DB->sql_like($fullname, ':search', false) . "
 204               ORDER BY " . $DB->sql_fullname();
 205          $params = array_merge(array('userid' => $userid, 'userid2' => $userid, 'search' => '%' . $search . '%'), $params);
 206  
 207          // Convert all the user records into contacts.
 208          $contacts = array();
 209          if ($users = $DB->get_records_sql($sql, $params, $limitfrom, $limitnum)) {
 210              foreach ($users as $user) {
 211                  $user->blocked = $user->isblocked ? 1 : 0;
 212                  $contacts[] = helper::create_contact($user);
 213              }
 214          }
 215  
 216          return $contacts;
 217      }
 218  
 219      /**
 220       * Handles searching for user in the message area.
 221       *
 222       * TODO: This function should be removed once the related web service goes through final deprecation.
 223       * The related web service is data_for_messagearea_search_users.
 224       * Followup: MDL-63261
 225       *
 226       * @param int $userid The user id doing the searching
 227       * @param string $search The string the user is searching
 228       * @param int $limitnum
 229       * @return array
 230       */
 231      public static function search_users($userid, $search, $limitnum = 0) {
 232          global $CFG, $DB;
 233  
 234          // Used to search for contacts.
 235          $fullname = $DB->sql_fullname();
 236          $ufields = \user_picture::fields('u', array('lastaccess'));
 237  
 238          // Users not to include.
 239          $excludeusers = array($userid, $CFG->siteguest);
 240          list($exclude, $excludeparams) = $DB->get_in_or_equal($excludeusers, SQL_PARAMS_NAMED, 'param', false);
 241  
 242          // Ok, let's search for contacts first.
 243          $contacts = array();
 244          $sql = "SELECT $ufields, mub.id as isuserblocked
 245                    FROM {user} u
 246                    JOIN {message_contacts} mc
 247                      ON u.id = mc.contactid
 248               LEFT JOIN {message_users_blocked} mub
 249                      ON (mub.userid = :userid2 AND mub.blockeduserid = u.id)
 250                   WHERE mc.userid = :userid
 251                     AND u.deleted = 0
 252                     AND u.confirmed = 1
 253                     AND " . $DB->sql_like($fullname, ':search', false) . "
 254                     AND u.id $exclude
 255                ORDER BY " . $DB->sql_fullname();
 256          if ($users = $DB->get_records_sql($sql, array('userid' => $userid, 'userid2' => $userid,
 257                  'search' => '%' . $search . '%') + $excludeparams, 0, $limitnum)) {
 258              foreach ($users as $user) {
 259                  $user->blocked = $user->isuserblocked ? 1 : 0;
 260                  $contacts[] = helper::create_contact($user);
 261              }
 262          }
 263  
 264          // Now, let's get the courses.
 265          // Make sure to limit searches to enrolled courses.
 266          $enrolledcourses = enrol_get_my_courses(array('id', 'cacherev'));
 267          $courses = array();
 268          // Really we want the user to be able to view the participants if they have the capability
 269          // 'moodle/course:viewparticipants' or 'moodle/course:enrolreview', but since the search_courses function
 270          // only takes required parameters we can't. However, the chance of a user having 'moodle/course:enrolreview' but
 271          // *not* 'moodle/course:viewparticipants' are pretty much zero, so it is not worth addressing.
 272          if ($arrcourses = \core_course_category::search_courses(array('search' => $search), array('limit' => $limitnum),
 273                  array('moodle/course:viewparticipants'))) {
 274              foreach ($arrcourses as $course) {
 275                  if (isset($enrolledcourses[$course->id])) {
 276                      $data = new \stdClass();
 277                      $data->id = $course->id;
 278                      $data->shortname = $course->shortname;
 279                      $data->fullname = $course->fullname;
 280                      $courses[] = $data;
 281                  }
 282              }
 283          }
 284  
 285          // Let's get those non-contacts. Toast them gears boi.
 286          // Note - you can only block contacts, so these users will not be blocked, so no need to get that
 287          // extra detail from the database.
 288          $noncontacts = array();
 289          $sql = "SELECT $ufields
 290                    FROM {user} u
 291                   WHERE u.deleted = 0
 292                     AND u.confirmed = 1
 293                     AND " . $DB->sql_like($fullname, ':search', false) . "
 294                     AND u.id $exclude
 295                     AND u.id NOT IN (SELECT contactid
 296                                        FROM {message_contacts}
 297                                       WHERE userid = :userid)
 298                ORDER BY " . $DB->sql_fullname();
 299          if ($users = $DB->get_records_sql($sql,  array('userid' => $userid, 'search' => '%' . $search . '%') + $excludeparams,
 300                  0, $limitnum)) {
 301              foreach ($users as $user) {
 302                  $noncontacts[] = helper::create_contact($user);
 303              }
 304          }
 305  
 306          return array($contacts, $courses, $noncontacts);
 307      }
 308  
 309      /**
 310       * Handles searching for user.
 311       *
 312       * @param int $userid The user id doing the searching
 313       * @param string $search The string the user is searching
 314       * @param int $limitfrom
 315       * @param int $limitnum
 316       * @return array
 317       */
 318      public static function message_search_users(int $userid, string $search, int $limitfrom = 0, int $limitnum = 20) : array {
 319          global $CFG, $DB;
 320  
 321          // Check if messaging is enabled.
 322          if (empty($CFG->messaging)) {
 323              throw new \moodle_exception('disabled', 'message');
 324          }
 325  
 326          // Used to search for contacts.
 327          $fullname = $DB->sql_fullname();
 328  
 329          // Users not to include.
 330          $excludeusers = array($CFG->siteguest);
 331          if (!$selfconversation = self::get_self_conversation($userid)) {
 332              // Userid should only be excluded when she hasn't a self-conversation.
 333              $excludeusers[] = $userid;
 334          }
 335          list($exclude, $excludeparams) = $DB->get_in_or_equal($excludeusers, SQL_PARAMS_NAMED, 'param', false);
 336  
 337          $params = array('search' => '%' . $DB->sql_like_escape($search) . '%', 'userid1' => $userid, 'userid2' => $userid);
 338  
 339          // Ok, let's search for contacts first.
 340          $sql = "SELECT u.id
 341                    FROM {user} u
 342                    JOIN {message_contacts} mc
 343                      ON (u.id = mc.contactid AND mc.userid = :userid1) OR (u.id = mc.userid AND mc.contactid = :userid2)
 344                   WHERE u.deleted = 0
 345                     AND u.confirmed = 1
 346                     AND " . $DB->sql_like($fullname, ':search', false) . "
 347                     AND u.id $exclude
 348                ORDER BY " . $DB->sql_fullname();
 349          $foundusers = $DB->get_records_sql_menu($sql, $params + $excludeparams, $limitfrom, $limitnum);
 350  
 351          $contacts = [];
 352          if (!empty($foundusers)) {
 353              $contacts = helper::get_member_info($userid, array_keys($foundusers));
 354              foreach ($contacts as $memberuserid => $memberinfo) {
 355                  $contacts[$memberuserid]->conversations = self::get_conversations_between_users($userid, $memberuserid, 0, 1000);
 356              }
 357          }
 358  
 359          // Let's get those non-contacts.
 360          // Because we can't achieve all the required visibility checks in SQL, we'll iterate through the non-contact records
 361          // and stop once we have enough matching the 'visible' criteria.
 362          // TODO: MDL-63983 - Improve the performance of non-contact searches when site-wide messaging is disabled (default).
 363  
 364          // Use a local generator to achieve this iteration.
 365          $getnoncontactusers = function ($limitfrom = 0, $limitnum = 0) use($fullname, $exclude, $params, $excludeparams) {
 366              global $DB;
 367              $sql = "SELECT u.*
 368                    FROM {user} u
 369                   WHERE u.deleted = 0
 370                     AND u.confirmed = 1
 371                     AND " . $DB->sql_like($fullname, ':search', false) . "
 372                     AND u.id $exclude
 373                     AND NOT EXISTS (SELECT mc.id
 374                                       FROM {message_contacts} mc
 375                                      WHERE (mc.userid = u.id AND mc.contactid = :userid1)
 376                                         OR (mc.userid = :userid2 AND mc.contactid = u.id))
 377                ORDER BY " . $DB->sql_fullname();
 378              while ($records = $DB->get_records_sql($sql, $params + $excludeparams, $limitfrom, $limitnum)) {
 379                  yield $records;
 380                  $limitfrom += $limitnum;
 381              }
 382          };
 383  
 384          // Fetch in batches of $limitnum * 2 to improve the chances of matching a user without going back to the DB.
 385          // The generator cannot function without a sensible limiter, so set one if this is not set.
 386          $batchlimit = ($limitnum == 0) ? 20 : $limitnum;
 387  
 388          // We need to make the offset param work with the generator.
 389          // Basically, if we want to get say 10 records starting at the 40th record, we need to see 50 records and return only
 390          // those after the 40th record. We can never pass the method's offset param to the generator as we need to manage the
 391          // position within those valid records ourselves.
 392          // See MDL-63983 dealing with performance improvements to this area of code.
 393          $noofvalidseenrecords = 0;
 394          $returnedusers = [];
 395          foreach ($getnoncontactusers(0, $batchlimit) as $users) {
 396              foreach ($users as $id => $user) {
 397                  // User visibility checks: only return users who are visible to the user performing the search.
 398                  // Which visibility check to use depends on the 'messagingallusers' (site wide messaging) setting:
 399                  // - If enabled, return matched users whose profiles are visible to the current user anywhere (site or course).
 400                  // - If disabled, only return matched users whose course profiles are visible to the current user.
 401                  $userdetails = \core_message\helper::search_get_user_details($user);
 402  
 403                  // Return the user only if the searched field is returned.
 404                  // Otherwise it means that the $USER was not allowed to search the returned user.
 405                  if (!empty($userdetails) and !empty($userdetails['fullname'])) {
 406                      // We know we've matched, but only save the record if it's within the offset area we need.
 407                      if ($limitfrom == 0) {
 408                          // No offset specified, so just save.
 409                          $returnedusers[$id] = $user;
 410                      } else {
 411                          // There is an offset in play.
 412                          // If we've passed enough records already (> offset value), then we can save this one.
 413                          if ($noofvalidseenrecords >= $limitfrom) {
 414                              $returnedusers[$id] = $user;
 415                          }
 416                      }
 417                      if (count($returnedusers) == $limitnum) {
 418                          break 2;
 419                      }
 420                      $noofvalidseenrecords++;
 421                  }
 422              }
 423          }
 424          $foundusers = $returnedusers;
 425  
 426          $noncontacts = [];
 427          if (!empty($foundusers)) {
 428              $noncontacts = helper::get_member_info($userid, array_keys($foundusers));
 429              foreach ($noncontacts as $memberuserid => $memberinfo) {
 430                  if ($memberuserid !== $userid) {
 431                      $noncontacts[$memberuserid]->conversations = self::get_conversations_between_users($userid, $memberuserid, 0,
 432                          1000);
 433                  } else {
 434                      $noncontacts[$memberuserid]->conversations[$selfconversation->id] = $selfconversation;
 435                  }
 436              }
 437          }
 438  
 439          return array(array_values($contacts), array_values($noncontacts));
 440      }
 441  
 442      /**
 443       * Gets extra fields, like image url and subname for any conversations linked to components.
 444       *
 445       * The subname is like a subtitle for the conversation, to compliment it's name.
 446       * The imageurl is the location of the image for the conversation, as might be seen on a listing of conversations for a user.
 447       *
 448       * @param array $conversations a list of conversations records.
 449       * @return array the array of subnames, index by conversation id.
 450       * @throws \coding_exception
 451       * @throws \dml_exception
 452       */
 453      protected static function get_linked_conversation_extra_fields(array $conversations) : array {
 454          global $DB, $PAGE;
 455  
 456          $renderer = $PAGE->get_renderer('core');
 457  
 458          $linkedconversations = [];
 459          foreach ($conversations as $conversation) {
 460              if (!is_null($conversation->component) && !is_null($conversation->itemtype)) {
 461                  $linkedconversations[$conversation->component][$conversation->itemtype][$conversation->id]
 462                      = $conversation->itemid;
 463              }
 464          }
 465          if (empty($linkedconversations)) {
 466              return [];
 467          }
 468  
 469          // TODO: MDL-63814: Working out the subname for linked conversations should be done in a generic way.
 470          // Get the itemid, but only for course group linked conversation for now.
 471          $extrafields = [];
 472          if (!empty($linkeditems = $linkedconversations['core_group']['groups'])) { // Format: [conversationid => itemid].
 473              // Get the name of the course to which the group belongs.
 474              list ($groupidsql, $groupidparams) = $DB->get_in_or_equal(array_values($linkeditems), SQL_PARAMS_NAMED, 'groupid');
 475              $sql = "SELECT g.*, c.shortname as courseshortname
 476                        FROM {groups} g
 477                        JOIN {course} c
 478                          ON g.courseid = c.id
 479                       WHERE g.id $groupidsql";
 480              $courseinfo = $DB->get_records_sql($sql, $groupidparams);
 481              foreach ($linkeditems as $convid => $groupid) {
 482                  if (array_key_exists($groupid, $courseinfo)) {
 483                      $group = $courseinfo[$groupid];
 484                      // Subname.
 485                      $extrafields[$convid]['subname'] = format_string($courseinfo[$groupid]->courseshortname);
 486  
 487                      // Imageurl.
 488                      $extrafields[$convid]['imageurl'] = $renderer->image_url('g/g1')->out(false); // default image.
 489                      if ($url = get_group_picture_url($group, $group->courseid, true)) {
 490                          $extrafields[$convid]['imageurl'] = $url->out(false);
 491                      }
 492                  }
 493              }
 494          }
 495          return $extrafields;
 496      }
 497  
 498  
 499      /**
 500       * Returns the contacts and their conversation to display in the contacts area.
 501       *
 502       * ** WARNING **
 503       * It is HIGHLY recommended to use a sensible limit when calling this function. Trying
 504       * to retrieve too much information in a single call will cause performance problems.
 505       * ** WARNING **
 506       *
 507       * This function has specifically been altered to break each of the data sets it
 508       * requires into separate database calls. This is to avoid the performance problems
 509       * observed when attempting to join large data sets (e.g. the message tables and
 510       * the user table).
 511       *
 512       * While it is possible to gather the data in a single query, and it may even be
 513       * more efficient with a correctly tuned database, we have opted to trade off some of
 514       * the benefits of a single query in order to ensure this function will work on
 515       * most databases with default tunings and with large data sets.
 516       *
 517       * @param int $userid The user id
 518       * @param int $limitfrom
 519       * @param int $limitnum
 520       * @param int $type the type of the conversation, if you wish to filter to a certain type (see api constants).
 521       * @param bool $favourites whether to include NO favourites (false) or ONLY favourites (true), or null to ignore this setting.
 522       * @param bool $mergeself whether to include self-conversations (true) or ONLY private conversations (false)
 523       *             when private conversations are requested.
 524       * @return array the array of conversations
 525       * @throws \moodle_exception
 526       */
 527      public static function get_conversations($userid, $limitfrom = 0, $limitnum = 20, int $type = null,
 528              bool $favourites = null, bool $mergeself = false) {
 529          global $DB;
 530  
 531          if (!is_null($type) && !in_array($type, [self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
 532                  self::MESSAGE_CONVERSATION_TYPE_GROUP, self::MESSAGE_CONVERSATION_TYPE_SELF])) {
 533              throw new \moodle_exception("Invalid value ($type) for type param, please see api constants.");
 534          }
 535  
 536          self::lazy_create_self_conversation($userid);
 537  
 538          // We need to know which conversations are favourites, so we can either:
 539          // 1) Include the 'isfavourite' attribute on conversations (when $favourite = null and we're including all conversations)
 540          // 2) Restrict the results to ONLY those conversations which are favourites (when $favourite = true)
 541          // 3) Restrict the results to ONLY those conversations which are NOT favourites (when $favourite = false).
 542          $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
 543          $favouriteconversations = $service->find_favourites_by_type('core_message', 'message_conversations');
 544          $favouriteconversationids = array_column($favouriteconversations, 'itemid');
 545          if ($favourites && empty($favouriteconversationids)) {
 546              return []; // If we are aiming to return ONLY favourites, and we have none, there's nothing more to do.
 547          }
 548  
 549          // CONVERSATIONS AND MOST RECENT MESSAGE.
 550          // Include those conversations with messages first (ordered by most recent message, desc), then add any conversations which
 551          // don't have messages, such as newly created group conversations.
 552          // Because we're sorting by message 'timecreated', those conversations without messages could be at either the start or the
 553          // end of the results (behaviour for sorting of nulls differs between DB vendors), so we use the case to presort these.
 554  
 555          // If we need to return ONLY favourites, or NO favourites, generate the SQL snippet.
 556          $favouritesql = "";
 557          $favouriteparams = [];
 558          if (null !== $favourites && !empty($favouriteconversationids)) {
 559              list ($insql, $favouriteparams) =
 560                      $DB->get_in_or_equal($favouriteconversationids, SQL_PARAMS_NAMED, 'favouriteids', $favourites);
 561              $favouritesql = " AND mc.id {$insql} ";
 562          }
 563  
 564          // If we need to restrict type, generate the SQL snippet.
 565          $typesql = "";
 566          $typeparams = [];
 567          if (!is_null($type)) {
 568              if ($mergeself && $type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
 569                  // When $megerself is set to true, the self-conversations are returned also with the private conversations.
 570                  $typesql = " AND (mc.type = :convtype1 OR mc.type = :convtype2) ";
 571                  $typeparams = ['convtype1' => $type, 'convtype2' => self::MESSAGE_CONVERSATION_TYPE_SELF];
 572              } else {
 573                  $typesql = " AND mc.type = :convtype ";
 574                  $typeparams = ['convtype' => $type];
 575              }
 576          }
 577  
 578          $sql = "SELECT m.id as messageid, mc.id as id, mc.name as conversationname, mc.type as conversationtype, m.useridfrom,
 579                         m.smallmessage, m.fullmessage, m.fullmessageformat, m.fullmessagetrust, m.fullmessagehtml, m.timecreated,
 580                         mc.component, mc.itemtype, mc.itemid, mc.contextid, mca.action as ismuted
 581                    FROM {message_conversations} mc
 582              INNER JOIN {message_conversation_members} mcm
 583                      ON (mcm.conversationid = mc.id AND mcm.userid = :userid3)
 584              LEFT JOIN (
 585                            SELECT m.conversationid, MAX(m.id) AS messageid
 586                              FROM {messages} m
 587                        INNER JOIN (
 588                                        SELECT m.conversationid, MAX(m.timecreated) as maxtime
 589                                          FROM {messages} m
 590                                    INNER JOIN {message_conversation_members} mcm
 591                                            ON mcm.conversationid = m.conversationid
 592                                     LEFT JOIN {message_user_actions} mua
 593                                            ON (mua.messageid = m.id AND mua.userid = :userid AND mua.action = :action)
 594                                         WHERE mua.id is NULL
 595                                           AND mcm.userid = :userid2
 596                                      GROUP BY m.conversationid
 597                                   ) maxmessage
 598                                 ON maxmessage.maxtime = m.timecreated AND maxmessage.conversationid = m.conversationid
 599                           GROUP BY m.conversationid
 600                         ) lastmessage
 601                      ON lastmessage.conversationid = mc.id
 602              LEFT JOIN {messages} m
 603                     ON m.id = lastmessage.messageid
 604              LEFT JOIN {message_conversation_actions} mca
 605                     ON (mca.conversationid = mc.id AND mca.userid = :userid4 AND mca.action = :convaction)
 606                  WHERE mc.id IS NOT NULL
 607                    AND mc.enabled = 1 $typesql $favouritesql
 608                ORDER BY (CASE WHEN m.timecreated IS NULL THEN 0 ELSE 1 END) DESC, m.timecreated DESC, id DESC";
 609  
 610          $params = array_merge($favouriteparams, $typeparams, ['userid' => $userid, 'action' => self::MESSAGE_ACTION_DELETED,
 611              'userid2' => $userid, 'userid3' => $userid, 'userid4' => $userid, 'convaction' => self::CONVERSATION_ACTION_MUTED]);
 612          $conversationset = $DB->get_recordset_sql($sql, $params, $limitfrom, $limitnum);
 613  
 614          $conversations = [];
 615          $selfconversations = []; // Used to track conversations with one's self.
 616          $members = [];
 617          $individualmembers = [];
 618          $groupmembers = [];
 619          $selfmembers = [];
 620          foreach ($conversationset as $conversation) {
 621              $conversations[$conversation->id] = $conversation;
 622              $members[$conversation->id] = [];
 623          }
 624          $conversationset->close();
 625  
 626          // If there are no conversations found, then return early.
 627          if (empty($conversations)) {
 628              return [];
 629          }
 630  
 631          // COMPONENT-LINKED CONVERSATION FIELDS.
 632          // Conversations linked to components may have extra information, such as:
 633          // - subname: Essentially a subtitle for the conversation. So you'd have "name: subname".
 634          // - imageurl: A URL to the image for the linked conversation.
 635          // For now, this is ONLY course groups.
 636          $convextrafields = self::get_linked_conversation_extra_fields($conversations);
 637  
 638          // MEMBERS.
 639          // Ideally, we want to get 1 member for each conversation, but this depends on the type and whether there is a recent
 640          // message or not.
 641          //
 642          // For 'individual' type conversations between 2 users, regardless of who sent the last message,
 643          // we want the details of the other member in the conversation (i.e. not the current user).
 644          //
 645          // For 'group' type conversations, we want the details of the member who sent the last message, if there is one.
 646          // This can be the current user or another group member, but for groups without messages, this will be empty.
 647          //
 648          // For 'self' type conversations, we want the details of the current user.
 649          //
 650          // This also means that if type filtering is specified and only group conversations are returned, we don't need this extra
 651          // query to get the 'other' user as we already have that information.
 652  
 653          // Work out which members we have already, and which ones we might need to fetch.
 654          // If all the last messages were from another user, then we don't need to fetch anything further.
 655          foreach ($conversations as $conversation) {
 656              if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
 657                  if (!is_null($conversation->useridfrom) && $conversation->useridfrom != $userid) {
 658                      $members[$conversation->id][$conversation->useridfrom] = $conversation->useridfrom;
 659                      $individualmembers[$conversation->useridfrom] = $conversation->useridfrom;
 660                  } else {
 661                      $individualconversations[] = $conversation->id;
 662                  }
 663              } else if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
 664                  // If we have a recent message, the sender is our member.
 665                  if (!is_null($conversation->useridfrom)) {
 666                      $members[$conversation->id][$conversation->useridfrom] = $conversation->useridfrom;
 667                      $groupmembers[$conversation->useridfrom] = $conversation->useridfrom;
 668                  }
 669              } else if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_SELF) {
 670                  $selfconversations[$conversation->id] = $conversation->id;
 671                  $members[$conversation->id][$userid] = $userid;
 672                  $selfmembers[$userid] = $userid;
 673              }
 674          }
 675          // If we need to fetch any member information for any of the individual conversations.
 676          // This is the case if any of the individual conversations have a recent message sent by the current user.
 677          if (!empty($individualconversations)) {
 678              list ($icidinsql, $icidinparams) = $DB->get_in_or_equal($individualconversations, SQL_PARAMS_NAMED, 'convid');
 679              $indmembersql = "SELECT mcm.id, mcm.conversationid, mcm.userid
 680                          FROM {message_conversation_members} mcm
 681                         WHERE mcm.conversationid $icidinsql
 682                         AND mcm.userid != :userid
 683                         ORDER BY mcm.id";
 684              $indmemberparams = array_merge($icidinparams, ['userid' => $userid]);
 685              $conversationmembers = $DB->get_records_sql($indmembersql, $indmemberparams);
 686  
 687              foreach ($conversationmembers as $mid => $member) {
 688                  $members[$member->conversationid][$member->userid] = $member->userid;
 689                  $individualmembers[$member->userid] = $member->userid;
 690              }
 691          }
 692  
 693          // We could fail early here if we're sure that:
 694          // a) we have no otherusers for all the conversations (users may have been deleted)
 695          // b) we're sure that all conversations are individual (1:1).
 696  
 697          // We need to pull out the list of users info corresponding to the memberids in the conversations.This
 698          // needs to be done in a separate query to avoid doing a join on the messages tables and the user
 699          // tables because on large sites these tables are massive which results in extremely slow
 700          // performance (typically due to join buffer exhaustion).
 701          if (!empty($individualmembers) || !empty($groupmembers) || !empty($selfmembers)) {
 702              // Now, we want to remove any duplicates from the group members array. For individual members we will
 703              // be doing a more extensive call as we want their contact requests as well as privacy information,
 704              // which is not necessary for group conversations.
 705              $diffgroupmembers = array_diff($groupmembers, $individualmembers);
 706  
 707              $individualmemberinfo = helper::get_member_info($userid, $individualmembers, true, true);
 708              $groupmemberinfo = helper::get_member_info($userid, $diffgroupmembers);
 709              $selfmemberinfo = helper::get_member_info($userid, $selfmembers);
 710  
 711              // Don't use array_merge, as we lose array keys.
 712              $memberinfo = $individualmemberinfo + $groupmemberinfo + $selfmemberinfo;
 713  
 714              if (empty($memberinfo)) {
 715                  return [];
 716              }
 717  
 718              // Update the members array with the member information.
 719              $deletedmembers = [];
 720              foreach ($members as $convid => $memberarr) {
 721                  foreach ($memberarr as $key => $memberid) {
 722                      if (array_key_exists($memberid, $memberinfo)) {
 723                          // If the user is deleted, remember that.
 724                          if ($memberinfo[$memberid]->isdeleted) {
 725                              $deletedmembers[$convid][] = $memberid;
 726                          }
 727  
 728                          $members[$convid][$key] = clone $memberinfo[$memberid];
 729  
 730                          if ($conversations[$convid]->conversationtype == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
 731                              // Remove data we don't need for group.
 732                              $members[$convid][$key]->requirescontact = null;
 733                              $members[$convid][$key]->canmessage = null;
 734                              $members[$convid][$key]->contactrequests = [];
 735                          }
 736                      } else { // Remove all members and individual conversations where we could not get the member's information.
 737                          unset($members[$convid][$key]);
 738  
 739                          // If the conversation is an individual conversation, then we should remove it from the list.
 740                          if ($conversations[$convid]->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
 741                              unset($conversations[$convid]);
 742                          }
 743                      }
 744                  }
 745              }
 746          }
 747  
 748          // MEMBER COUNT.
 749          $cids = array_column($conversations, 'id');
 750          list ($cidinsql, $cidinparams) = $DB->get_in_or_equal($cids, SQL_PARAMS_NAMED, 'convid');
 751          $membercountsql = "SELECT conversationid, count(DISTINCT userid) AS membercount
 752                               FROM {message_conversation_members} mcm
 753                              WHERE mcm.conversationid $cidinsql
 754                           GROUP BY mcm.conversationid";
 755          $membercounts = $DB->get_records_sql($membercountsql, $cidinparams);
 756  
 757          // UNREAD MESSAGE COUNT.
 758          // Finally, let's get the unread messages count for this user so that we can add it
 759          // to the conversation. Remember we need to ignore the messages the user sent.
 760          $unreadcountssql = 'SELECT m.conversationid, count(m.id) as unreadcount
 761                                FROM {messages} m
 762                          INNER JOIN {message_conversations} mc
 763                                  ON mc.id = m.conversationid
 764                          INNER JOIN {message_conversation_members} mcm
 765                                  ON m.conversationid = mcm.conversationid
 766                           LEFT JOIN {message_user_actions} mua
 767                                  ON (mua.messageid = m.id AND mua.userid = ? AND
 768                                     (mua.action = ? OR mua.action = ?))
 769                               WHERE mcm.userid = ?
 770                                 AND m.useridfrom != ?
 771                                 AND mua.id is NULL
 772                            GROUP BY m.conversationid';
 773          $unreadcounts = $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, self::MESSAGE_ACTION_DELETED,
 774              $userid, $userid]);
 775  
 776          // For the self-conversations, get the total number of messages (to know if the conversation is new or it has been emptied).
 777          $selfmessagessql = "SELECT COUNT(m.id)
 778                                FROM {messages} m
 779                          INNER JOIN {message_conversations} mc
 780                                  ON mc.id = m.conversationid
 781                               WHERE mc.type = ? AND convhash = ?";
 782          $selfmessagestotal = $DB->count_records_sql(
 783              $selfmessagessql,
 784              [self::MESSAGE_CONVERSATION_TYPE_SELF, helper::get_conversation_hash([$userid])]
 785          );
 786  
 787          // Because we'll be calling format_string on each conversation name and passing contexts, we preload them here.
 788          // This warms the cache and saves potentially hitting the DB once for each context fetch below.
 789          \context_helper::preload_contexts_by_id(array_column($conversations, 'contextid'));
 790  
 791          // Now, create the final return structure.
 792          $arrconversations = [];
 793          foreach ($conversations as $conversation) {
 794              // Do not include any individual which do not contain a recent message for the user.
 795              // This happens if the user has deleted all messages.
 796              // Exclude the self-conversations with messages but without a recent message because the user has deleted all them.
 797              // Self-conversations without any message should be included, to display them first time they are created.
 798              // Group conversations with deleted users or no messages are always returned.
 799              if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL && empty($conversation->messageid) ||
 800                     ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_SELF && empty($conversation->messageid)
 801                      && $selfmessagestotal > 0)) {
 802                  continue;
 803              }
 804  
 805              $conv = new \stdClass();
 806              $conv->id = $conversation->id;
 807  
 808              // Name should be formatted and depends on the context the conversation resides in.
 809              // If not set, the context is always context_user.
 810              if (is_null($conversation->contextid)) {
 811                  $convcontext = \context_user::instance($userid);
 812                  // We'll need to check the capability to delete messages for all users in context system when contextid is null.
 813                  $contexttodeletemessageforall = \context_system::instance();
 814              } else {
 815                  $convcontext = \context::instance_by_id($conversation->contextid);
 816                  $contexttodeletemessageforall = $convcontext;
 817              }
 818              $conv->name = format_string($conversation->conversationname, true, ['context' => $convcontext]);
 819  
 820              $conv->subname = $convextrafields[$conv->id]['subname'] ?? null;
 821              $conv->imageurl = $convextrafields[$conv->id]['imageurl'] ?? null;
 822              $conv->type = $conversation->conversationtype;
 823              $conv->membercount = $membercounts[$conv->id]->membercount;
 824              $conv->isfavourite = in_array($conv->id, $favouriteconversationids);
 825              $conv->isread = isset($unreadcounts[$conv->id]) ? false : true;
 826              $conv->unreadcount = isset($unreadcounts[$conv->id]) ? $unreadcounts[$conv->id]->unreadcount : null;
 827              $conv->ismuted = $conversation->ismuted ? true : false;
 828              $conv->members = $members[$conv->id];
 829  
 830              // Add the most recent message information.
 831              $conv->messages = [];
 832              // Add if the user has to allow delete messages for all users in the conversation.
 833              $conv->candeletemessagesforallusers = has_capability('moodle/site:deleteanymessage',  $contexttodeletemessageforall);
 834              if ($conversation->smallmessage) {
 835                  $msg = new \stdClass();
 836                  $msg->id = $conversation->messageid;
 837                  $msg->text = message_format_message_text($conversation);
 838                  $msg->useridfrom = $conversation->useridfrom;
 839                  $msg->timecreated = $conversation->timecreated;
 840                  $conv->messages[] = $msg;
 841              }
 842  
 843              $arrconversations[] = $conv;
 844          }
 845          return $arrconversations;
 846      }
 847  
 848      /**
 849       * Returns all conversations between two users
 850       *
 851       * @param int $userid1 One of the user's id
 852       * @param int $userid2 The other user's id
 853       * @param int $limitfrom
 854       * @param int $limitnum
 855       * @return array
 856       * @throws \dml_exception
 857       */
 858      public static function get_conversations_between_users(int $userid1, int $userid2,
 859                                                             int $limitfrom = 0, int $limitnum = 20) : array {
 860  
 861          global $DB;
 862  
 863          if ($userid1 == $userid2) {
 864              return array();
 865          }
 866  
 867          // Get all conversation where both user1 and user2 are members.
 868          // TODO: Add subname value. Waiting for definite table structure.
 869          $sql = "SELECT mc.id, mc.type, mc.name, mc.timecreated
 870                    FROM {message_conversations} mc
 871              INNER JOIN {message_conversation_members} mcm1
 872                      ON mc.id = mcm1.conversationid
 873              INNER JOIN {message_conversation_members} mcm2
 874                      ON mc.id = mcm2.conversationid
 875                   WHERE mcm1.userid = :userid1
 876                     AND mcm2.userid = :userid2
 877                     AND mc.enabled != 0
 878                ORDER BY mc.timecreated DESC";
 879  
 880          return $DB->get_records_sql($sql, array('userid1' => $userid1, 'userid2' => $userid2), $limitfrom, $limitnum);
 881      }
 882  
 883      /**
 884       * Return a conversation.
 885       *
 886       * @param int $userid The user id to get the conversation for
 887       * @param int $conversationid The id of the conversation to fetch
 888       * @param bool $includecontactrequests Should contact requests be included between members
 889       * @param bool $includeprivacyinfo Should privacy info be included between members
 890       * @param int $memberlimit Limit number of members to load
 891       * @param int $memberoffset Offset members by this amount
 892       * @param int $messagelimit Limit number of messages to load
 893       * @param int $messageoffset Offset the messages
 894       * @param bool $newestmessagesfirst Order messages by newest first
 895       * @return \stdClass
 896       */
 897      public static function get_conversation(
 898          int $userid,
 899          int $conversationid,
 900          bool $includecontactrequests = false,
 901          bool $includeprivacyinfo = false,
 902          int $memberlimit = 0,
 903          int $memberoffset = 0,
 904          int $messagelimit = 0,
 905          int $messageoffset = 0,
 906          bool $newestmessagesfirst = true
 907      ) {
 908          global $USER, $DB;
 909  
 910          $systemcontext = \context_system::instance();
 911          $canreadallmessages = has_capability('moodle/site:readallmessages', $systemcontext);
 912          if (($USER->id != $userid) && !$canreadallmessages) {
 913              throw new \moodle_exception('You do not have permission to perform this action.');
 914          }
 915  
 916          $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]);
 917          if (!$conversation) {
 918              return null;
 919          }
 920  
 921          // Get the context of the conversation. This will be used to check whether the conversation is a favourite.
 922          // This will be either 'user' (for individual conversations) or, in the case of linked conversations,
 923          // the context stored in the record.
 924          $userctx = \context_user::instance($userid);
 925          $conversationctx = empty($conversation->contextid) ? $userctx : \context::instance_by_id($conversation->contextid);
 926  
 927          $isconversationmember = $DB->record_exists(
 928              'message_conversation_members',
 929              [
 930                  'conversationid' => $conversationid,
 931                  'userid' => $userid
 932              ]
 933          );
 934  
 935          if (!$isconversationmember && !$canreadallmessages) {
 936              throw new \moodle_exception('You do not have permission to view this conversation.');
 937          }
 938  
 939          $members = self::get_conversation_members(
 940              $userid,
 941              $conversationid,
 942              $includecontactrequests,
 943              $includeprivacyinfo,
 944              $memberoffset,
 945              $memberlimit
 946          );
 947          if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_SELF) {
 948              // Strip out the requesting user to match what get_conversations does, except for self-conversations.
 949              $members = array_filter($members, function($member) use ($userid) {
 950                  return $member->id != $userid;
 951              });
 952          }
 953  
 954          $messages = self::get_conversation_messages(
 955              $userid,
 956              $conversationid,
 957              $messageoffset,
 958              $messagelimit,
 959              $newestmessagesfirst ? 'timecreated DESC' : 'timecreated ASC'
 960          );
 961  
 962          $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
 963          $isfavourite = $service->favourite_exists('core_message', 'message_conversations', $conversationid, $conversationctx);
 964  
 965          $convextrafields = self::get_linked_conversation_extra_fields([$conversation]);
 966          $subname = isset($convextrafields[$conversationid]) ? $convextrafields[$conversationid]['subname'] : null;
 967          $imageurl = isset($convextrafields[$conversationid]) ? $convextrafields[$conversationid]['imageurl'] : null;
 968  
 969          $unreadcountssql = 'SELECT count(m.id)
 970                                FROM {messages} m
 971                          INNER JOIN {message_conversations} mc
 972                                  ON mc.id = m.conversationid
 973                           LEFT JOIN {message_user_actions} mua
 974                                  ON (mua.messageid = m.id AND mua.userid = ? AND
 975                                     (mua.action = ? OR mua.action = ?))
 976                               WHERE m.conversationid = ?
 977                                 AND m.useridfrom != ?
 978                                 AND mua.id is NULL';
 979          $unreadcount = $DB->count_records_sql(
 980              $unreadcountssql,
 981              [
 982                  $userid,
 983                  self::MESSAGE_ACTION_READ,
 984                  self::MESSAGE_ACTION_DELETED,
 985                  $conversationid,
 986                  $userid
 987              ]
 988          );
 989  
 990          $membercount = $DB->count_records('message_conversation_members', ['conversationid' => $conversationid]);
 991  
 992          $ismuted = false;
 993          if ($DB->record_exists('message_conversation_actions', ['userid' => $userid,
 994                  'conversationid' => $conversationid, 'action' => self::CONVERSATION_ACTION_MUTED])) {
 995              $ismuted = true;
 996          }
 997  
 998          // Get the context of the conversation. This will be used to check if the user can delete all messages in the conversation.
 999          $deleteallcontext = empty($conversation->contextid) ? $systemcontext : \context::instance_by_id($conversation->contextid);
1000  
1001          return (object) [
1002              'id' => $conversation->id,
1003              'name' => $conversation->name,
1004              'subname' => $subname,
1005              'imageurl' => $imageurl,
1006              'type' => $conversation->type,
1007              'membercount' => $membercount,
1008              'isfavourite' => $isfavourite,
1009              'isread' => empty($unreadcount),
1010              'unreadcount' => $unreadcount,
1011              'ismuted' => $ismuted,
1012              'members' => $members,
1013              'messages' => $messages['messages'],
1014              'candeletemessagesforallusers' => has_capability('moodle/site:deleteanymessage', $deleteallcontext)
1015          ];
1016      }
1017  
1018      /**
1019       * Mark a conversation as a favourite for the given user.
1020       *
1021       * @param int $conversationid the id of the conversation to mark as a favourite.
1022       * @param int $userid the id of the user to whom the favourite belongs.
1023       * @return favourite the favourite object.
1024       * @throws \moodle_exception if the user or conversation don't exist.
1025       */
1026      public static function set_favourite_conversation(int $conversationid, int $userid) : favourite {
1027          global $DB;
1028  
1029          if (!self::is_user_in_conversation($userid, $conversationid)) {
1030              throw new \moodle_exception("Conversation doesn't exist or user is not a member");
1031          }
1032          // Get the context for this conversation.
1033          $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]);
1034          $userctx = \context_user::instance($userid);
1035          if (empty($conversation->contextid)) {
1036              // When the conversation hasn't any contextid value defined, the favourite will be added to the user context.
1037              $conversationctx = $userctx;
1038          } else {
1039              // If the contextid is defined, the favourite will be added there.
1040              $conversationctx = \context::instance_by_id($conversation->contextid);
1041          }
1042  
1043          $ufservice = \core_favourites\service_factory::get_service_for_user_context($userctx);
1044  
1045          if ($favourite = $ufservice->get_favourite('core_message', 'message_conversations', $conversationid, $conversationctx)) {
1046              return $favourite;
1047          } else {
1048              return $ufservice->create_favourite('core_message', 'message_conversations', $conversationid, $conversationctx);
1049          }
1050      }
1051  
1052      /**
1053       * Unset a conversation as a favourite for the given user.
1054       *
1055       * @param int $conversationid the id of the conversation to unset as a favourite.
1056       * @param int $userid the id to whom the favourite belongs.
1057       * @throws \moodle_exception if the favourite does not exist for the user.
1058       */
1059      public static function unset_favourite_conversation(int $conversationid, int $userid) {
1060          global $DB;
1061  
1062          // Get the context for this conversation.
1063          $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]);
1064          $userctx = \context_user::instance($userid);
1065          if (empty($conversation->contextid)) {
1066              // When the conversation hasn't any contextid value defined, the favourite will be added to the user context.
1067              $conversationctx = $userctx;
1068          } else {
1069              // If the contextid is defined, the favourite will be added there.
1070              $conversationctx = \context::instance_by_id($conversation->contextid);
1071          }
1072  
1073          $ufservice = \core_favourites\service_factory::get_service_for_user_context($userctx);
1074          $ufservice->delete_favourite('core_message', 'message_conversations', $conversationid, $conversationctx);
1075      }
1076  
1077      /**
1078       * Returns the contacts to display in the contacts area.
1079       *
1080       * TODO: This function should be removed once the related web service goes through final deprecation.
1081       * The related web service is data_for_messagearea_contacts.
1082       * Followup: MDL-63261
1083       *
1084       * @param int $userid The user id
1085       * @param int $limitfrom
1086       * @param int $limitnum
1087       * @return array
1088       */
1089      public static function get_contacts($userid, $limitfrom = 0, $limitnum = 0) {
1090          global $DB;
1091  
1092          $contactids = [];
1093          $sql = "SELECT mc.*
1094                    FROM {message_contacts} mc
1095                   WHERE mc.userid = ? OR mc.contactid = ?
1096                ORDER BY timecreated DESC";
1097          if ($contacts = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
1098              foreach ($contacts as $contact) {
1099                  if ($userid == $contact->userid) {
1100                      $contactids[] = $contact->contactid;
1101                  } else {
1102                      $contactids[] = $contact->userid;
1103                  }
1104              }
1105          }
1106  
1107          if (!empty($contactids)) {
1108              list($insql, $inparams) = $DB->get_in_or_equal($contactids);
1109  
1110              $sql = "SELECT u.*, mub.id as isblocked
1111                        FROM {user} u
1112                   LEFT JOIN {message_users_blocked} mub
1113                          ON u.id = mub.blockeduserid
1114                       WHERE u.id $insql";
1115              if ($contacts = $DB->get_records_sql($sql, $inparams)) {
1116                  $arrcontacts = [];
1117                  foreach ($contacts as $contact) {
1118                      $contact->blocked = $contact->isblocked ? 1 : 0;
1119                      $arrcontacts[] = helper::create_contact($contact);
1120                  }
1121  
1122                  return $arrcontacts;
1123              }
1124          }
1125  
1126          return [];
1127      }
1128  
1129      /**
1130       * Get the contacts for a given user.
1131       *
1132       * @param int $userid
1133       * @param int $limitfrom
1134       * @param int $limitnum
1135       * @return array An array of contacts
1136       */
1137      public static function get_user_contacts(int $userid, int $limitfrom = 0, int $limitnum = 0) {
1138          global $DB;
1139  
1140          $sql = "SELECT *
1141                    FROM {message_contacts} mc
1142                   WHERE mc.userid = ? OR mc.contactid = ?
1143                ORDER BY timecreated DESC, id ASC";
1144          if ($contacts = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
1145              $userids = [];
1146              foreach ($contacts as $contact) {
1147                  if ($contact->userid == $userid) {
1148                      $userids[] = $contact->contactid;
1149                  } else {
1150                      $userids[] = $contact->userid;
1151                  }
1152              }
1153              return helper::get_member_info($userid, $userids);
1154          }
1155  
1156          return [];
1157      }
1158  
1159      /**
1160       * Returns the contacts count.
1161       *
1162       * @param int $userid The user id
1163       * @return array
1164       */
1165      public static function count_contacts(int $userid) : int {
1166          global $DB;
1167  
1168          $sql = "SELECT COUNT(id)
1169                    FROM {message_contacts}
1170                   WHERE userid = ? OR contactid = ?";
1171          return $DB->count_records_sql($sql, [$userid, $userid]);
1172      }
1173  
1174      /**
1175       * Returns the an array of the users the given user is in a conversation
1176       * with who are a contact and the number of unread messages.
1177       *
1178       * @param int $userid The user id
1179       * @param int $limitfrom
1180       * @param int $limitnum
1181       * @return array
1182       */
1183      public static function get_contacts_with_unread_message_count($userid, $limitfrom = 0, $limitnum = 0) {
1184          global $DB;
1185  
1186          $userfields = \user_picture::fields('u', array('lastaccess'));
1187          $unreadcountssql = "SELECT $userfields, count(m.id) as messagecount
1188                                FROM {message_contacts} mc
1189                          INNER JOIN {user} u
1190                                  ON (u.id = mc.contactid OR u.id = mc.userid)
1191                           LEFT JOIN {messages} m
1192                                  ON ((m.useridfrom = mc.contactid OR m.useridfrom = mc.userid) AND m.useridfrom != ?)
1193                           LEFT JOIN {message_conversation_members} mcm
1194                                  ON mcm.conversationid = m.conversationid AND mcm.userid = ? AND mcm.userid != m.useridfrom
1195                           LEFT JOIN {message_user_actions} mua
1196                                  ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1197                           LEFT JOIN {message_users_blocked} mub
1198                                  ON (mub.userid = ? AND mub.blockeduserid = u.id)
1199                               WHERE mua.id is NULL
1200                                 AND mub.id is NULL
1201                                 AND (mc.userid = ? OR mc.contactid = ?)
1202                                 AND u.id != ?
1203                                 AND u.deleted = 0
1204                            GROUP BY $userfields";
1205  
1206          return $DB->get_records_sql($unreadcountssql, [$userid, $userid, $userid, self::MESSAGE_ACTION_READ,
1207              $userid, $userid, $userid, $userid], $limitfrom, $limitnum);
1208      }
1209  
1210      /**
1211       * Returns the an array of the users the given user is in a conversation
1212       * with who are not a contact and the number of unread messages.
1213       *
1214       * @param int $userid The user id
1215       * @param int $limitfrom
1216       * @param int $limitnum
1217       * @return array
1218       */
1219      public static function get_non_contacts_with_unread_message_count($userid, $limitfrom = 0, $limitnum = 0) {
1220          global $DB;
1221  
1222          $userfields = \user_picture::fields('u', array('lastaccess'));
1223          $unreadcountssql = "SELECT $userfields, count(m.id) as messagecount
1224                                FROM {user} u
1225                          INNER JOIN {messages} m
1226                                  ON m.useridfrom = u.id
1227                          INNER JOIN {message_conversation_members} mcm
1228                                  ON mcm.conversationid = m.conversationid
1229                           LEFT JOIN {message_user_actions} mua
1230                                  ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1231                           LEFT JOIN {message_contacts} mc
1232                                  ON (mc.userid = ? AND mc.contactid = u.id)
1233                           LEFT JOIN {message_users_blocked} mub
1234                                  ON (mub.userid = ? AND mub.blockeduserid = u.id)
1235                               WHERE mcm.userid = ?
1236                                 AND mcm.userid != m.useridfrom
1237                                 AND mua.id is NULL
1238                                 AND mub.id is NULL
1239                                 AND mc.id is NULL
1240                                 AND u.deleted = 0
1241                            GROUP BY $userfields";
1242  
1243          return $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, $userid, $userid, $userid],
1244              $limitfrom, $limitnum);
1245      }
1246  
1247      /**
1248       * Returns the messages to display in the message area.
1249       *
1250       * TODO: This function should be removed once the related web service goes through final deprecation.
1251       * The related web service is data_for_messagearea_messages.
1252       * Followup: MDL-63261
1253       *
1254       * @param int $userid the current user
1255       * @param int $otheruserid the other user
1256       * @param int $limitfrom
1257       * @param int $limitnum
1258       * @param string $sort
1259       * @param int $timefrom the time from the message being sent
1260       * @param int $timeto the time up until the message being sent
1261       * @return array
1262       */
1263      public static function get_messages($userid, $otheruserid, $limitfrom = 0, $limitnum = 0,
1264              $sort = 'timecreated ASC', $timefrom = 0, $timeto = 0) {
1265  
1266          if (!empty($timefrom)) {
1267              // Get the conversation between userid and otheruserid.
1268              $userids = [$userid, $otheruserid];
1269              if (!$conversationid = self::get_conversation_between_users($userids)) {
1270                  // This method was always used for individual conversations.
1271                  $conversation = self::create_conversation(self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, $userids);
1272                  $conversationid = $conversation->id;
1273              }
1274  
1275              // Check the cache to see if we even need to do a DB query.
1276              $cache = \cache::make('core', 'message_time_last_message_between_users');
1277              $key = helper::get_last_message_time_created_cache_key($conversationid);
1278              $lastcreated = $cache->get($key);
1279  
1280              // The last known message time is earlier than the one being requested so we can
1281              // just return an empty result set rather than having to query the DB.
1282              if ($lastcreated && $lastcreated < $timefrom) {
1283                  return [];
1284              }
1285          }
1286  
1287          $arrmessages = array();
1288          if ($messages = helper::get_messages($userid, $otheruserid, 0, $limitfrom, $limitnum,
1289                                               $sort, $timefrom, $timeto)) {
1290              $arrmessages = helper::create_messages($userid, $messages);
1291          }
1292  
1293          return $arrmessages;
1294      }
1295  
1296      /**
1297       * Returns the messages for the defined conversation.
1298       *
1299       * @param  int $userid The current user.
1300       * @param  int $convid The conversation where the messages belong. Could be an object or just the id.
1301       * @param  int $limitfrom Return a subset of records, starting at this point (optional).
1302       * @param  int $limitnum Return a subset comprising this many records in total (optional, required if $limitfrom is set).
1303       * @param  string $sort The column name to order by including optionally direction.
1304       * @param  int $timefrom The time from the message being sent.
1305       * @param  int $timeto The time up until the message being sent.
1306       * @return array of messages
1307       */
1308      public static function get_conversation_messages(int $userid, int $convid, int $limitfrom = 0, int $limitnum = 0,
1309          string $sort = 'timecreated ASC', int $timefrom = 0, int $timeto = 0) : array {
1310  
1311          if (!empty($timefrom)) {
1312              // Check the cache to see if we even need to do a DB query.
1313              $cache = \cache::make('core', 'message_time_last_message_between_users');
1314              $key = helper::get_last_message_time_created_cache_key($convid);
1315              $lastcreated = $cache->get($key);
1316  
1317              // The last known message time is earlier than the one being requested so we can
1318              // just return an empty result set rather than having to query the DB.
1319              if ($lastcreated && $lastcreated < $timefrom) {
1320                  return helper::format_conversation_messages($userid, $convid, []);
1321              }
1322          }
1323  
1324          $messages = helper::get_conversation_messages($userid, $convid, 0, $limitfrom, $limitnum, $sort, $timefrom, $timeto);
1325          return helper::format_conversation_messages($userid, $convid, $messages);
1326      }
1327  
1328      /**
1329       * Returns the most recent message between two users.
1330       *
1331       * TODO: This function should be removed once the related web service goes through final deprecation.
1332       * The related web service is data_for_messagearea_get_most_recent_message.
1333       * Followup: MDL-63261
1334       *
1335       * @param int $userid the current user
1336       * @param int $otheruserid the other user
1337       * @return \stdClass|null
1338       */
1339      public static function get_most_recent_message($userid, $otheruserid) {
1340          // We want two messages here so we get an accurate 'blocktime' value.
1341          if ($messages = helper::get_messages($userid, $otheruserid, 0, 0, 2, 'timecreated DESC')) {
1342              // Swap the order so we now have them in historical order.
1343              $messages = array_reverse($messages);
1344              $arrmessages = helper::create_messages($userid, $messages);
1345              return array_pop($arrmessages);
1346          }
1347  
1348          return null;
1349      }
1350  
1351      /**
1352       * Returns the most recent message in a conversation.
1353       *
1354       * @param int $convid The conversation identifier.
1355       * @param int $currentuserid The current user identifier.
1356       * @return \stdClass|null The most recent message.
1357       */
1358      public static function get_most_recent_conversation_message(int $convid, int $currentuserid = 0) {
1359          global $USER;
1360  
1361          if (empty($currentuserid)) {
1362              $currentuserid = $USER->id;
1363          }
1364  
1365          if ($messages = helper::get_conversation_messages($currentuserid, $convid, 0, 0, 1, 'timecreated DESC')) {
1366              $convmessages = helper::format_conversation_messages($currentuserid, $convid, $messages);
1367              return array_pop($convmessages['messages']);
1368          }
1369  
1370          return null;
1371      }
1372  
1373      /**
1374       * Returns the profile information for a contact for a user.
1375       *
1376       * TODO: This function should be removed once the related web service goes through final deprecation.
1377       * The related web service is data_for_messagearea_get_profile.
1378       * Followup: MDL-63261
1379       *
1380       * @param int $userid The user id
1381       * @param int $otheruserid The id of the user whose profile we want to view.
1382       * @return \stdClass
1383       */
1384      public static function get_profile($userid, $otheruserid) {
1385          global $CFG, $PAGE;
1386  
1387          require_once($CFG->dirroot . '/user/lib.php');
1388  
1389          $user = \core_user::get_user($otheruserid, '*', MUST_EXIST);
1390  
1391          // Create the data we are going to pass to the renderable.
1392          $data = new \stdClass();
1393          $data->userid = $otheruserid;
1394          $data->fullname = fullname($user);
1395          $data->city = '';
1396          $data->country = '';
1397          $data->email = '';
1398          $data->isonline = null;
1399          // Get the user picture data - messaging has always shown these to the user.
1400          $userpicture = new \user_picture($user);
1401          $userpicture->size = 1; // Size f1.
1402          $data->profileimageurl = $userpicture->get_url($PAGE)->out(false);
1403          $userpicture->size = 0; // Size f2.
1404          $data->profileimageurlsmall = $userpicture->get_url($PAGE)->out(false);
1405  
1406          $userfields = user_get_user_details($user, null, array('city', 'country', 'email', 'lastaccess'));
1407          if ($userfields) {
1408              if (isset($userfields['city'])) {
1409                  $data->city = $userfields['city'];
1410              }
1411              if (isset($userfields['country'])) {
1412                  $data->country = $userfields['country'];
1413              }
1414              if (isset($userfields['email'])) {
1415                  $data->email = $userfields['email'];
1416              }
1417              if (isset($userfields['lastaccess'])) {
1418                  $data->isonline = helper::is_online($userfields['lastaccess']);
1419              }
1420          }
1421  
1422          $data->isblocked = self::is_blocked($userid, $otheruserid);
1423          $data->iscontact = self::is_contact($userid, $otheruserid);
1424  
1425          return $data;
1426      }
1427  
1428      /**
1429       * Checks if a user can delete messages they have either received or sent.
1430       *
1431       * @param int $userid The user id of who we want to delete the messages for (this may be done by the admin
1432       *  but will still seem as if it was by the user)
1433       * @param int $conversationid The id of the conversation
1434       * @return bool Returns true if a user can delete the conversation, false otherwise.
1435       */
1436      public static function can_delete_conversation(int $userid, int $conversationid = null) : bool {
1437          global $USER;
1438  
1439          if (is_null($conversationid)) {
1440              debugging('\core_message\api::can_delete_conversation() now expects a \'conversationid\' to be passed.',
1441                  DEBUG_DEVELOPER);
1442              return false;
1443          }
1444  
1445          $systemcontext = \context_system::instance();
1446  
1447          if (has_capability('moodle/site:deleteanymessage', $systemcontext)) {
1448              return true;
1449          }
1450  
1451          if (!self::is_user_in_conversation($userid, $conversationid)) {
1452              return false;
1453          }
1454  
1455          if (has_capability('moodle/site:deleteownmessage', $systemcontext) &&
1456                  $USER->id == $userid) {
1457              return true;
1458          }
1459  
1460          return false;
1461      }
1462  
1463      /**
1464       * Deletes a conversation.
1465       *
1466       * This function does not verify any permissions.
1467       *
1468       * @deprecated since 3.6
1469       * @param int $userid The user id of who we want to delete the messages for (this may be done by the admin
1470       *  but will still seem as if it was by the user)
1471       * @param int $otheruserid The id of the other user in the conversation
1472       * @return bool
1473       */
1474      public static function delete_conversation($userid, $otheruserid) {
1475          debugging('\core_message\api::delete_conversation() is deprecated, please use ' .
1476              '\core_message\api::delete_conversation_by_id() instead.', DEBUG_DEVELOPER);
1477  
1478          $conversationid = self::get_conversation_between_users([$userid, $otheruserid]);
1479  
1480          // If there is no conversation, there is nothing to do.
1481          if (!$conversationid) {
1482              return true;
1483          }
1484  
1485          self::delete_conversation_by_id($userid, $conversationid);
1486  
1487          return true;
1488      }
1489  
1490      /**
1491       * Deletes a conversation for a specified user.
1492       *
1493       * This function does not verify any permissions.
1494       *
1495       * @param int $userid The user id of who we want to delete the messages for (this may be done by the admin
1496       *  but will still seem as if it was by the user)
1497       * @param int $conversationid The id of the other user in the conversation
1498       */
1499      public static function delete_conversation_by_id(int $userid, int $conversationid) {
1500          global $DB, $USER;
1501  
1502          // Get all messages belonging to this conversation that have not already been deleted by this user.
1503          $sql = "SELECT m.*
1504                   FROM {messages} m
1505             INNER JOIN {message_conversations} mc
1506                     ON m.conversationid = mc.id
1507              LEFT JOIN {message_user_actions} mua
1508                     ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1509                  WHERE mua.id is NULL
1510                    AND mc.id = ?
1511               ORDER BY m.timecreated ASC";
1512          $messages = $DB->get_records_sql($sql, [$userid, self::MESSAGE_ACTION_DELETED, $conversationid]);
1513  
1514          // Ok, mark these as deleted.
1515          foreach ($messages as $message) {
1516              $mua = new \stdClass();
1517              $mua->userid = $userid;
1518              $mua->messageid = $message->id;
1519              $mua->action = self::MESSAGE_ACTION_DELETED;
1520              $mua->timecreated = time();
1521              $mua->id = $DB->insert_record('message_user_actions', $mua);
1522  
1523              \core\event\message_deleted::create_from_ids($userid, $USER->id,
1524                  $message->id, $mua->id)->trigger();
1525          }
1526      }
1527  
1528      /**
1529       * Returns the count of unread conversations (collection of messages from a single user) for
1530       * the given user.
1531       *
1532       * @param \stdClass $user the user who's conversations should be counted
1533       * @return int the count of the user's unread conversations
1534       */
1535      public static function count_unread_conversations($user = null) {
1536          global $USER, $DB;
1537  
1538          if (empty($user)) {
1539              $user = $USER;
1540          }
1541  
1542          $sql = "SELECT COUNT(DISTINCT(m.conversationid))
1543                    FROM {messages} m
1544              INNER JOIN {message_conversations} mc
1545                      ON m.conversationid = mc.id
1546              INNER JOIN {message_conversation_members} mcm
1547                      ON mc.id = mcm.conversationid
1548               LEFT JOIN {message_user_actions} mua
1549                      ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1550                   WHERE mcm.userid = ?
1551                     AND mc.enabled = ?
1552                     AND mcm.userid != m.useridfrom
1553                     AND mua.id is NULL";
1554  
1555          return $DB->count_records_sql($sql, [$user->id, self::MESSAGE_ACTION_READ, $user->id,
1556              self::MESSAGE_CONVERSATION_ENABLED]);
1557      }
1558  
1559      /**
1560       * Checks if a user can mark all messages as read.
1561       *
1562       * @param int $userid The user id of who we want to mark the messages for
1563       * @param int $conversationid The id of the conversation
1564       * @return bool true if user is permitted, false otherwise
1565       * @since 3.6
1566       */
1567      public static function can_mark_all_messages_as_read(int $userid, int $conversationid) : bool {
1568          global $USER;
1569  
1570          $systemcontext = \context_system::instance();
1571  
1572          if (has_capability('moodle/site:readallmessages', $systemcontext)) {
1573              return true;
1574          }
1575  
1576          if (!self::is_user_in_conversation($userid, $conversationid)) {
1577              return false;
1578          }
1579  
1580          if ($USER->id == $userid) {
1581              return true;
1582          }
1583  
1584          return false;
1585      }
1586  
1587      /**
1588       * Returns the count of conversations (collection of messages from a single user) for
1589       * the given user.
1590       *
1591       * @param int $userid The user whose conversations should be counted.
1592       * @return array the array of conversations counts, indexed by type.
1593       */
1594      public static function get_conversation_counts(int $userid) : array {
1595          global $DB;
1596          self::lazy_create_self_conversation($userid);
1597  
1598          // Some restrictions we need to be aware of:
1599          // - Individual conversations containing soft-deleted user must be counted.
1600          // - Individual conversations containing only deleted messages must NOT be counted.
1601          // - Self-conversations with 0 messages must be counted.
1602          // - Self-conversations containing only deleted messages must NOT be counted.
1603          // - Group conversations with 0 messages must be counted.
1604          // - Linked conversations which are disabled (enabled = 0) must NOT be counted.
1605          // - Any type of conversation can be included in the favourites count, however, the type counts and the favourites count
1606          // are mutually exclusive; any conversations which are counted in favourites cannot be counted elsewhere.
1607  
1608          // First, ask the favourites service to give us the join SQL for favourited conversations,
1609          // so we can include favourite information in the query.
1610          $usercontext = \context_user::instance($userid);
1611          $favservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
1612          list($favsql, $favparams) = $favservice->get_join_sql_by_type('core_message', 'message_conversations', 'fav', 'mc.id');
1613  
1614          $sql = "SELECT mc.type, fav.itemtype, COUNT(DISTINCT mc.id) as count, MAX(maxvisibleconvmessage.convid) as maxconvidmessage
1615                    FROM {message_conversations} mc
1616              INNER JOIN {message_conversation_members} mcm
1617                      ON mcm.conversationid = mc.id
1618               LEFT JOIN (
1619                                SELECT m.conversationid as convid, MAX(m.timecreated) as maxtime
1620                                  FROM {messages} m
1621                            INNER JOIN {message_conversation_members} mcm
1622                                    ON mcm.conversationid = m.conversationid
1623                             LEFT JOIN {message_user_actions} mua
1624                                    ON (mua.messageid = m.id AND mua.userid = :userid AND mua.action = :action)
1625                                 WHERE mua.id is NULL
1626                                   AND mcm.userid = :userid2
1627                              GROUP BY m.conversationid
1628                         ) maxvisibleconvmessage
1629                      ON maxvisibleconvmessage.convid = mc.id
1630                 $favsql
1631                   WHERE mcm.userid = :userid3
1632                     AND mc.enabled = :enabled
1633                     AND (
1634                            (mc.type = :individualtype AND maxvisibleconvmessage.convid IS NOT NULL) OR
1635                            (mc.type = :grouptype) OR
1636                            (mc.type = :selftype)
1637                         )
1638                GROUP BY mc.type, fav.itemtype
1639                ORDER BY mc.type ASC";
1640  
1641          $params = [
1642              'userid' => $userid,
1643              'userid2' => $userid,
1644              'userid3' => $userid,
1645              'userid4' => $userid,
1646              'userid5' => $userid,
1647              'action' => self::MESSAGE_ACTION_DELETED,
1648              'enabled' => self::MESSAGE_CONVERSATION_ENABLED,
1649              'individualtype' => self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
1650              'grouptype' => self::MESSAGE_CONVERSATION_TYPE_GROUP,
1651              'selftype' => self::MESSAGE_CONVERSATION_TYPE_SELF,
1652          ] + $favparams;
1653  
1654          // Assemble the return array.
1655          $counts = [
1656              'favourites' => 0,
1657              'types' => [
1658                  self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
1659                  self::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
1660                  self::MESSAGE_CONVERSATION_TYPE_SELF => 0
1661              ]
1662          ];
1663  
1664          // For the self-conversations, get the total number of messages (to know if the conversation is new or it has been emptied).
1665          $selfmessagessql = "SELECT COUNT(m.id)
1666                                FROM {messages} m
1667                          INNER JOIN {message_conversations} mc
1668                                  ON mc.id = m.conversationid
1669                               WHERE mc.type = ? AND convhash = ?";
1670          $selfmessagestotal = $DB->count_records_sql(
1671              $selfmessagessql,
1672              [self::MESSAGE_CONVERSATION_TYPE_SELF, helper::get_conversation_hash([$userid])]
1673          );
1674  
1675          $countsrs = $DB->get_recordset_sql($sql, $params);
1676          foreach ($countsrs as $key => $val) {
1677              // Empty self-conversations with deleted messages should be excluded.
1678              if ($val->type == self::MESSAGE_CONVERSATION_TYPE_SELF && empty($val->maxconvidmessage) && $selfmessagestotal > 0) {
1679                  continue;
1680              }
1681              if (!empty($val->itemtype)) {
1682                  $counts['favourites'] += $val->count;
1683                  continue;
1684              }
1685              $counts['types'][$val->type] = $val->count;
1686          }
1687          $countsrs->close();
1688  
1689          return $counts;
1690      }
1691  
1692      /**
1693       * Marks all messages being sent to a user in a particular conversation.
1694       *
1695       * If $conversationdid is null then it marks all messages as read sent to $userid.
1696       *
1697       * @param int $userid
1698       * @param int|null $conversationid The conversation the messages belong to mark as read, if null mark all
1699       */
1700      public static function mark_all_messages_as_read($userid, $conversationid = null) {
1701          global $DB;
1702  
1703          $messagesql = "SELECT m.*
1704                           FROM {messages} m
1705                     INNER JOIN {message_conversations} mc
1706                             ON mc.id = m.conversationid
1707                     INNER JOIN {message_conversation_members} mcm
1708                             ON mcm.conversationid = mc.id
1709                      LEFT JOIN {message_user_actions} mua
1710                             ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1711                          WHERE mua.id is NULL
1712                            AND mcm.userid = ?
1713                            AND m.useridfrom != ?";
1714          $messageparams = [];
1715          $messageparams[] = $userid;
1716          $messageparams[] = self::MESSAGE_ACTION_READ;
1717          $messageparams[] = $userid;
1718          $messageparams[] = $userid;
1719          if (!is_null($conversationid)) {
1720              $messagesql .= " AND mc.id = ?";
1721              $messageparams[] = $conversationid;
1722          }
1723  
1724          $messages = $DB->get_recordset_sql($messagesql, $messageparams);
1725          foreach ($messages as $message) {
1726              self::mark_message_as_read($userid, $message);
1727          }
1728          $messages->close();
1729      }
1730  
1731      /**
1732       * Marks all notifications being sent from one user to another user as read.
1733       *
1734       * If the from user is null then it marks all notifications as read sent to the to user.
1735       *
1736       * @param int $touserid the id of the message recipient
1737       * @param int|null $fromuserid the id of the message sender, null if all messages
1738       * @param int|null $timecreatedto mark notifications created before this time as read
1739       * @return void
1740       */
1741      public static function mark_all_notifications_as_read($touserid, $fromuserid = null, $timecreatedto = null) {
1742          global $DB;
1743  
1744          $notificationsql = "SELECT n.*
1745                                FROM {notifications} n
1746                               WHERE useridto = ?
1747                                 AND timeread is NULL";
1748          $notificationsparams = [$touserid];
1749          if (!empty($fromuserid)) {
1750              $notificationsql .= " AND useridfrom = ?";
1751              $notificationsparams[] = $fromuserid;
1752          }
1753          if (!empty($timecreatedto)) {
1754              $notificationsql .= " AND timecreated <= ?";
1755              $notificationsparams[] = $timecreatedto;
1756          }
1757  
1758          $notifications = $DB->get_recordset_sql($notificationsql, $notificationsparams);
1759          foreach ($notifications as $notification) {
1760              self::mark_notification_as_read($notification);
1761          }
1762          $notifications->close();
1763      }
1764  
1765      /**
1766       * @deprecated since 3.5
1767       */
1768      public static function mark_all_read_for_user() {
1769          throw new \coding_exception('\core_message\api::mark_all_read_for_user has been removed. Please either use ' .
1770              '\core_message\api::mark_all_notifications_as_read or \core_message\api::mark_all_messages_as_read');
1771      }
1772  
1773      /**
1774       * Returns message preferences.
1775       *
1776       * @param array $processors
1777       * @param array $providers
1778       * @param \stdClass $user
1779       * @return \stdClass
1780       * @since 3.2
1781       */
1782      public static function get_all_message_preferences($processors, $providers, $user) {
1783          $preferences = helper::get_providers_preferences($providers, $user->id);
1784          $preferences->userdefaultemail = $user->email; // May be displayed by the email processor.
1785  
1786          // For every processors put its options on the form (need to get function from processor's lib.php).
1787          foreach ($processors as $processor) {
1788              $processor->object->load_data($preferences, $user->id);
1789          }
1790  
1791          // Load general messaging preferences.
1792          $preferences->blocknoncontacts = self::get_user_privacy_messaging_preference($user->id);
1793          $preferences->mailformat = $user->mailformat;
1794          $preferences->mailcharset = get_user_preferences('mailcharset', '', $user->id);
1795  
1796          return $preferences;
1797      }
1798  
1799      /**
1800       * Count the number of users blocked by a user.
1801       *
1802       * @param \stdClass $user The user object
1803       * @return int the number of blocked users
1804       */
1805      public static function count_blocked_users($user = null) {
1806          global $USER, $DB;
1807  
1808          if (empty($user)) {
1809              $user = $USER;
1810          }
1811  
1812          $sql = "SELECT count(mub.id)
1813                    FROM {message_users_blocked} mub
1814                   WHERE mub.userid = :userid";
1815          return $DB->count_records_sql($sql, array('userid' => $user->id));
1816      }
1817  
1818      /**
1819       * Determines if a user is permitted to send another user a private message.
1820       * If no sender is provided then it defaults to the logged in user.
1821       *
1822       * @deprecated since 3.8
1823       * @todo Final deprecation in MDL-66266
1824       * @param \stdClass $recipient The user object.
1825       * @param \stdClass|null $sender The user object.
1826       * @return bool true if user is permitted, false otherwise.
1827       */
1828      public static function can_post_message($recipient, $sender = null) {
1829          global $USER;
1830  
1831          debugging('\core_message\api::can_post_message is deprecated, please use ' .
1832              '\core_message\api::can_send_message instead.', DEBUG_DEVELOPER);
1833  
1834          if (is_null($sender)) {
1835              // The message is from the logged in user, unless otherwise specified.
1836              $sender = $USER;
1837          }
1838  
1839          return self::can_send_message($recipient->id, $sender->id);
1840      }
1841  
1842      /**
1843       * Determines if a user is permitted to send another user a private message.
1844       *
1845       * @param int $recipientid The recipient user id.
1846       * @param int $senderid The sender user id.
1847       * @param bool $evenifblocked This lets the user know, that even if the recipient has blocked the user
1848       *        the user is still able to send a message.
1849       * @return bool true if user is permitted, false otherwise.
1850       */
1851      public static function can_send_message(int $recipientid, int $senderid, bool $evenifblocked = false) : bool {
1852          $systemcontext = \context_system::instance();
1853  
1854          if (!has_capability('moodle/site:sendmessage', $systemcontext, $senderid)) {
1855              return false;
1856          }
1857  
1858          if (has_capability('moodle/site:readallmessages', $systemcontext, $senderid)) {
1859              return true;
1860          }
1861  
1862          // Check if the recipient can be messaged by the sender.
1863          return self::can_contact_user($recipientid, $senderid, $evenifblocked);
1864      }
1865  
1866      /**
1867       * Determines if a user is permitted to send a message to a given conversation.
1868       * If no sender is provided then it defaults to the logged in user.
1869       *
1870       * @param int $userid the id of the user on which the checks will be applied.
1871       * @param int $conversationid the id of the conversation we wish to check.
1872       * @return bool true if the user can send a message to the conversation, false otherwise.
1873       * @throws \moodle_exception
1874       */
1875      public static function can_send_message_to_conversation(int $userid, int $conversationid) : bool {
1876          global $DB;
1877  
1878          $systemcontext = \context_system::instance();
1879          if (!has_capability('moodle/site:sendmessage', $systemcontext, $userid)) {
1880              return false;
1881          }
1882  
1883          if (!self::is_user_in_conversation($userid, $conversationid)) {
1884              return false;
1885          }
1886  
1887          // User can post messages and is in the conversation, but we need to check the conversation type to
1888          // know whether or not to check the user privacy settings via can_contact_user().
1889          $conversation = $DB->get_record('message_conversations', ['id' => $conversationid], '*', MUST_EXIST);
1890          if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_GROUP ||
1891              $conversation->type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
1892              return true;
1893          } else if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
1894              // Get the other user in the conversation.
1895              $members = self::get_conversation_members($userid, $conversationid);
1896              $otheruser = array_filter($members, function($member) use($userid) {
1897                  return $member->id != $userid;
1898              });
1899              $otheruser = reset($otheruser);
1900  
1901              return self::can_contact_user($otheruser->id, $userid);
1902          } else {
1903              throw new \moodle_exception("Invalid conversation type '$conversation->type'.");
1904          }
1905      }
1906  
1907      /**
1908       * Send a message from a user to a conversation.
1909       *
1910       * This method will create the basic eventdata and delegate to message creation to message_send.
1911       * The message_send() method is responsible for event data that is specific to each recipient.
1912       *
1913       * @param int $userid the sender id.
1914       * @param int $conversationid the conversation id.
1915       * @param string $message the message to send.
1916       * @param int $format the format of the message to send.
1917       * @return \stdClass the message created.
1918       * @throws \coding_exception
1919       * @throws \moodle_exception if the user is not permitted to send a message to the conversation.
1920       */
1921      public static function send_message_to_conversation(int $userid, int $conversationid, string $message,
1922                                                          int $format) : \stdClass {
1923          global $DB, $PAGE;
1924  
1925          if (!self::can_send_message_to_conversation($userid, $conversationid)) {
1926              throw new \moodle_exception("User $userid cannot send a message to conversation $conversationid");
1927          }
1928  
1929          $eventdata = new \core\message\message();
1930          $eventdata->courseid         = 1;
1931          $eventdata->component        = 'moodle';
1932          $eventdata->name             = 'instantmessage';
1933          $eventdata->userfrom         = \core_user::get_user($userid);
1934          $eventdata->convid           = $conversationid;
1935  
1936          if ($format == FORMAT_HTML) {
1937              $eventdata->fullmessagehtml  = $message;
1938              // Some message processors may revert to sending plain text even if html is supplied,
1939              // so we keep both plain and html versions if we're intending to send html.
1940              $eventdata->fullmessage = html_to_text($eventdata->fullmessagehtml);
1941          } else {
1942              $eventdata->fullmessage      = $message;
1943              $eventdata->fullmessagehtml  = '';
1944          }
1945  
1946          $eventdata->fullmessageformat = $format;
1947          $eventdata->smallmessage = $message; // Store the message unfiltered. Clean up on output.
1948  
1949          $eventdata->timecreated     = time();
1950          $eventdata->notification    = 0;
1951          // Custom data for event.
1952          $customdata = [
1953              'actionbuttons' => [
1954                  'send' => get_string('send', 'message'),
1955              ],
1956              'placeholders' => [
1957                  'send' => get_string('writeamessage', 'message'),
1958              ],
1959          ];
1960  
1961          $userpicture = new \user_picture($eventdata->userfrom);
1962          $userpicture->size = 1; // Use f1 size.
1963          $userpicture = $userpicture->get_url($PAGE)->out(false);
1964  
1965          $conv = $DB->get_record('message_conversations', ['id' => $conversationid]);
1966          if ($conv->type == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
1967              $convextrafields = self::get_linked_conversation_extra_fields([$conv]);
1968              // Conversation images.
1969              $customdata['notificationsendericonurl'] = $userpicture;
1970              $imageurl = isset($convextrafields[$conv->id]) ? $convextrafields[$conv->id]['imageurl'] : null;
1971              if ($imageurl) {
1972                  $customdata['notificationiconurl'] = $imageurl;
1973              }
1974              // Conversation name.
1975              if (is_null($conv->contextid)) {
1976                  $convcontext = \context_user::instance($userid);
1977              } else {
1978                  $convcontext = \context::instance_by_id($conv->contextid);
1979              }
1980              $customdata['conversationname'] = format_string($conv->name, true, ['context' => $convcontext]);
1981          } else if ($conv->type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
1982              $customdata['notificationiconurl'] = $userpicture;
1983          }
1984          $eventdata->customdata = $customdata;
1985  
1986          $messageid = message_send($eventdata);
1987  
1988          if (!$messageid) {
1989              throw new \moodle_exception('messageundeliveredbynotificationsettings', 'moodle');
1990          }
1991  
1992          $messagerecord = $DB->get_record('messages', ['id' => $messageid], 'id, useridfrom, fullmessage,
1993                  timecreated, fullmessagetrust');
1994          $message = (object) [
1995              'id' => $messagerecord->id,
1996              'useridfrom' => $messagerecord->useridfrom,
1997              'text' => $messagerecord->fullmessage,
1998              'timecreated' => $messagerecord->timecreated,
1999              'fullmessagetrust' => $messagerecord->fullmessagetrust
2000          ];
2001          return $message;
2002      }
2003  
2004      /**
2005       * Get the messaging preference for a user.
2006       * If the user has not any messaging privacy preference:
2007       * - When $CFG->messagingallusers = false the default user preference is MESSAGE_PRIVACY_COURSEMEMBER.
2008       * - When $CFG->messagingallusers = true the default user preference is MESSAGE_PRIVACY_SITE.
2009       *
2010       * @param  int    $userid The user identifier.
2011       * @return int    The default messaging preference.
2012       */
2013      public static function get_user_privacy_messaging_preference(int $userid) : int {
2014          global $CFG, $USER;
2015  
2016          // When $CFG->messagingallusers is enabled, default value for the messaging preference will be "Anyone on the site";
2017          // otherwise, the default value will be "My contacts and anyone in my courses".
2018          if (empty($CFG->messagingallusers)) {
2019              $defaultprefvalue = self::MESSAGE_PRIVACY_COURSEMEMBER;
2020          } else {
2021              $defaultprefvalue = self::MESSAGE_PRIVACY_SITE;
2022          }
2023          if ($userid == $USER->id) {
2024              $user = $USER;
2025          } else {
2026              $user = $userid;
2027          }
2028          $privacypreference = get_user_preferences('message_blocknoncontacts', $defaultprefvalue, $user);
2029  
2030          // When the $CFG->messagingallusers privacy setting is disabled, MESSAGE_PRIVACY_SITE is
2031          // also disabled, so it has to be replaced to MESSAGE_PRIVACY_COURSEMEMBER.
2032          if (empty($CFG->messagingallusers) && $privacypreference == self::MESSAGE_PRIVACY_SITE) {
2033              $privacypreference = self::MESSAGE_PRIVACY_COURSEMEMBER;
2034          }
2035  
2036          return $privacypreference;
2037      }
2038  
2039      /**
2040       * Checks if the recipient is allowing messages from users that aren't a
2041       * contact. If not then it checks to make sure the sender is in the
2042       * recipient's contacts.
2043       *
2044       * @deprecated since 3.6
2045       * @param \stdClass $recipient The user object.
2046       * @param \stdClass|null $sender The user object.
2047       * @return bool true if $sender is blocked, false otherwise.
2048       */
2049      public static function is_user_non_contact_blocked($recipient, $sender = null) {
2050          debugging('\core_message\api::is_user_non_contact_blocked() is deprecated', DEBUG_DEVELOPER);
2051  
2052          global $USER, $CFG;
2053  
2054          if (is_null($sender)) {
2055              // The message is from the logged in user, unless otherwise specified.
2056              $sender = $USER;
2057          }
2058  
2059          $privacypreference = self::get_user_privacy_messaging_preference($recipient->id);
2060          switch ($privacypreference) {
2061              case self::MESSAGE_PRIVACY_SITE:
2062                  if (!empty($CFG->messagingallusers)) {
2063                      // Users can be messaged without being contacts or members of the same course.
2064                      break;
2065                  }
2066                  // When the $CFG->messagingallusers privacy setting is disabled, continue with the next
2067                  // case, because MESSAGE_PRIVACY_SITE is replaced to MESSAGE_PRIVACY_COURSEMEMBER.
2068              case self::MESSAGE_PRIVACY_COURSEMEMBER:
2069                  // Confirm the sender and the recipient are both members of the same course.
2070                  if (enrol_sharing_course($recipient, $sender)) {
2071                      // All good, the recipient and the sender are members of the same course.
2072                      return false;
2073                  }
2074              case self::MESSAGE_PRIVACY_ONLYCONTACTS:
2075                  // True if they aren't contacts (they can't send a message because of the privacy settings), false otherwise.
2076                  return !self::is_contact($sender->id, $recipient->id);
2077          }
2078  
2079          return false;
2080      }
2081  
2082      /**
2083       * Checks if the recipient has specifically blocked the sending user.
2084       *
2085       * Note: This function will always return false if the sender has the
2086       * readallmessages capability at the system context level.
2087       *
2088       * @deprecated since 3.6
2089       * @param int $recipientid User ID of the recipient.
2090       * @param int $senderid User ID of the sender.
2091       * @return bool true if $sender is blocked, false otherwise.
2092       */
2093      public static function is_user_blocked($recipientid, $senderid = null) {
2094          debugging('\core_message\api::is_user_blocked is deprecated and should not be used.',
2095              DEBUG_DEVELOPER);
2096  
2097          global $USER;
2098  
2099          if (is_null($senderid)) {
2100              // The message is from the logged in user, unless otherwise specified.
2101              $senderid = $USER->id;
2102          }
2103  
2104          $systemcontext = \context_system::instance();
2105          if (has_capability('moodle/site:readallmessages', $systemcontext, $senderid)) {
2106              return false;
2107          }
2108  
2109          if (self::is_blocked($recipientid, $senderid)) {
2110              return true;
2111          }
2112  
2113          return false;
2114      }
2115  
2116      /**
2117       * Get specified message processor, validate corresponding plugin existence and
2118       * system configuration.
2119       *
2120       * @param string $name  Name of the processor.
2121       * @param bool $ready only return ready-to-use processors.
2122       * @return mixed $processor if processor present else empty array.
2123       * @since Moodle 3.2
2124       */
2125      public static function get_message_processor($name, $ready = false) {
2126          global $DB, $CFG;
2127  
2128          $processor = $DB->get_record('message_processors', array('name' => $name));
2129          if (empty($processor)) {
2130              // Processor not found, return.
2131              return array();
2132          }
2133  
2134          $processor = self::get_processed_processor_object($processor);
2135          if ($ready) {
2136              if ($processor->enabled && $processor->configured) {
2137                  return $processor;
2138              } else {
2139                  return array();
2140              }
2141          } else {
2142              return $processor;
2143          }
2144      }
2145  
2146      /**
2147       * Returns weather a given processor is enabled or not.
2148       * Note:- This doesn't check if the processor is configured or not.
2149       *
2150       * @param string $name Name of the processor
2151       * @return bool
2152       */
2153      public static function is_processor_enabled($name) {
2154  
2155          $cache = \cache::make('core', 'message_processors_enabled');
2156          $status = $cache->get($name);
2157  
2158          if ($status === false) {
2159              $processor = self::get_message_processor($name);
2160              if (!empty($processor)) {
2161                  $cache->set($name, $processor->enabled);
2162                  return $processor->enabled;
2163              } else {
2164                  return false;
2165              }
2166          }
2167  
2168          return $status;
2169      }
2170  
2171      /**
2172       * Set status of a processor.
2173       *
2174       * @param \stdClass $processor processor record.
2175       * @param 0|1 $enabled 0 or 1 to set the processor status.
2176       * @return bool
2177       * @since Moodle 3.2
2178       */
2179      public static function update_processor_status($processor, $enabled) {
2180          global $DB;
2181          $cache = \cache::make('core', 'message_processors_enabled');
2182          $cache->delete($processor->name);
2183          return $DB->set_field('message_processors', 'enabled', $enabled, array('id' => $processor->id));
2184      }
2185  
2186      /**
2187       * Given a processor object, loads information about it's settings and configurations.
2188       * This is not a public api, instead use @see \core_message\api::get_message_processor()
2189       * or @see \get_message_processors()
2190       *
2191       * @param \stdClass $processor processor object
2192       * @return \stdClass processed processor object
2193       * @since Moodle 3.2
2194       */
2195      public static function get_processed_processor_object(\stdClass $processor) {
2196          global $CFG;
2197  
2198          $processorfile = $CFG->dirroot. '/message/output/'.$processor->name.'/message_output_'.$processor->name.'.php';
2199          if (is_readable($processorfile)) {
2200              include_once($processorfile);
2201              $processclass = 'message_output_' . $processor->name;
2202              if (class_exists($processclass)) {
2203                  $pclass = new $processclass();
2204                  $processor->object = $pclass;
2205                  $processor->configured = 0;
2206                  if ($pclass->is_system_configured()) {
2207                      $processor->configured = 1;
2208                  }
2209                  $processor->hassettings = 0;
2210                  if (is_readable($CFG->dirroot.'/message/output/'.$processor->name.'/settings.php')) {
2211                      $processor->hassettings = 1;
2212                  }
2213                  $processor->available = 1;
2214              } else {
2215                  print_error('errorcallingprocessor', 'message');
2216              }
2217          } else {
2218              $processor->available = 0;
2219          }
2220          return $processor;
2221      }
2222  
2223      /**
2224       * Retrieve users blocked by $user1
2225       *
2226       * @param int $userid The user id of the user whos blocked users we are returning
2227       * @return array the users blocked
2228       */
2229      public static function get_blocked_users($userid) {
2230          global $DB;
2231  
2232          $userfields = \user_picture::fields('u', array('lastaccess'));
2233          $blockeduserssql = "SELECT $userfields
2234                                FROM {message_users_blocked} mub
2235                          INNER JOIN {user} u
2236                                  ON u.id = mub.blockeduserid
2237                               WHERE u.deleted = 0
2238                                 AND mub.userid = ?
2239                            GROUP BY $userfields
2240                            ORDER BY u.firstname ASC";
2241          return $DB->get_records_sql($blockeduserssql, [$userid]);
2242      }
2243  
2244      /**
2245       * Mark a single message as read.
2246       *
2247       * @param int $userid The user id who marked the message as read
2248       * @param \stdClass $message The message
2249       * @param int|null $timeread The time the message was marked as read, if null will default to time()
2250       */
2251      public static function mark_message_as_read($userid, $message, $timeread = null) {
2252          global $DB;
2253  
2254          if (is_null($timeread)) {
2255              $timeread = time();
2256          }
2257  
2258          $mua = new \stdClass();
2259          $mua->userid = $userid;
2260          $mua->messageid = $message->id;
2261          $mua->action = self::MESSAGE_ACTION_READ;
2262          $mua->timecreated = $timeread;
2263          $mua->id = $DB->insert_record('message_user_actions', $mua);
2264  
2265          // Get the context for the user who received the message.
2266          $context = \context_user::instance($userid, IGNORE_MISSING);
2267          // If the user no longer exists the context value will be false, in this case use the system context.
2268          if ($context === false) {
2269              $context = \context_system::instance();
2270          }
2271  
2272          // Trigger event for reading a message.
2273          $event = \core\event\message_viewed::create(array(
2274              'objectid' => $mua->id,
2275              'userid' => $userid, // Using the user who read the message as they are the ones performing the action.
2276              'context' => $context,
2277              'relateduserid' => $message->useridfrom,
2278              'other' => array(
2279                  'messageid' => $message->id
2280              )
2281          ));
2282          $event->trigger();
2283      }
2284  
2285      /**
2286       * Mark a single notification as read.
2287       *
2288       * @param \stdClass $notification The notification
2289       * @param int|null $timeread The time the message was marked as read, if null will default to time()
2290       */
2291      public static function mark_notification_as_read($notification, $timeread = null) {
2292          global $DB;
2293  
2294          if (is_null($timeread)) {
2295              $timeread = time();
2296          }
2297  
2298          if (is_null($notification->timeread)) {
2299              $updatenotification = new \stdClass();
2300              $updatenotification->id = $notification->id;
2301              $updatenotification->timeread = $timeread;
2302  
2303              $DB->update_record('notifications', $updatenotification);
2304  
2305              // Trigger event for reading a notification.
2306              \core\event\notification_viewed::create_from_ids(
2307                  $notification->useridfrom,
2308                  $notification->useridto,
2309                  $notification->id
2310              )->trigger();
2311          }
2312      }
2313  
2314      /**
2315       * Checks if a user can delete a message.
2316       *
2317       * @param int $userid the user id of who we want to delete the message for (this may be done by the admin
2318       *  but will still seem as if it was by the user)
2319       * @param int $messageid The message id
2320       * @return bool Returns true if a user can delete the message, false otherwise.
2321       */
2322      public static function can_delete_message($userid, $messageid) {
2323          global $DB, $USER;
2324  
2325          $systemcontext = \context_system::instance();
2326  
2327          $conversationid = $DB->get_field('messages', 'conversationid', ['id' => $messageid], MUST_EXIST);
2328  
2329          if (has_capability('moodle/site:deleteanymessage', $systemcontext)) {
2330              return true;
2331          }
2332  
2333          if (!self::is_user_in_conversation($userid, $conversationid)) {
2334              return false;
2335          }
2336  
2337          if (has_capability('moodle/site:deleteownmessage', $systemcontext) &&
2338                  $USER->id == $userid) {
2339              return true;
2340          }
2341  
2342          return false;
2343      }
2344  
2345      /**
2346       * Deletes a message.
2347       *
2348       * This function does not verify any permissions.
2349       *
2350       * @param int $userid the user id of who we want to delete the message for (this may be done by the admin
2351       *  but will still seem as if it was by the user)
2352       * @param int $messageid The message id
2353       * @return bool
2354       */
2355      public static function delete_message($userid, $messageid) {
2356          global $DB, $USER;
2357  
2358          if (!$DB->record_exists('messages', ['id' => $messageid])) {
2359              return false;
2360          }
2361  
2362          // Check if the user has already deleted this message.
2363          if (!$DB->record_exists('message_user_actions', ['userid' => $userid,
2364                  'messageid' => $messageid, 'action' => self::MESSAGE_ACTION_DELETED])) {
2365              $mua = new \stdClass();
2366              $mua->userid = $userid;
2367              $mua->messageid = $messageid;
2368              $mua->action = self::MESSAGE_ACTION_DELETED;
2369              $mua->timecreated = time();
2370              $mua->id = $DB->insert_record('message_user_actions', $mua);
2371  
2372              // Trigger event for deleting a message.
2373              \core\event\message_deleted::create_from_ids($userid, $USER->id,
2374                  $messageid, $mua->id)->trigger();
2375  
2376              return true;
2377          }
2378  
2379          return false;
2380      }
2381  
2382      /**
2383       * Returns the conversation between two users.
2384       *
2385       * @param array $userids
2386       * @return int|bool The id of the conversation, false if not found
2387       */
2388      public static function get_conversation_between_users(array $userids) {
2389          global $DB;
2390  
2391          if (empty($userids)) {
2392              return false;
2393          }
2394  
2395          $hash = helper::get_conversation_hash($userids);
2396  
2397          if ($conversation = $DB->get_record('message_conversations', ['type' => self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
2398                  'convhash' => $hash])) {
2399              return $conversation->id;
2400          }
2401  
2402          return false;
2403      }
2404  
2405      /**
2406       * Returns the conversations between sets of users.
2407       *
2408       * The returned array of results will be in the same order as the requested
2409       * arguments, null will be returned if there is no conversation for that user
2410       * pair.
2411       *
2412       * For example:
2413       * If we have 6 users with ids 1, 2, 3, 4, 5, 6 where only 2 conversations
2414       * exist. One between 1 and 2 and another between 5 and 6.
2415       *
2416       * Then if we call:
2417       * $conversations = get_individual_conversations_between_users([[1,2], [3,4], [5,6]]);
2418       *
2419       * The conversations array will look like:
2420       * [<conv_record>, null, <conv_record>];
2421       *
2422       * Where null is returned for the pairing of [3, 4] since no record exists.
2423       *
2424       * @deprecated since 3.8
2425       * @param array $useridsets An array of arrays where the inner array is the set of user ids
2426       * @return stdClass[] Array of conversation records
2427       */
2428      public static function get_individual_conversations_between_users(array $useridsets) : array {
2429          global $DB;
2430  
2431          debugging('\core_message\api::get_individual_conversations_between_users is deprecated and no longer used',
2432              DEBUG_DEVELOPER);
2433  
2434          if (empty($useridsets)) {
2435              return [];
2436          }
2437  
2438          $hashes = array_map(function($userids) {
2439              return  helper::get_conversation_hash($userids);
2440          }, $useridsets);
2441  
2442          list($inorequalsql, $params) = $DB->get_in_or_equal($hashes);
2443          array_unshift($params, self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL);
2444          $where = "type = ? AND convhash $inorequalsql}";
2445          $conversations = array_fill(0, count($hashes), null);
2446          $records = $DB->get_records_select('message_conversations', $where, $params);
2447  
2448          foreach (array_values($records) as $record) {
2449              $index = array_search($record->convhash, $hashes);
2450              if ($index !== false) {
2451                  $conversations[$index] = $record;
2452              }
2453          }
2454  
2455          return $conversations;
2456      }
2457  
2458      /**
2459       * Returns the self conversation for a user.
2460       *
2461       * @param int $userid The user id to get the self-conversations
2462       * @return \stdClass|false The self-conversation object or false if it doesn't exist
2463       * @since Moodle 3.7
2464       */
2465      public static function get_self_conversation(int $userid) {
2466          global $DB;
2467          self::lazy_create_self_conversation($userid);
2468  
2469          $conditions = [
2470              'type' => self::MESSAGE_CONVERSATION_TYPE_SELF,
2471              'convhash' => helper::get_conversation_hash([$userid])
2472          ];
2473          return $DB->get_record('message_conversations', $conditions);
2474      }
2475  
2476      /**
2477       * Creates a conversation between two users.
2478       *
2479       * @deprecated since 3.6
2480       * @param array $userids
2481       * @return int The id of the conversation
2482       */
2483      public static function create_conversation_between_users(array $userids) {
2484          debugging('\core_message\api::create_conversation_between_users is deprecated, please use ' .
2485              '\core_message\api::create_conversation instead.', DEBUG_DEVELOPER);
2486  
2487          // This method was always used for individual conversations.
2488          $conversation = self::create_conversation(self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, $userids);
2489  
2490          return $conversation->id;
2491      }
2492  
2493      /**
2494       * Creates a conversation with selected users and messages.
2495       *
2496       * @param int $type The type of conversation
2497       * @param int[] $userids The array of users to add to the conversation
2498       * @param string|null $name The name of the conversation
2499       * @param int $enabled Determines if the conversation is created enabled or disabled
2500       * @param string|null $component Defines the Moodle component which the conversation belongs to, if any
2501       * @param string|null $itemtype Defines the type of the component
2502       * @param int|null $itemid The id of the component
2503       * @param int|null $contextid The id of the context
2504       * @return \stdClass
2505       */
2506      public static function create_conversation(int $type, array $userids, string $name = null,
2507              int $enabled = self::MESSAGE_CONVERSATION_ENABLED, string $component = null,
2508              string $itemtype = null, int $itemid = null, int $contextid = null) {
2509  
2510          global $DB;
2511  
2512          $validtypes = [
2513              self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
2514              self::MESSAGE_CONVERSATION_TYPE_GROUP,
2515              self::MESSAGE_CONVERSATION_TYPE_SELF
2516          ];
2517  
2518          if (!in_array($type, $validtypes)) {
2519              throw new \moodle_exception('An invalid conversation type was specified.');
2520          }
2521  
2522          // Sanity check.
2523          if ($type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
2524              if (count($userids) > 2) {
2525                  throw new \moodle_exception('An individual conversation can not have more than two users.');
2526              }
2527              if ($userids[0] == $userids[1]) {
2528                  throw new \moodle_exception('Trying to create an individual conversation instead of a self conversation.');
2529              }
2530          } else if ($type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
2531              if (count($userids) != 1) {
2532                  throw new \moodle_exception('A self conversation can not have more than one user.');
2533              }
2534          }
2535  
2536          $conversation = new \stdClass();
2537          $conversation->type = $type;
2538          $conversation->name = $name;
2539          $conversation->convhash = null;
2540          if ($type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL || $type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
2541              $conversation->convhash = helper::get_conversation_hash($userids);
2542  
2543              // Don't blindly create a conversation between 2 users if there is already one present - return that.
2544              // This stops us making duplicate self and individual conversations, which is invalid.
2545              if ($record = $DB->get_record('message_conversations', ['convhash' => $conversation->convhash])) {
2546                  return $record;
2547              }
2548          }
2549          $conversation->component = $component;
2550          $conversation->itemtype = $itemtype;
2551          $conversation->itemid = $itemid;
2552          $conversation->contextid = $contextid;
2553          $conversation->enabled = $enabled;
2554          $conversation->timecreated = time();
2555          $conversation->timemodified = $conversation->timecreated;
2556          $conversation->id = $DB->insert_record('message_conversations', $conversation);
2557  
2558          // Add users to this conversation.
2559          $arrmembers = [];
2560          foreach ($userids as $userid) {
2561              $member = new \stdClass();
2562              $member->conversationid = $conversation->id;
2563              $member->userid = $userid;
2564              $member->timecreated = time();
2565              $member->id = $DB->insert_record('message_conversation_members', $member);
2566  
2567              $arrmembers[] = $member;
2568          }
2569  
2570          $conversation->members = $arrmembers;
2571  
2572          return $conversation;
2573      }
2574  
2575      /**
2576       * Checks if a user can create a group conversation.
2577       *
2578       * @param int $userid The id of the user attempting to create the conversation
2579       * @param \context $context The context they are creating the conversation from, most likely course context
2580       * @return bool
2581       */
2582      public static function can_create_group_conversation(int $userid, \context $context) : bool {
2583          global $CFG;
2584  
2585          // If we can't message at all, then we can't create a conversation.
2586          if (empty($CFG->messaging)) {
2587              return false;
2588          }
2589  
2590          // We need to check they have the capability to create the conversation.
2591          return has_capability('moodle/course:creategroupconversations', $context, $userid);
2592      }
2593  
2594      /**
2595       * Checks if a user can create a contact request.
2596       *
2597       * @param int $userid The id of the user who is creating the contact request
2598       * @param int $requesteduserid The id of the user being requested
2599       * @return bool
2600       */
2601      public static function can_create_contact(int $userid, int $requesteduserid) : bool {
2602          global $CFG;
2603  
2604          // If we can't message at all, then we can't create a contact.
2605          if (empty($CFG->messaging)) {
2606              return false;
2607          }
2608  
2609          // If we can message anyone on the site then we can create a contact.
2610          if ($CFG->messagingallusers) {
2611              return true;
2612          }
2613  
2614          // We need to check if they are in the same course.
2615          return enrol_sharing_course($userid, $requesteduserid);
2616      }
2617  
2618      /**
2619       * Handles creating a contact request.
2620       *
2621       * @param int $userid The id of the user who is creating the contact request
2622       * @param int $requesteduserid The id of the user being requested
2623       * @return \stdClass the request
2624       */
2625      public static function create_contact_request(int $userid, int $requesteduserid) : \stdClass {
2626          global $DB, $PAGE, $SITE;
2627  
2628          $request = new \stdClass();
2629          $request->userid = $userid;
2630          $request->requesteduserid = $requesteduserid;
2631          $request->timecreated = time();
2632  
2633          $request->id = $DB->insert_record('message_contact_requests', $request);
2634  
2635          // Send a notification.
2636          $userfrom = \core_user::get_user($userid);
2637          $userfromfullname = fullname($userfrom);
2638          $userto = \core_user::get_user($requesteduserid);
2639          $url = new \moodle_url('/message/index.php', ['view' => 'contactrequests']);
2640  
2641          $subject = get_string_manager()->get_string('messagecontactrequestsubject', 'core_message', (object) [
2642              'sitename' => format_string($SITE->fullname, true, ['context' => \context_system::instance()]),
2643              'user' => $userfromfullname,
2644          ], $userto->lang);
2645  
2646          $fullmessage = get_string_manager()->get_string('messagecontactrequest', 'core_message', (object) [
2647              'url' => $url->out(),
2648              'user' => $userfromfullname,
2649          ], $userto->lang);
2650  
2651          $message = new \core\message\message();
2652          $message->courseid = SITEID;
2653          $message->component = 'moodle';
2654          $message->name = 'messagecontactrequests';
2655          $message->notification = 1;
2656          $message->userfrom = $userfrom;
2657          $message->userto = $userto;
2658          $message->subject = $subject;
2659          $message->fullmessage = text_to_html($fullmessage);
2660          $message->fullmessageformat = FORMAT_HTML;
2661          $message->fullmessagehtml = $fullmessage;
2662          $message->smallmessage = '';
2663          $message->contexturl = $url->out(false);
2664          $userpicture = new \user_picture($userfrom);
2665          $userpicture->size = 1; // Use f1 size.
2666          $userpicture->includetoken = $userto->id; // Generate an out-of-session token for the user receiving the message.
2667          $message->customdata = [
2668              'notificationiconurl' => $userpicture->get_url($PAGE)->out(false),
2669              'actionbuttons' => [
2670                  'accept' => get_string_manager()->get_string('accept', 'moodle', null, $userto->lang),
2671                  'reject' => get_string_manager()->get_string('reject', 'moodle', null, $userto->lang),
2672              ],
2673          ];
2674  
2675          message_send($message);
2676  
2677          return $request;
2678      }
2679  
2680  
2681      /**
2682       * Handles confirming a contact request.
2683       *
2684       * @param int $userid The id of the user who created the contact request
2685       * @param int $requesteduserid The id of the user confirming the request
2686       */
2687      public static function confirm_contact_request(int $userid, int $requesteduserid) {
2688          global $DB;
2689  
2690          if ($request = $DB->get_record('message_contact_requests', ['userid' => $userid,
2691                  'requesteduserid' => $requesteduserid])) {
2692              self::add_contact($userid, $requesteduserid);
2693  
2694              $DB->delete_records('message_contact_requests', ['id' => $request->id]);
2695          }
2696      }
2697  
2698      /**
2699       * Handles declining a contact request.
2700       *
2701       * @param int $userid The id of the user who created the contact request
2702       * @param int $requesteduserid The id of the user declining the request
2703       */
2704      public static function decline_contact_request(int $userid, int $requesteduserid) {
2705          global $DB;
2706  
2707          if ($request = $DB->get_record('message_contact_requests', ['userid' => $userid,
2708                  'requesteduserid' => $requesteduserid])) {
2709              $DB->delete_records('message_contact_requests', ['id' => $request->id]);
2710          }
2711      }
2712  
2713      /**
2714       * Handles returning the contact requests for a user.
2715       *
2716       * This also includes the user data necessary to display information
2717       * about the user.
2718       *
2719       * It will not include blocked users.
2720       *
2721       * @param int $userid
2722       * @param int $limitfrom
2723       * @param int $limitnum
2724       * @return array The list of contact requests
2725       */
2726      public static function get_contact_requests(int $userid, int $limitfrom = 0, int $limitnum = 0) : array {
2727          global $DB;
2728  
2729          $sql = "SELECT mcr.userid
2730                    FROM {message_contact_requests} mcr
2731               LEFT JOIN {message_users_blocked} mub
2732                      ON (mub.userid = ? AND mub.blockeduserid = mcr.userid)
2733                   WHERE mcr.requesteduserid = ?
2734                     AND mub.id is NULL
2735                ORDER BY mcr.timecreated ASC";
2736          if ($contactrequests = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
2737              $userids = array_keys($contactrequests);
2738              return helper::get_member_info($userid, $userids);
2739          }
2740  
2741          return [];
2742      }
2743  
2744      /**
2745       * Returns the number of contact requests the user has received.
2746       *
2747       * @param int $userid The ID of the user we want to return the number of received contact requests for
2748       * @return int The count
2749       */
2750      public static function get_received_contact_requests_count(int $userid) : int {
2751          global $DB;
2752          $sql = "SELECT COUNT(mcr.id)
2753                    FROM {message_contact_requests} mcr
2754               LEFT JOIN {message_users_blocked} mub
2755                      ON mub.userid = mcr.requesteduserid AND mub.blockeduserid = mcr.userid
2756                   WHERE mcr.requesteduserid = :requesteduserid
2757                     AND mub.id IS NULL";
2758          $params = ['requesteduserid' => $userid];
2759          return $DB->count_records_sql($sql, $params);
2760      }
2761  
2762      /**
2763       * Handles adding a contact.
2764       *
2765       * @param int $userid The id of the user who requested to be a contact
2766       * @param int $contactid The id of the contact
2767       */
2768      public static function add_contact(int $userid, int $contactid) {
2769          global $DB;
2770  
2771          $messagecontact = new \stdClass();
2772          $messagecontact->userid = $userid;
2773          $messagecontact->contactid = $contactid;
2774          $messagecontact->timecreated = time();
2775          $messagecontact->id = $DB->insert_record('message_contacts', $messagecontact);
2776  
2777          $eventparams = [
2778              'objectid' => $messagecontact->id,
2779              'userid' => $userid,
2780              'relateduserid' => $contactid,
2781              'context' => \context_user::instance($userid)
2782          ];
2783          $event = \core\event\message_contact_added::create($eventparams);
2784          $event->add_record_snapshot('message_contacts', $messagecontact);
2785          $event->trigger();
2786      }
2787  
2788      /**
2789       * Handles removing a contact.
2790       *
2791       * @param int $userid The id of the user who is removing a user as a contact
2792       * @param int $contactid The id of the user to be removed as a contact
2793       */
2794      public static function remove_contact(int $userid, int $contactid) {
2795          global $DB;
2796  
2797          if ($contact = self::get_contact($userid, $contactid)) {
2798              $DB->delete_records('message_contacts', ['id' => $contact->id]);
2799  
2800              $event = \core\event\message_contact_removed::create(array(
2801                  'objectid' => $contact->id,
2802                  'userid' => $userid,
2803                  'relateduserid' => $contactid,
2804                  'context' => \context_user::instance($userid)
2805              ));
2806              $event->add_record_snapshot('message_contacts', $contact);
2807              $event->trigger();
2808          }
2809      }
2810  
2811      /**
2812       * Handles blocking a user.
2813       *
2814       * @param int $userid The id of the user who is blocking
2815       * @param int $usertoblockid The id of the user being blocked
2816       */
2817      public static function block_user(int $userid, int $usertoblockid) {
2818          global $DB;
2819  
2820          $blocked = new \stdClass();
2821          $blocked->userid = $userid;
2822          $blocked->blockeduserid = $usertoblockid;
2823          $blocked->timecreated = time();
2824          $blocked->id = $DB->insert_record('message_users_blocked', $blocked);
2825  
2826          // Trigger event for blocking a contact.
2827          $event = \core\event\message_user_blocked::create(array(
2828              'objectid' => $blocked->id,
2829              'userid' => $userid,
2830              'relateduserid' => $usertoblockid,
2831              'context' => \context_user::instance($userid)
2832          ));
2833          $event->add_record_snapshot('message_users_blocked', $blocked);
2834          $event->trigger();
2835      }
2836  
2837      /**
2838       * Handles unblocking a user.
2839       *
2840       * @param int $userid The id of the user who is unblocking
2841       * @param int $usertounblockid The id of the user being unblocked
2842       */
2843      public static function unblock_user(int $userid, int $usertounblockid) {
2844          global $DB;
2845  
2846          if ($blockeduser = $DB->get_record('message_users_blocked',
2847                  ['userid' => $userid, 'blockeduserid' => $usertounblockid])) {
2848              $DB->delete_records('message_users_blocked', ['id' => $blockeduser->id]);
2849  
2850              // Trigger event for unblocking a contact.
2851              $event = \core\event\message_user_unblocked::create(array(
2852                  'objectid' => $blockeduser->id,
2853                  'userid' => $userid,
2854                  'relateduserid' => $usertounblockid,
2855                  'context' => \context_user::instance($userid)
2856              ));
2857              $event->add_record_snapshot('message_users_blocked', $blockeduser);
2858              $event->trigger();
2859          }
2860      }
2861  
2862      /**
2863       * Checks if users are already contacts.
2864       *
2865       * @param int $userid The id of one of the users
2866       * @param int $contactid The id of the other user
2867       * @return bool Returns true if they are a contact, false otherwise
2868       */
2869      public static function is_contact(int $userid, int $contactid) : bool {
2870          global $DB;
2871  
2872          $sql = "SELECT id
2873                    FROM {message_contacts} mc
2874                   WHERE (mc.userid = ? AND mc.contactid = ?)
2875                      OR (mc.userid = ? AND mc.contactid = ?)";
2876          return $DB->record_exists_sql($sql, [$userid, $contactid, $contactid, $userid]);
2877      }
2878  
2879      /**
2880       * Returns the row in the database table message_contacts that represents the contact between two people.
2881       *
2882       * @param int $userid The id of one of the users
2883       * @param int $contactid The id of the other user
2884       * @return mixed A fieldset object containing the record, false otherwise
2885       */
2886      public static function get_contact(int $userid, int $contactid) {
2887          global $DB;
2888  
2889          $sql = "SELECT mc.*
2890                    FROM {message_contacts} mc
2891                   WHERE (mc.userid = ? AND mc.contactid = ?)
2892                      OR (mc.userid = ? AND mc.contactid = ?)";
2893          return $DB->get_record_sql($sql, [$userid, $contactid, $contactid, $userid]);
2894      }
2895  
2896      /**
2897       * Checks if a user is already blocked.
2898       *
2899       * @param int $userid
2900       * @param int $blockeduserid
2901       * @return bool Returns true if they are a blocked, false otherwise
2902       */
2903      public static function is_blocked(int $userid, int $blockeduserid) : bool {
2904          global $DB;
2905  
2906          return $DB->record_exists('message_users_blocked', ['userid' => $userid, 'blockeduserid' => $blockeduserid]);
2907      }
2908  
2909      /**
2910       * Get contact requests between users.
2911       *
2912       * @param int $userid The id of the user who is creating the contact request
2913       * @param int $requesteduserid The id of the user being requested
2914       * @return \stdClass[]
2915       */
2916      public static function get_contact_requests_between_users(int $userid, int $requesteduserid) : array {
2917          global $DB;
2918  
2919          $sql = "SELECT *
2920                    FROM {message_contact_requests} mcr
2921                   WHERE (mcr.userid = ? AND mcr.requesteduserid = ?)
2922                      OR (mcr.userid = ? AND mcr.requesteduserid = ?)";
2923          return $DB->get_records_sql($sql, [$userid, $requesteduserid, $requesteduserid, $userid]);
2924      }
2925  
2926      /**
2927       * Checks if a contact request already exists between users.
2928       *
2929       * @param int $userid The id of the user who is creating the contact request
2930       * @param int $requesteduserid The id of the user being requested
2931       * @return bool Returns true if a contact request exists, false otherwise
2932       */
2933      public static function does_contact_request_exist(int $userid, int $requesteduserid) : bool {
2934          global $DB;
2935  
2936          $sql = "SELECT id
2937                    FROM {message_contact_requests} mcr
2938                   WHERE (mcr.userid = ? AND mcr.requesteduserid = ?)
2939                      OR (mcr.userid = ? AND mcr.requesteduserid = ?)";
2940          return $DB->record_exists_sql($sql, [$userid, $requesteduserid, $requesteduserid, $userid]);
2941      }
2942  
2943      /**
2944       * Checks if a user is already in a conversation.
2945       *
2946       * @param int $userid The id of the user we want to check if they are in a group
2947       * @param int $conversationid The id of the conversation
2948       * @return bool Returns true if a contact request exists, false otherwise
2949       */
2950      public static function is_user_in_conversation(int $userid, int $conversationid) : bool {
2951          global $DB;
2952  
2953          return $DB->record_exists('message_conversation_members', ['conversationid' => $conversationid,
2954              'userid' => $userid]);
2955      }
2956  
2957      /**
2958       * Checks if the sender can message the recipient.
2959       *
2960       * @param int $recipientid
2961       * @param int $senderid
2962       * @param bool $evenifblocked This lets the user know, that even if the recipient has blocked the user
2963       *        the user is still able to send a message.
2964       * @return bool true if recipient hasn't blocked sender and sender can contact to recipient, false otherwise.
2965       */
2966      protected static function can_contact_user(int $recipientid, int $senderid, bool $evenifblocked = false) : bool {
2967          if (has_capability('moodle/site:messageanyuser', \context_system::instance(), $senderid) ||
2968              $recipientid == $senderid) {
2969              // The sender has the ability to contact any user across the entire site or themselves.
2970              return true;
2971          }
2972  
2973          // The initial value of $cancontact is null to indicate that a value has not been determined.
2974          $cancontact = null;
2975  
2976          if (self::is_blocked($recipientid, $senderid) || $evenifblocked) {
2977              // The recipient has specifically blocked this sender.
2978              $cancontact = false;
2979          }
2980  
2981          $sharedcourses = null;
2982          if (null === $cancontact) {
2983              // There are three user preference options:
2984              // - Site: Allow anyone not explicitly blocked to contact me;
2985              // - Course members: Allow anyone I am in a course with to contact me; and
2986              // - Contacts: Only allow my contacts to contact me.
2987              //
2988              // The Site option is only possible when the messagingallusers site setting is also enabled.
2989  
2990              $privacypreference = self::get_user_privacy_messaging_preference($recipientid);
2991              if (self::MESSAGE_PRIVACY_SITE === $privacypreference) {
2992                  // The user preference is to allow any user to contact them.
2993                  // No need to check anything else.
2994                  $cancontact = true;
2995              } else {
2996                  // This user only allows their own contacts, and possibly course peers, to contact them.
2997                  // If the users are contacts then we can avoid the more expensive shared courses check.
2998                  $cancontact = self::is_contact($senderid, $recipientid);
2999  
3000                  if (!$cancontact && self::MESSAGE_PRIVACY_COURSEMEMBER === $privacypreference) {
3001                      // The users are not contacts and the user allows course member messaging.
3002                      // Check whether these two users share any course together.
3003                      $sharedcourses = enrol_get_shared_courses($recipientid, $senderid, true);
3004                      $cancontact = (!empty($sharedcourses));
3005                  }
3006              }
3007          }
3008  
3009          if (false === $cancontact) {
3010              // At the moment the users cannot contact one another.
3011              // Check whether the messageanyuser capability applies in any of the shared courses.
3012              // This is intended to allow teachers to message students regardless of message settings.
3013  
3014              // Note: You cannot use empty($sharedcourses) here because this may be an empty array.
3015              if (null === $sharedcourses) {
3016                  $sharedcourses = enrol_get_shared_courses($recipientid, $senderid, true);
3017              }
3018  
3019              foreach ($sharedcourses as $course) {
3020                  // Note: enrol_get_shared_courses will preload any shared context.
3021                  if (has_capability('moodle/site:messageanyuser', \context_course::instance($course->id), $senderid)) {
3022                      $cancontact = true;
3023                      break;
3024                  }
3025              }
3026          }
3027  
3028          return $cancontact;
3029      }
3030  
3031      /**
3032       * Add some new members to an existing conversation.
3033       *
3034       * @param array $userids User ids array to add as members.
3035       * @param int $convid The conversation id. Must exists.
3036       * @throws \dml_missing_record_exception If convid conversation doesn't exist
3037       * @throws \dml_exception If there is a database error
3038       * @throws \moodle_exception If trying to add a member(s) to a non-group conversation
3039       */
3040      public static function add_members_to_conversation(array $userids, int $convid) {
3041          global $DB;
3042  
3043          $conversation = $DB->get_record('message_conversations', ['id' => $convid], '*', MUST_EXIST);
3044  
3045          // We can only add members to a group conversation.
3046          if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_GROUP) {
3047              throw new \moodle_exception('You can not add members to a non-group conversation.');
3048          }
3049  
3050          // Be sure we are not trying to add a non existing user to the conversation. Work only with existing users.
3051          list($useridcondition, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
3052          $existingusers = $DB->get_fieldset_select('user', 'id', "id $useridcondition", $params);
3053  
3054          // Be sure we are not adding a user is already member of the conversation. Take all the members.
3055          $memberuserids = array_values($DB->get_records_menu(
3056              'message_conversation_members', ['conversationid' => $convid], 'id', 'id, userid')
3057          );
3058  
3059          // Work with existing new members.
3060          $members = array();
3061          $newuserids = array_diff($existingusers, $memberuserids);
3062          foreach ($newuserids as $userid) {
3063              $member = new \stdClass();
3064              $member->conversationid = $convid;
3065              $member->userid = $userid;
3066              $member->timecreated = time();
3067              $members[] = $member;
3068          }
3069  
3070          $DB->insert_records('message_conversation_members', $members);
3071      }
3072  
3073      /**
3074       * Remove some members from an existing conversation.
3075       *
3076       * @param array $userids The user ids to remove from conversation members.
3077       * @param int $convid The conversation id. Must exists.
3078       * @throws \dml_exception
3079       * @throws \moodle_exception If trying to remove a member(s) from a non-group conversation
3080       */
3081      public static function remove_members_from_conversation(array $userids, int $convid) {
3082          global $DB;
3083  
3084          $conversation = $DB->get_record('message_conversations', ['id' => $convid], '*', MUST_EXIST);
3085  
3086          if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_GROUP) {
3087              throw new \moodle_exception('You can not remove members from a non-group conversation.');
3088          }
3089  
3090          list($useridcondition, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
3091          $params['convid'] = $convid;
3092  
3093          $DB->delete_records_select('message_conversation_members',
3094              "conversationid = :convid AND userid $useridcondition", $params);
3095      }
3096  
3097      /**
3098       * Count conversation members.
3099       *
3100       * @param int $convid The conversation id.
3101       * @return int Number of conversation members.
3102       * @throws \dml_exception
3103       */
3104      public static function count_conversation_members(int $convid) : int {
3105          global $DB;
3106  
3107          return $DB->count_records('message_conversation_members', ['conversationid' => $convid]);
3108      }
3109  
3110      /**
3111       * Checks whether or not a conversation area is enabled.
3112       *
3113       * @param string $component Defines the Moodle component which the area was added to.
3114       * @param string $itemtype Defines the type of the component.
3115       * @param int $itemid The id of the component.
3116       * @param int $contextid The id of the context.
3117       * @return bool Returns if a conversation area exists and is enabled, false otherwise
3118       */
3119      public static function is_conversation_area_enabled(string $component, string $itemtype, int $itemid, int $contextid) : bool {
3120          global $DB;
3121  
3122          return $DB->record_exists('message_conversations',
3123              [
3124                  'itemid' => $itemid,
3125                  'contextid' => $contextid,
3126                  'component' => $component,
3127                  'itemtype' => $itemtype,
3128                  'enabled' => self::MESSAGE_CONVERSATION_ENABLED
3129              ]
3130          );
3131      }
3132  
3133      /**
3134       * Get conversation by area.
3135       *
3136       * @param string $component Defines the Moodle component which the area was added to.
3137       * @param string $itemtype Defines the type of the component.
3138       * @param int $itemid The id of the component.
3139       * @param int $contextid The id of the context.
3140       * @return \stdClass
3141       */
3142      public static function get_conversation_by_area(string $component, string $itemtype, int $itemid, int $contextid) {
3143          global $DB;
3144  
3145          return $DB->get_record('message_conversations',
3146              [
3147                  'itemid' => $itemid,
3148                  'contextid' => $contextid,
3149                  'component' => $component,
3150                  'itemtype'  => $itemtype
3151              ]
3152          );
3153      }
3154  
3155      /**
3156       * Enable a conversation.
3157       *
3158       * @param int $conversationid The id of the conversation.
3159       * @return void
3160       */
3161      public static function enable_conversation(int $conversationid) {
3162          global $DB;
3163  
3164          $conversation = new \stdClass();
3165          $conversation->id = $conversationid;
3166          $conversation->enabled = self::MESSAGE_CONVERSATION_ENABLED;
3167          $conversation->timemodified = time();
3168          $DB->update_record('message_conversations', $conversation);
3169      }
3170  
3171      /**
3172       * Disable a conversation.
3173       *
3174       * @param int $conversationid The id of the conversation.
3175       * @return void
3176       */
3177      public static function disable_conversation(int $conversationid) {
3178          global $DB;
3179  
3180          $conversation = new \stdClass();
3181          $conversation->id = $conversationid;
3182          $conversation->enabled = self::MESSAGE_CONVERSATION_DISABLED;
3183          $conversation->timemodified = time();
3184          $DB->update_record('message_conversations', $conversation);
3185      }
3186  
3187      /**
3188       * Update the name of a conversation.
3189       *
3190       * @param int $conversationid The id of a conversation.
3191       * @param string $name The main name of the area
3192       * @return void
3193       */
3194      public static function update_conversation_name(int $conversationid, string $name) {
3195          global $DB;
3196  
3197          if ($conversation = $DB->get_record('message_conversations', array('id' => $conversationid))) {
3198              if ($name <> $conversation->name) {
3199                  $conversation->name = $name;
3200                  $conversation->timemodified = time();
3201                  $DB->update_record('message_conversations', $conversation);
3202              }
3203          }
3204      }
3205  
3206      /**
3207       * Returns a list of conversation members.
3208       *
3209       * @param int $userid The user we are returning the conversation members for, used by helper::get_member_info.
3210       * @param int $conversationid The id of the conversation
3211       * @param bool $includecontactrequests Do we want to include contact requests with this data?
3212       * @param bool $includeprivacyinfo Do we want to include privacy requests with this data?
3213       * @param int $limitfrom
3214       * @param int $limitnum
3215       * @return array
3216       */
3217      public static function get_conversation_members(int $userid, int $conversationid, bool $includecontactrequests = false,
3218                                                      bool $includeprivacyinfo = false, int $limitfrom = 0,
3219                                                      int $limitnum = 0) : array {
3220          global $DB;
3221  
3222          if ($members = $DB->get_records('message_conversation_members', ['conversationid' => $conversationid],
3223                  'timecreated ASC, id ASC', 'userid', $limitfrom, $limitnum)) {
3224              $userids = array_keys($members);
3225              $members = helper::get_member_info($userid, $userids, $includecontactrequests, $includeprivacyinfo);
3226  
3227              return $members;
3228          }
3229  
3230          return [];
3231      }
3232  
3233      /**
3234       * Get the unread counts for all conversations for the user, sorted by type, and including favourites.
3235       *
3236       * @param int $userid the id of the user whose conversations we'll check.
3237       * @return array the unread counts for each conversation, indexed by type.
3238       */
3239      public static function get_unread_conversation_counts(int $userid) : array {
3240          global $DB;
3241  
3242          // Get all conversations the user is in, and check unread.
3243          $unreadcountssql = 'SELECT conv.id, conv.type, indcounts.unreadcount
3244                                FROM {message_conversations} conv
3245                          INNER JOIN (
3246                                        SELECT m.conversationid, count(m.id) as unreadcount
3247                                          FROM {messages} m
3248                                    INNER JOIN {message_conversations} mc
3249                                            ON mc.id = m.conversationid
3250                                    INNER JOIN {message_conversation_members} mcm
3251                                            ON m.conversationid = mcm.conversationid
3252                                     LEFT JOIN {message_user_actions} mua
3253                                            ON (mua.messageid = m.id AND mua.userid = ? AND
3254                                               (mua.action = ? OR mua.action = ?))
3255                                         WHERE mcm.userid = ?
3256                                           AND m.useridfrom != ?
3257                                           AND mua.id is NULL
3258                                      GROUP BY m.conversationid
3259                                     ) indcounts
3260                                  ON indcounts.conversationid = conv.id
3261                               WHERE conv.enabled = 1';
3262  
3263          $unreadcounts = $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, self::MESSAGE_ACTION_DELETED,
3264              $userid, $userid]);
3265  
3266          // Get favourites, so we can track these separately.
3267          $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
3268          $favouriteconversations = $service->find_favourites_by_type('core_message', 'message_conversations');
3269          $favouriteconvids = array_flip(array_column($favouriteconversations, 'itemid'));
3270  
3271          // Assemble the return array.
3272          $counts = ['favourites' => 0, 'types' => [
3273              self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
3274              self::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
3275              self::MESSAGE_CONVERSATION_TYPE_SELF => 0
3276          ]];
3277          foreach ($unreadcounts as $convid => $info) {
3278              if (isset($favouriteconvids[$convid])) {
3279                  $counts['favourites']++;
3280                  continue;
3281              }
3282              $counts['types'][$info->type]++;
3283          }
3284  
3285          return $counts;
3286      }
3287  
3288      /**
3289       * Handles muting a conversation.
3290       *
3291       * @param int $userid The id of the user
3292       * @param int $conversationid The id of the conversation
3293       */
3294      public static function mute_conversation(int $userid, int $conversationid) : void {
3295          global $DB;
3296  
3297          $mutedconversation = new \stdClass();
3298          $mutedconversation->userid = $userid;
3299          $mutedconversation->conversationid = $conversationid;
3300          $mutedconversation->action = self::CONVERSATION_ACTION_MUTED;
3301          $mutedconversation->timecreated = time();
3302  
3303          $DB->insert_record('message_conversation_actions', $mutedconversation);
3304      }
3305  
3306      /**
3307       * Handles unmuting a conversation.
3308       *
3309       * @param int $userid The id of the user
3310       * @param int $conversationid The id of the conversation
3311       */
3312      public static function unmute_conversation(int $userid, int $conversationid) : void {
3313          global $DB;
3314  
3315          $DB->delete_records('message_conversation_actions',
3316              [
3317                  'userid' => $userid,
3318                  'conversationid' => $conversationid,
3319                  'action' => self::CONVERSATION_ACTION_MUTED
3320              ]
3321          );
3322      }
3323  
3324      /**
3325       * Checks whether a conversation is muted or not.
3326       *
3327       * @param int $userid The id of the user
3328       * @param int $conversationid The id of the conversation
3329       * @return bool Whether or not the conversation is muted or not
3330       */
3331      public static function is_conversation_muted(int $userid, int $conversationid) : bool {
3332          global $DB;
3333  
3334          return $DB->record_exists('message_conversation_actions',
3335              [
3336                  'userid' => $userid,
3337                  'conversationid' => $conversationid,
3338                  'action' => self::CONVERSATION_ACTION_MUTED
3339              ]
3340          );
3341      }
3342  
3343      /**
3344       * Completely removes all related data in the DB for a given conversation.
3345       *
3346       * @param int $conversationid The id of the conversation
3347       */
3348      public static function delete_all_conversation_data(int $conversationid) {
3349          global $DB;
3350  
3351          $conv = $DB->get_record('message_conversations', ['id' => $conversationid], 'id, contextid');
3352          $convcontext = !empty($conv->contextid) ? \context::instance_by_id($conv->contextid) : null;
3353  
3354          $DB->delete_records('message_conversations', ['id' => $conversationid]);
3355          $DB->delete_records('message_conversation_members', ['conversationid' => $conversationid]);
3356          $DB->delete_records('message_conversation_actions', ['conversationid' => $conversationid]);
3357  
3358          // Now, go through and delete any messages and related message actions for the conversation.
3359          if ($messages = $DB->get_records('messages', ['conversationid' => $conversationid])) {
3360              $messageids = array_keys($messages);
3361  
3362              list($insql, $inparams) = $DB->get_in_or_equal($messageids);
3363              $DB->delete_records_select('message_user_actions', "messageid $insql", $inparams);
3364  
3365              // Delete the messages now.
3366              $DB->delete_records('messages', ['conversationid' => $conversationid]);
3367          }
3368  
3369          // Delete all favourite records for all users relating to this conversation.
3370          $service = \core_favourites\service_factory::get_service_for_component('core_message');
3371          $service->delete_favourites_by_type_and_item('message_conversations', $conversationid, $convcontext);
3372      }
3373  
3374      /**
3375       * Checks if a user can delete a message for all users.
3376       *
3377       * @param int $userid the user id of who we want to delete the message for all users
3378       * @param int $messageid The message id
3379       * @return bool Returns true if a user can delete the message for all users, false otherwise.
3380       */
3381      public static function can_delete_message_for_all_users(int $userid, int $messageid) : bool {
3382          global $DB;
3383  
3384          $sql = "SELECT mc.id, mc.contextid
3385                    FROM {message_conversations} mc
3386              INNER JOIN {messages} m
3387                      ON mc.id = m.conversationid
3388                   WHERE m.id = :messageid";
3389          $conversation = $DB->get_record_sql($sql, ['messageid' => $messageid]);
3390  
3391          if (!empty($conversation->contextid)) {
3392              return has_capability('moodle/site:deleteanymessage',
3393                  \context::instance_by_id($conversation->contextid), $userid);
3394          }
3395  
3396          return has_capability('moodle/site:deleteanymessage', \context_system::instance(), $userid);
3397      }
3398      /**
3399       * Delete a message for all users.
3400       *
3401       * This function does not verify any permissions.
3402       *
3403       * @param int $messageid The message id
3404       * @return void
3405       */
3406      public static function delete_message_for_all_users(int $messageid) {
3407          global $DB, $USER;
3408  
3409          if (!$DB->record_exists('messages', ['id' => $messageid])) {
3410              return false;
3411          }
3412  
3413          // Get all members in the conversation where the message belongs.
3414          $membersql = "SELECT mcm.id, mcm.userid
3415                          FROM {message_conversation_members} mcm
3416                    INNER JOIN {messages} m
3417                            ON mcm.conversationid = m.conversationid
3418                         WHERE m.id = :messageid";
3419          $params = [
3420              'messageid' => $messageid
3421          ];
3422          $members = $DB->get_records_sql($membersql, $params);
3423          if ($members) {
3424              foreach ($members as $member) {
3425                  self::delete_message($member->userid, $messageid);
3426              }
3427          }
3428      }
3429  
3430      /**
3431       * Create a self conversation for a user, only if one doesn't already exist.
3432       *
3433       * @param int $userid the user to whom the conversation belongs.
3434       */
3435      protected static function lazy_create_self_conversation(int $userid) : void {
3436          global $DB;
3437          // Check if the self-conversation for this user exists.
3438          // If not, create and star it for the user.
3439          // Don't use the API methods here, as they in turn may rely on
3440          // lazy creation and we'll end up with recursive loops of doom.
3441          $conditions = [
3442              'type' => self::MESSAGE_CONVERSATION_TYPE_SELF,
3443              'convhash' => helper::get_conversation_hash([$userid])
3444          ];
3445          if (empty($DB->get_record('message_conversations', $conditions))) {
3446              $selfconversation = self::create_conversation(self::MESSAGE_CONVERSATION_TYPE_SELF, [$userid]);
3447              self::set_favourite_conversation($selfconversation->id, $userid);
3448          }
3449      }
3450  }