Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401]

   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   * New messaging manager class.
  19   *
  20   * @package   core_message
  21   * @since     Moodle 2.8
  22   * @copyright 2014 Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
  23   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   * @author    Petr Skoda <petr.skoda@totaralms.com>
  25   */
  26  
  27  namespace core\message;
  28  
  29  defined('MOODLE_INTERNAL') || die();
  30  
  31  /**
  32   * Class used for various messaging related stuff.
  33   *
  34   * Note: Do NOT use directly in your code, it is intended to be used from core code only.
  35   *
  36   * @access private
  37   *
  38   * @package   core_message
  39   * @since     Moodle 2.8
  40   * @copyright 2014 Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
  41   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  42   * @author    Petr Skoda <petr.skoda@totaralms.com>
  43   */
  44  class manager {
  45      /** @var array buffer of pending messages */
  46      protected static $buffer = array();
  47  
  48      /** @var array buffer of pending messages to conversations */
  49      protected static $convmessagebuffer = array();
  50  
  51      /**
  52       * Used for calling processors, and generating event data when sending a message to a conversation.
  53       *
  54       * This is ONLY used for messages of type 'message' (notification=0), and is responsible for:
  55       *
  56       * 1. generation of per-user event data (to pass to processors)
  57       * 2. generation of the processors for each recipient member of the conversation
  58       * 3. calling said processors for each member, passing in the per-user (local) eventdata.
  59       * 4. generation of an appropriate event for the message send, depending on the conversation type
  60       *   - messages to individual conversations generate a 'message_sent' event (as per legacy send_message())
  61       *   - messages to group conversations generate a 'group_message_sent' event.
  62       *
  63       * @param message $eventdata
  64       * @param \stdClass $savemessage
  65       * @return int
  66       */
  67      public static function send_message_to_conversation(message $eventdata, \stdClass $savemessage) : int {
  68          global $DB, $CFG, $SITE;
  69  
  70          if (empty($eventdata->convid)) {
  71              throw new \moodle_exception("Message is not being sent to a conversation. Please check event data.");
  72          }
  73  
  74          // Fetch default (site) preferences.
  75          $defaultpreferences = get_message_output_default_preferences();
  76          $preferencebase = $eventdata->component.'_'.$eventdata->name;
  77  
  78          // Because we're dealing with multiple recipients, we need to send a localised (per-user) version of the eventdata to each
  79          // processor, because of things like the language-specific subject. We're going to modify this, for each recipient member.
  80          // Any time we're modifying the event data here, we should be using the localised version.
  81          // This localised version is based on the generic event data, but we should keep that object intact, so we clone it.
  82          $localisedeventdata = clone $eventdata;
  83  
  84          // Get user records for all members of the conversation.
  85          // We must fetch distinct users, because it's possible for a user to message themselves via bulk user actions.
  86          // In such cases, there will be 2 records referring to the same user.
  87          $sql = "SELECT u.*, mca.id as ismuted
  88                    FROM {user} u
  89               LEFT JOIN {message_conversation_actions} mca
  90                      ON mca.userid = u.id AND mca.conversationid = ? AND mca.action = ?
  91                   WHERE u.id IN (
  92                            SELECT mcm.userid FROM {message_conversation_members} mcm
  93                             WHERE mcm.conversationid = ?
  94                   )";
  95          $members = $DB->get_records_sql($sql, [$eventdata->convid, \core_message\api::CONVERSATION_ACTION_MUTED,
  96              $eventdata->convid]);
  97          if (empty($members)) {
  98              throw new \moodle_exception("Conversation has no members or does not exist.");
  99          }
 100  
 101          if (!is_object($localisedeventdata->userfrom)) {
 102              $localisedeventdata->userfrom = $members[$localisedeventdata->userfrom];
 103          }
 104  
 105          // This should now hold only the other users (recipients).
 106          unset($members[$localisedeventdata->userfrom->id]);
 107          $otherusers = $members;
 108  
 109          // Get conversation type and name. We'll use this to determine which message subject to generate, depending on type.
 110          $conv = $DB->get_record('message_conversations', ['id' => $eventdata->convid], 'id, type, name');
 111  
 112          // For now Self conversations are not processed because users are aware of the messages sent by themselves, so we
 113          // can return early.
 114          if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF) {
 115              return $savemessage->id;
 116          }
 117          $localisedeventdata->conversationtype = $conv->type;
 118  
 119          // We treat individual conversations the same as any direct message with 'userfrom' and 'userto' specified.
 120          // We know the other user, so set the 'userto' field so that the event code will get access to this field.
 121          // If this was a legacy caller (eventdata->userto is set), then use that instead, as we want to use the fields specified
 122          // in that object instead of using one fetched from the DB.
 123          $legacymessage = false;
 124          if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
 125              if (isset($eventdata->userto)) {
 126                  $legacymessage = true;
 127              } else {
 128                  $otheruser = reset($otherusers);
 129                  $eventdata->userto = $otheruser;
 130              }
 131          }
 132  
 133          // Fetch enabled processors.
 134          // If we are dealing with a message some processors may want to handle it regardless of user and site settings.
 135          $processors = array_filter(get_message_processors(false), function($processor) {
 136              if ($processor->object->force_process_messages()) {
 137                  return true;
 138              }
 139  
 140              return ($processor->enabled && $processor->configured);
 141          });
 142  
 143          // For each member of the conversation, other than the sender:
 144          // 1. Set recipient specific event data (language specific, user prefs, etc)
 145          // 2. Generate recipient specific processor list
 146          // 3. Call send_message() to pass the message to processors and generate the relevant per-user events.
 147          $eventprocmaps = []; // Init the event/processors buffer.
 148          foreach ($otherusers as $recipient) {
 149              // If this message was a legacy (1:1) message, then we use the userto.
 150              if ($legacymessage) {
 151                  $ismuted = $recipient->ismuted;
 152  
 153                  $recipient = $eventdata->userto;
 154                  $recipient->ismuted = $ismuted;
 155              }
 156  
 157              $usertoisrealuser = (\core_user::is_real_user($recipient->id) != false);
 158  
 159              // Using string manager directly so that strings in the message will be in the message recipients language rather than
 160              // the sender's.
 161              if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
 162                  $localisedeventdata->subject = get_string_manager()->get_string('unreadnewmessage', 'message',
 163                      fullname($localisedeventdata->userfrom), $recipient->lang);
 164              } else if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP) {
 165                  $stringdata = (object) ['name' => fullname($localisedeventdata->userfrom), 'conversationname' => $conv->name];
 166                  $localisedeventdata->subject = get_string_manager()->get_string('unreadnewgroupconversationmessage', 'message',
 167                      $stringdata, $recipient->lang);
 168              }
 169  
 170              // Spoof the userto based on the current member id.
 171              $localisedeventdata->userto = $recipient;
 172              // Check if the notification is including images that will need a user token to be displayed outside Moodle.
 173              if (!empty($localisedeventdata->customdata)) {
 174                  $customdata = json_decode($localisedeventdata->customdata);
 175                  if (is_object($customdata) && !empty($customdata->notificationiconurl)) {
 176                      $customdata->tokenpluginfile = get_user_key('core_files', $localisedeventdata->userto->id);
 177                      $localisedeventdata->customdata = $customdata; // Message class will JSON encode again.
 178                  }
 179              }
 180  
 181              $s = new \stdClass();
 182              $s->sitename = format_string($SITE->shortname, true, array('context' => \context_course::instance(SITEID)));
 183              $s->url = $CFG->wwwroot.'/message/index.php?id='.$eventdata->userfrom->id;
 184              $emailtagline = get_string_manager()->get_string('emailtagline', 'message', $s, $recipient->lang);
 185  
 186              $localisedeventdata->fullmessage = $eventdata->fullmessage;
 187              $localisedeventdata->fullmessagehtml = $eventdata->fullmessagehtml;
 188              if (!empty($localisedeventdata->fullmessage)) {
 189                  // Prevent unclosed HTML elements.
 190                  $localisedeventdata->fullmessage =
 191                      \core_message\helper::prevent_unclosed_html_tags($localisedeventdata->fullmessage, true);
 192  
 193                  $localisedeventdata->fullmessage .= "\n\n---------------------------------------------------------------------\n"
 194                      . $emailtagline;
 195              }
 196              if (!empty($localisedeventdata->fullmessagehtml)) {
 197                  // Prevent unclosed HTML elements.
 198                  $localisedeventdata->fullmessagehtml =
 199                      \core_message\helper::prevent_unclosed_html_tags($localisedeventdata->fullmessagehtml, true);
 200  
 201                  $localisedeventdata->fullmessagehtml .=
 202                      "<br><br>---------------------------------------------------------------------<br>" . $emailtagline;
 203              }
 204  
 205              // If recipient is internal user (noreply user), and emailstop is set then don't send any msg.
 206              if (!$usertoisrealuser && !empty($recipient->emailstop)) {
 207                  debugging('Attempt to send msg to internal (noreply) user', DEBUG_NORMAL);
 208                  return false;
 209              }
 210  
 211              // Fill in the array of processors to be used based on default and user preferences.
 212              // Do not process muted conversations.
 213              $processorlist = [];
 214              if (!$recipient->ismuted) {
 215                  foreach ($processors as $processor) {
 216                      // Skip adding processors for internal user, if processor doesn't support sending message to internal user.
 217                      if (!$usertoisrealuser && !$processor->object->can_send_to_any_users()) {
 218                          continue;
 219                      }
 220  
 221                      // First find out permissions.
 222                      $defaultlockedpreference = $processor->name . '_provider_' . $preferencebase . '_locked';
 223                      $locked = false;
 224                      if (isset($defaultpreferences->{$defaultlockedpreference})) {
 225                          $locked = $defaultpreferences->{$defaultlockedpreference};
 226                      } else {
 227                          // MDL-25114 They supplied an $eventdata->component $eventdata->name combination which doesn't
 228                          // exist in the message_provider table (thus there is no default settings for them).
 229                          $preferrormsg = "Could not load preference $defaultlockedpreference.
 230                       Make sure the component and name you supplied to message_send() are valid.";
 231                          throw new \coding_exception($preferrormsg);
 232                      }
 233  
 234                      $enabledpreference = 'message_provider_'.$preferencebase . '_enabled';
 235                      $forced = false;
 236                      if ($locked && isset($defaultpreferences->{$enabledpreference})) {
 237                          $forced = $defaultpreferences->{$enabledpreference};
 238                      }
 239  
 240                      // Find out if user has configured this output.
 241                      // Some processors cannot function without settings from the user.
 242                      $userisconfigured = $processor->object->is_user_configured($recipient);
 243  
 244                      // DEBUG: notify if we are forcing unconfigured output.
 245                      if ($forced && !$userisconfigured) {
 246                          debugging('Attempt to force message delivery to user who has "' . $processor->name .
 247                              '" output unconfigured', DEBUG_NORMAL);
 248                      }
 249  
 250                      // Populate the list of processors we will be using.
 251                      if (!$eventdata->notification && $processor->object->force_process_messages()) {
 252                          $processorlist[] = $processor->name;
 253                      } else if ($forced && $userisconfigured) {
 254                          // An admin is forcing users to use this message processor. Use this processor unconditionally.
 255                          $processorlist[] = $processor->name;
 256                      } else if (!$locked && $userisconfigured && !$recipient->emailstop) {
 257                          // User has not disabled notifications.
 258                          // See if user set any notification preferences, otherwise use site default ones.
 259                          $userpreferencename = 'message_provider_' . $preferencebase . '_enabled';
 260                          if ($userpreference = get_user_preferences($userpreferencename, null, $recipient)) {
 261                              if (in_array($processor->name, explode(',', $userpreference))) {
 262                                  $processorlist[] = $processor->name;
 263                              }
 264                          } else if (isset($defaultpreferences->{$userpreferencename})) {
 265                              if (in_array($processor->name, explode(',', $defaultpreferences->{$userpreferencename}))) {
 266                                  $processorlist[] = $processor->name;
 267                              }
 268                          }
 269                      }
 270                  }
 271              }
 272              // Batch up the localised event data and processor list for all users into a local buffer.
 273              $eventprocmaps[] = [clone($localisedeventdata), $processorlist];
 274          }
 275          // Then pass it off as one item of work, to be processed by send_conversation_message_to_processors(), which will
 276          // handle all transaction buffering logic.
 277          self::send_conversation_message_to_processors($eventprocmaps, $eventdata, $savemessage);
 278  
 279          return $savemessage->id;
 280      }
 281  
 282      /**
 283       * Takes a list of localised event data, and tries to send them to their respective member's message processors.
 284       *
 285       * Input format:
 286       *  [CONVID => [$localisedeventdata, $savemessage, $processorlist], ].
 287       *
 288       * @param array $eventprocmaps the array of localised event data and processors for each member of the conversation.
 289       * @param message $eventdata the original conversation message eventdata
 290       * @param \stdClass $savemessage the saved message record.
 291       * @throws \coding_exception
 292       */
 293      protected static function send_conversation_message_to_processors(array $eventprocmaps, message $eventdata,
 294                                                                        \stdClass $savemessage) {
 295          global $DB;
 296  
 297          // We cannot communicate with external systems in DB transactions,
 298          // buffer the messages if necessary.
 299          if ($DB->is_transaction_started()) {
 300              // Buffer this group conversation message and it's record.
 301              self::$convmessagebuffer[] = [$eventprocmaps, $eventdata, $savemessage];
 302              return;
 303          }
 304  
 305          // Send each localised version of the event data to each member's respective processors.
 306          foreach ($eventprocmaps as $eventprocmap) {
 307              $eventdata = $eventprocmap[0];
 308              $processorlist = $eventprocmap[1];
 309              self::call_processors($eventdata, $processorlist);
 310          }
 311  
 312          // Trigger event for sending a message or notification - we need to do this before marking as read!
 313          self::trigger_message_events($eventdata, $savemessage);
 314      }
 315  
 316      /**
 317       * Do the message sending.
 318       *
 319       * NOTE: to be used from message_send() only.
 320       *
 321       * @param \core\message\message $eventdata fully prepared event data for processors
 322       * @param \stdClass $savemessage the message saved in 'message' table
 323       * @param array $processorlist list of processors for target user
 324       * @return int $messageid the id from 'messages' (false is not returned)
 325       */
 326      public static function send_message(message $eventdata, \stdClass $savemessage, array $processorlist) {
 327          global $CFG;
 328  
 329          require_once($CFG->dirroot.'/message/lib.php'); // This is most probably already included from messagelib.php file.
 330  
 331          if (empty($processorlist)) {
 332              // Trigger event for sending a message or notification - we need to do this before marking as read!
 333              self::trigger_message_events($eventdata, $savemessage);
 334  
 335              if ($eventdata->notification) {
 336                  // If they have deselected all processors and it's a notification mark it read. The user doesn't want to be
 337                  // bothered.
 338                  $savemessage->timeread = null;
 339                  \core_message\api::mark_notification_as_read($savemessage);
 340              } else if (empty($CFG->messaging)) {
 341                  // If it's a message and messaging is disabled mark it read.
 342                  \core_message\api::mark_message_as_read($eventdata->userto->id, $savemessage);
 343              }
 344  
 345              return $savemessage->id;
 346          }
 347  
 348          // Let the manager do the sending or buffering when db transaction in progress.
 349          return self::send_message_to_processors($eventdata, $savemessage, $processorlist);
 350      }
 351  
 352      /**
 353       * Send message to message processors.
 354       *
 355       * @param \stdClass|\core\message\message $eventdata
 356       * @param \stdClass $savemessage
 357       * @param array $processorlist
 358       * @throws \moodle_exception
 359       * @return int $messageid
 360       */
 361      protected static function send_message_to_processors($eventdata, \stdClass $savemessage, array
 362      $processorlist) {
 363          global $CFG, $DB;
 364  
 365          // We cannot communicate with external systems in DB transactions,
 366          // buffer the messages if necessary.
 367          if ($DB->is_transaction_started()) {
 368              // We need to clone all objects so that devs may not modify it from outside later.
 369              $eventdata = clone($eventdata);
 370              $eventdata->userto = clone($eventdata->userto);
 371              $eventdata->userfrom = clone($eventdata->userfrom);
 372  
 373              // Conserve some memory the same was as $USER setup does.
 374              unset($eventdata->userto->description);
 375              unset($eventdata->userfrom->description);
 376  
 377              self::$buffer[] = array($eventdata, $savemessage, $processorlist);
 378              return $savemessage->id;
 379          }
 380  
 381          // Send the message to processors.
 382          if (!self::call_processors($eventdata, $processorlist)) {
 383              throw new \moodle_exception("Message was not sent.");
 384          }
 385  
 386          // Trigger event for sending a message or notification - we need to do this before marking as read!
 387          self::trigger_message_events($eventdata, $savemessage);
 388  
 389          if (!$eventdata->notification && empty($CFG->messaging)) {
 390              // If it's a message and messaging is disabled mark it read.
 391              \core_message\api::mark_message_as_read($eventdata->userto->id, $savemessage);
 392          }
 393  
 394          return $savemessage->id;
 395      }
 396  
 397      /**
 398       * Notification from DML layer.
 399       *
 400       * Note: to be used from DML layer only.
 401       */
 402      public static function database_transaction_commited() {
 403          if (!self::$buffer && !self::$convmessagebuffer) {
 404              return;
 405          }
 406          self::process_buffer();
 407      }
 408  
 409      /**
 410       * Notification from DML layer.
 411       *
 412       * Note: to be used from DML layer only.
 413       */
 414      public static function database_transaction_rolledback() {
 415          self::$buffer = array();
 416          self::$convmessagebuffer = array();
 417      }
 418  
 419      /**
 420       * Sent out any buffered messages if necessary.
 421       */
 422      protected static function process_buffer() {
 423          // Reset the buffers first in case we get exception from processor.
 424          $messages = self::$buffer;
 425          self::$buffer = array();
 426          $convmessages = self::$convmessagebuffer;
 427          self::$convmessagebuffer = array();
 428  
 429          foreach ($messages as $message) {
 430              list($eventdata, $savemessage, $processorlist) = $message;
 431              self::send_message_to_processors($eventdata, $savemessage, $processorlist);
 432          }
 433  
 434          foreach ($convmessages as $convmessage) {
 435              list($eventprocmap, $eventdata, $savemessage) = $convmessage;
 436              self::send_conversation_message_to_processors($eventprocmap, $eventdata, $savemessage);
 437          }
 438      }
 439  
 440      /**
 441       * Trigger an appropriate message creation event, based on the supplied $eventdata and $savemessage.
 442       *
 443       * @param message $eventdata the eventdata for the message.
 444       * @param \stdClass $savemessage the message record.
 445       * @throws \coding_exception
 446       */
 447      protected static function trigger_message_events(message $eventdata, \stdClass $savemessage) {
 448          global $DB;
 449          if ($eventdata->notification) {
 450              \core\event\notification_sent::create_from_ids(
 451                  $eventdata->userfrom->id,
 452                  $eventdata->userto->id,
 453                  $savemessage->id,
 454                  $eventdata->courseid
 455              )->trigger();
 456          } else { // Must be a message.
 457              // If the message is a group conversation, then trigger the 'group_message_sent' event.
 458              if ($eventdata->convid) {
 459                  $conv = $DB->get_record('message_conversations', ['id' => $eventdata->convid], 'id, type');
 460                  if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP) {
 461                      \core\event\group_message_sent::create_from_ids(
 462                          $eventdata->userfrom->id,
 463                          $eventdata->convid,
 464                          $savemessage->id,
 465                          $eventdata->courseid
 466                      )->trigger();
 467                      return;
 468                  }
 469                  // Individual type conversations fall through to the default 'message_sent' event.
 470              }
 471              \core\event\message_sent::create_from_ids(
 472                  $eventdata->userfrom->id,
 473                  $eventdata->userto->id,
 474                  $savemessage->id,
 475                  $eventdata->courseid
 476              )->trigger();
 477          }
 478      }
 479  
 480      /**
 481       * For each processor, call it's send_message() method.
 482       *
 483       * @param message $eventdata the message object.
 484       * @param array $processorlist the list of processors for a single user.
 485       * @return bool false if error calling message processor
 486       */
 487      protected static function call_processors(message $eventdata, array $processorlist) {
 488          // Allow plugins to change the message/notification data before sending it.
 489          $pluginsfunction = get_plugins_with_function('pre_processor_message_send');
 490          $sendmsgsuccessful = true;
 491          foreach ($processorlist as $procname) {
 492              // Let new messaging class add custom content based on the processor.
 493              $proceventdata = ($eventdata instanceof message) ? $eventdata->get_eventobject_for_processor($procname) : $eventdata;
 494  
 495              if ($pluginsfunction) {
 496                  foreach ($pluginsfunction as $plugintype => $plugins) {
 497                      foreach ($plugins as $pluginfunction) {
 498                          $pluginfunction($procname, $proceventdata);
 499                      }
 500                  }
 501              }
 502  
 503              $stdproc = new \stdClass();
 504              $stdproc->name = $procname;
 505              $processor = \core_message\api::get_processed_processor_object($stdproc);
 506              if (!$processor->object->send_message($proceventdata)) {
 507                  debugging('Error calling message processor ' . $procname);
 508                  $sendmsgsuccessful = false;
 509              }
 510          }
 511          return $sendmsgsuccessful;
 512      }
 513  }