Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

   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              // Set the online state.
 212              if (isset($CFG->block_online_users_timetosee)) {
 213                  $timetoshowusers = $CFG->block_online_users_timetosee * 60;
 214              } else {
 215                  $timetoshowusers = 300;
 216              }
 217  
 218              // Work out if the user is logged in or not.
 219              $userstate = 'loggedoff';
 220              if (!empty($localisedeventdata->userto->lastaccess)
 221                      && (time() - $timetoshowusers) < $localisedeventdata->userto->lastaccess) {
 222                  $userstate = 'loggedin';
 223              }
 224  
 225              // Fill in the array of processors to be used based on default and user preferences.
 226              // Do not process muted conversations.
 227              $processorlist = [];
 228              if (!$recipient->ismuted) {
 229                  foreach ($processors as $processor) {
 230                      // Skip adding processors for internal user, if processor doesn't support sending message to internal user.
 231                      if (!$usertoisrealuser && !$processor->object->can_send_to_any_users()) {
 232                          continue;
 233                      }
 234  
 235                      // First find out permissions.
 236                      $defaultpreference = $processor->name . '_provider_' . $preferencebase . '_permitted';
 237                      if (isset($defaultpreferences->{$defaultpreference})) {
 238                          $permitted = $defaultpreferences->{$defaultpreference};
 239                      } else {
 240                          // MDL-25114 They supplied an $eventdata->component $eventdata->name combination which doesn't
 241                          // exist in the message_provider table (thus there is no default settings for them).
 242                          $preferrormsg = "Could not load preference $defaultpreference. Make sure the component and name you supplied
 243                      to message_send() are valid.";
 244                          throw new \coding_exception($preferrormsg);
 245                      }
 246  
 247                      // Find out if user has configured this output.
 248                      // Some processors cannot function without settings from the user.
 249                      $userisconfigured = $processor->object->is_user_configured($recipient);
 250  
 251                      // DEBUG: notify if we are forcing unconfigured output.
 252                      if ($permitted == 'forced' && !$userisconfigured) {
 253                          debugging('Attempt to force message delivery to user who has "' . $processor->name .
 254                              '" output unconfigured', DEBUG_NORMAL);
 255                      }
 256  
 257                      // Populate the list of processors we will be using.
 258                      if (!$eventdata->notification && $processor->object->force_process_messages()) {
 259                          $processorlist[] = $processor->name;
 260                      } else if ($permitted == 'forced' && $userisconfigured) {
 261                          // An admin is forcing users to use this message processor. Use this processor unconditionally.
 262                          $processorlist[] = $processor->name;
 263                      } else if ($permitted == 'permitted' && $userisconfigured && !$recipient->emailstop) {
 264                          // User has not disabled notifications.
 265                          // See if user set any notification preferences, otherwise use site default ones.
 266                          $userpreferencename = 'message_provider_' . $preferencebase . '_' . $userstate;
 267                          if ($userpreference = get_user_preferences($userpreferencename, null, $recipient)) {
 268                              if (in_array($processor->name, explode(',', $userpreference))) {
 269                                  $processorlist[] = $processor->name;
 270                              }
 271                          } else if (isset($defaultpreferences->{$userpreferencename})) {
 272                              if (in_array($processor->name, explode(',', $defaultpreferences->{$userpreferencename}))) {
 273                                  $processorlist[] = $processor->name;
 274                              }
 275                          }
 276                      }
 277                  }
 278              }
 279              // Batch up the localised event data and processor list for all users into a local buffer.
 280              $eventprocmaps[] = [clone($localisedeventdata), $processorlist];
 281          }
 282          // Then pass it off as one item of work, to be processed by send_conversation_message_to_processors(), which will
 283          // handle all transaction buffering logic.
 284          self::send_conversation_message_to_processors($eventprocmaps, $eventdata, $savemessage);
 285  
 286          return $savemessage->id;
 287      }
 288  
 289      /**
 290       * Takes a list of localised event data, and tries to send them to their respective member's message processors.
 291       *
 292       * Input format:
 293       *  [CONVID => [$localisedeventdata, $savemessage, $processorlist], ].
 294       *
 295       * @param array $eventprocmaps the array of localised event data and processors for each member of the conversation.
 296       * @param message $eventdata the original conversation message eventdata
 297       * @param \stdClass $savemessage the saved message record.
 298       * @throws \coding_exception
 299       */
 300      protected static function send_conversation_message_to_processors(array $eventprocmaps, message $eventdata,
 301                                                                        \stdClass $savemessage) {
 302          global $DB;
 303  
 304          // We cannot communicate with external systems in DB transactions,
 305          // buffer the messages if necessary.
 306          if ($DB->is_transaction_started()) {
 307              // Buffer this group conversation message and it's record.
 308              self::$convmessagebuffer[] = [$eventprocmaps, $eventdata, $savemessage];
 309              return;
 310          }
 311  
 312          // Send each localised version of the event data to each member's respective processors.
 313          foreach ($eventprocmaps as $eventprocmap) {
 314              $eventdata = $eventprocmap[0];
 315              $processorlist = $eventprocmap[1];
 316              self::call_processors($eventdata, $processorlist);
 317          }
 318  
 319          // Trigger event for sending a message or notification - we need to do this before marking as read!
 320          self::trigger_message_events($eventdata, $savemessage);
 321      }
 322  
 323      /**
 324       * Do the message sending.
 325       *
 326       * NOTE: to be used from message_send() only.
 327       *
 328       * @param \core\message\message $eventdata fully prepared event data for processors
 329       * @param \stdClass $savemessage the message saved in 'message' table
 330       * @param array $processorlist list of processors for target user
 331       * @return int $messageid the id from 'messages' (false is not returned)
 332       */
 333      public static function send_message(message $eventdata, \stdClass $savemessage, array $processorlist) {
 334          global $CFG;
 335  
 336          require_once($CFG->dirroot.'/message/lib.php'); // This is most probably already included from messagelib.php file.
 337  
 338          if (empty($processorlist)) {
 339              // Trigger event for sending a message or notification - we need to do this before marking as read!
 340              self::trigger_message_events($eventdata, $savemessage);
 341  
 342              if ($eventdata->notification) {
 343                  // If they have deselected all processors and it's a notification mark it read. The user doesn't want to be
 344                  // bothered.
 345                  $savemessage->timeread = null;
 346                  \core_message\api::mark_notification_as_read($savemessage);
 347              } else if (empty($CFG->messaging)) {
 348                  // If it's a message and messaging is disabled mark it read.
 349                  \core_message\api::mark_message_as_read($eventdata->userto->id, $savemessage);
 350              }
 351  
 352              return $savemessage->id;
 353          }
 354  
 355          // Let the manager do the sending or buffering when db transaction in progress.
 356          return self::send_message_to_processors($eventdata, $savemessage, $processorlist);
 357      }
 358  
 359      /**
 360       * Send message to message processors.
 361       *
 362       * @param \stdClass|\core\message\message $eventdata
 363       * @param \stdClass $savemessage
 364       * @param array $processorlist
 365       * @return int $messageid
 366       */
 367      protected static function send_message_to_processors($eventdata, \stdClass $savemessage, array
 368      $processorlist) {
 369          global $CFG, $DB;
 370  
 371          // We cannot communicate with external systems in DB transactions,
 372          // buffer the messages if necessary.
 373          if ($DB->is_transaction_started()) {
 374              // We need to clone all objects so that devs may not modify it from outside later.
 375              $eventdata = clone($eventdata);
 376              $eventdata->userto = clone($eventdata->userto);
 377              $eventdata->userfrom = clone($eventdata->userfrom);
 378  
 379              // Conserve some memory the same was as $USER setup does.
 380              unset($eventdata->userto->description);
 381              unset($eventdata->userfrom->description);
 382  
 383              self::$buffer[] = array($eventdata, $savemessage, $processorlist);
 384              return $savemessage->id;
 385          }
 386  
 387          // Send the message to processors.
 388          self::call_processors($eventdata, $processorlist);
 389  
 390          // Trigger event for sending a message or notification - we need to do this before marking as read!
 391          self::trigger_message_events($eventdata, $savemessage);
 392  
 393          if (!$eventdata->notification && empty($CFG->messaging)) {
 394              // If it's a message and messaging is disabled mark it read.
 395              \core_message\api::mark_message_as_read($eventdata->userto->id, $savemessage);
 396          }
 397  
 398          return $savemessage->id;
 399      }
 400  
 401      /**
 402       * Notification from DML layer.
 403       *
 404       * Note: to be used from DML layer only.
 405       */
 406      public static function database_transaction_commited() {
 407          if (!self::$buffer && !self::$convmessagebuffer) {
 408              return;
 409          }
 410          self::process_buffer();
 411      }
 412  
 413      /**
 414       * Notification from DML layer.
 415       *
 416       * Note: to be used from DML layer only.
 417       */
 418      public static function database_transaction_rolledback() {
 419          self::$buffer = array();
 420          self::$convmessagebuffer = array();
 421      }
 422  
 423      /**
 424       * Sent out any buffered messages if necessary.
 425       */
 426      protected static function process_buffer() {
 427          // Reset the buffers first in case we get exception from processor.
 428          $messages = self::$buffer;
 429          self::$buffer = array();
 430          $convmessages = self::$convmessagebuffer;
 431          self::$convmessagebuffer = array();
 432  
 433          foreach ($messages as $message) {
 434              list($eventdata, $savemessage, $processorlist) = $message;
 435              self::send_message_to_processors($eventdata, $savemessage, $processorlist);
 436          }
 437  
 438          foreach ($convmessages as $convmessage) {
 439              list($eventprocmap, $eventdata, $savemessage) = $convmessage;
 440              self::send_conversation_message_to_processors($eventprocmap, $eventdata, $savemessage);
 441          }
 442      }
 443  
 444      /**
 445       * Trigger an appropriate message creation event, based on the supplied $eventdata and $savemessage.
 446       *
 447       * @param message $eventdata the eventdata for the message.
 448       * @param \stdClass $savemessage the message record.
 449       * @throws \coding_exception
 450       */
 451      protected static function trigger_message_events(message $eventdata, \stdClass $savemessage) {
 452          global $DB;
 453          if ($eventdata->notification) {
 454              \core\event\notification_sent::create_from_ids(
 455                  $eventdata->userfrom->id,
 456                  $eventdata->userto->id,
 457                  $savemessage->id,
 458                  $eventdata->courseid
 459              )->trigger();
 460          } else { // Must be a message.
 461              // If the message is a group conversation, then trigger the 'group_message_sent' event.
 462              if ($eventdata->convid) {
 463                  $conv = $DB->get_record('message_conversations', ['id' => $eventdata->convid], 'id, type');
 464                  if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP) {
 465                      \core\event\group_message_sent::create_from_ids(
 466                          $eventdata->userfrom->id,
 467                          $eventdata->convid,
 468                          $savemessage->id,
 469                          $eventdata->courseid
 470                      )->trigger();
 471                      return;
 472                  }
 473                  // Individual type conversations fall through to the default 'message_sent' event.
 474              }
 475              \core\event\message_sent::create_from_ids(
 476                  $eventdata->userfrom->id,
 477                  $eventdata->userto->id,
 478                  $savemessage->id,
 479                  $eventdata->courseid
 480              )->trigger();
 481          }
 482      }
 483  
 484      /**
 485       * For each processor, call it's send_message() method.
 486       *
 487       * @param message $eventdata the message object.
 488       * @param array $processorlist the list of processors for a single user.
 489       */
 490      protected static function call_processors(message $eventdata, array $processorlist) {
 491          // Allow plugins to change the message/notification data before sending it.
 492          $pluginsfunction = get_plugins_with_function('pre_processor_message_send');
 493  
 494          foreach ($processorlist as $procname) {
 495              // Let new messaging class add custom content based on the processor.
 496              $proceventdata = ($eventdata instanceof message) ? $eventdata->get_eventobject_for_processor($procname) : $eventdata;
 497  
 498              if ($pluginsfunction) {
 499                  foreach ($pluginsfunction as $plugintype => $plugins) {
 500                      foreach ($plugins as $pluginfunction) {
 501                          $pluginfunction($procname, $proceventdata);
 502                      }
 503                  }
 504              }
 505  
 506              $stdproc = new \stdClass();
 507              $stdproc->name = $procname;
 508              $processor = \core_message\api::get_processed_processor_object($stdproc);
 509              if (!$processor->object->send_message($proceventdata)) {
 510                  debugging('Error calling message processor ' . $procname);
 511              }
 512          }
 513      }
 514  }