Commit | Line | Data |
---|---|---|
349f98ad PS |
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 | ||
b30f4d6d JD |
48 | /** @var array buffer of pending messages to conversations */ |
49 | protected static $convmessagebuffer = array(); | |
50 | ||
9581bc3c JD |
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 | $sql = "SELECT u.* | |
86 | FROM {message_conversation_members} mcm | |
87 | JOIN {user} u | |
88 | ON (mcm.conversationid = :convid AND u.id = mcm.userid) | |
e1266eaf | 89 | ORDER BY u.id desc"; |
9581bc3c JD |
90 | $members = $DB->get_records_sql($sql, ['convid' => $eventdata->convid]); |
91 | if (empty($members)) { | |
92 | throw new \moodle_exception("Conversation has no members or does not exist."); | |
93 | } | |
94 | ||
95 | if (!is_object($localisedeventdata->userfrom)) { | |
96 | $localisedeventdata->userfrom = $members[$localisedeventdata->userfrom]; | |
97 | } | |
98 | ||
99 | // This should now hold only the other users (recipients). | |
100 | unset($members[$localisedeventdata->userfrom->id]); | |
101 | $otherusers = $members; | |
102 | ||
103 | // Get conversation type and name. We'll use this to determine which message subject to generate, depending on type. | |
104 | $conv = $DB->get_record('message_conversations', ['id' => $eventdata->convid], 'id, type, name'); | |
105 | ||
106 | // We treat individual conversations the same as any direct message with 'userfrom' and 'userto' specified. | |
107 | // We know the other user, so set the 'userto' field so that the event code will get access to this field. | |
108 | // If this was a legacy caller (eventdata->userto is set), then use that instead, as we want to use the fields specified | |
109 | // in that object instead of using one fetched from the DB. | |
110 | $legacymessage = false; | |
111 | if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) { | |
112 | if (isset($eventdata->userto)) { | |
113 | $legacymessage = true; | |
114 | } else { | |
115 | $otheruser = reset($otherusers); | |
116 | $eventdata->userto = $otheruser; | |
117 | } | |
118 | } | |
119 | ||
120 | // Fetch enabled processors. | |
121 | // If we are dealing with a message some processors may want to handle it regardless of user and site settings. | |
122 | $processors = array_filter(get_message_processors(false), function($processor) { | |
123 | if ($processor->object->force_process_messages()) { | |
124 | return true; | |
125 | } | |
126 | ||
127 | return ($processor->enabled && $processor->configured); | |
128 | }); | |
129 | ||
130 | // For each member of the conversation, other than the sender: | |
131 | // 1. Set recipient specific event data (language specific, user prefs, etc) | |
132 | // 2. Generate recipient specific processor list | |
133 | // 3. Call send_message() to pass the message to processors and generate the relevant per-user events. | |
134 | $eventprocmaps = []; // Init the event/processors buffer. | |
135 | foreach ($otherusers as $recipient) { | |
136 | // If this message was a legacy (1:1) message, then we use the userto. | |
137 | if ($legacymessage) { | |
138 | $recipient = $eventdata->userto; | |
139 | } | |
140 | ||
141 | $usertoisrealuser = (\core_user::is_real_user($recipient->id) != false); | |
142 | ||
143 | // Using string manager directly so that strings in the message will be in the message recipients language rather than | |
144 | // the sender's. | |
145 | if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) { | |
146 | $localisedeventdata->subject = get_string_manager()->get_string('unreadnewmessage', 'message', | |
147 | fullname($localisedeventdata->userfrom), $recipient->lang); | |
148 | } else if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP) { | |
149 | $stringdata = (object) ['name' => fullname($localisedeventdata->userfrom), 'conversationname' => $conv->name]; | |
150 | $localisedeventdata->subject = get_string_manager()->get_string('unreadnewgroupconversationmessage', 'message', | |
151 | $stringdata, $recipient->lang); | |
152 | } | |
153 | ||
154 | // Spoof the userto based on the current member id. | |
155 | $localisedeventdata->userto = $recipient; | |
156 | ||
157 | $s = new \stdClass(); | |
158 | $s->sitename = format_string($SITE->shortname, true, array('context' => \context_course::instance(SITEID))); | |
159 | // When the new interface lands, the URL may be reintroduced, but for now it isn't supported, so just hit the index. | |
160 | $s->url = $CFG->wwwroot.'/message/index.php'; | |
161 | $emailtagline = get_string_manager()->get_string('emailtagline', 'message', $s, $recipient->lang); | |
162 | ||
163 | $localisedeventdata->fullmessage = $eventdata->fullmessage; | |
164 | $localisedeventdata->fullmessagehtml = $eventdata->fullmessagehtml; | |
165 | if (!empty($localisedeventdata->fullmessage)) { | |
166 | $localisedeventdata->fullmessage .= "\n\n---------------------------------------------------------------------\n" | |
167 | . $emailtagline; | |
168 | } | |
169 | if (!empty($localisedeventdata->fullmessagehtml)) { | |
170 | $localisedeventdata->fullmessagehtml .= | |
171 | "<br><br>---------------------------------------------------------------------<br>" . $emailtagline; | |
172 | } | |
173 | ||
174 | // If recipient is internal user (noreply user), and emailstop is set then don't send any msg. | |
175 | if (!$usertoisrealuser && !empty($recipient->emailstop)) { | |
176 | debugging('Attempt to send msg to internal (noreply) user', DEBUG_NORMAL); | |
177 | return false; | |
178 | } | |
179 | ||
180 | // Set the online state. | |
181 | if (isset($CFG->block_online_users_timetosee)) { | |
182 | $timetoshowusers = $CFG->block_online_users_timetosee * 60; | |
183 | } else { | |
184 | $timetoshowusers = 300; | |
185 | } | |
186 | ||
187 | // Work out if the user is logged in or not. | |
188 | $userstate = 'loggedoff'; | |
189 | if (!empty($localisedeventdata->userto->lastaccess) | |
190 | && (time() - $timetoshowusers) < $localisedeventdata->userto->lastaccess) { | |
191 | $userstate = 'loggedin'; | |
192 | } | |
193 | ||
194 | // Fill in the array of processors to be used based on default and user preferences. | |
195 | $processorlist = []; | |
196 | foreach ($processors as $processor) { | |
197 | // Skip adding processors for internal user, if processor doesn't support sending message to internal user. | |
198 | if (!$usertoisrealuser && !$processor->object->can_send_to_any_users()) { | |
199 | continue; | |
200 | } | |
201 | ||
202 | // First find out permissions. | |
203 | $defaultpreference = $processor->name.'_provider_'.$preferencebase.'_permitted'; | |
204 | if (isset($defaultpreferences->{$defaultpreference})) { | |
205 | $permitted = $defaultpreferences->{$defaultpreference}; | |
206 | } else { | |
207 | // MDL-25114 They supplied an $eventdata->component $eventdata->name combination which doesn't | |
208 | // exist in the message_provider table (thus there is no default settings for them). | |
209 | $preferrormsg = "Could not load preference $defaultpreference. Make sure the component and name you supplied | |
210 | to message_send() are valid."; | |
211 | throw new coding_exception($preferrormsg); | |
212 | } | |
213 | ||
214 | // Find out if user has configured this output. | |
215 | // Some processors cannot function without settings from the user. | |
216 | $userisconfigured = $processor->object->is_user_configured($recipient); | |
217 | ||
218 | // DEBUG: notify if we are forcing unconfigured output. | |
219 | if ($permitted == 'forced' && !$userisconfigured) { | |
220 | debugging('Attempt to force message delivery to user who has "'.$processor->name.'" output unconfigured', | |
221 | DEBUG_NORMAL); | |
222 | } | |
223 | ||
224 | // Populate the list of processors we will be using. | |
225 | if (!$eventdata->notification && $processor->object->force_process_messages()) { | |
226 | $processorlist[] = $processor->name; | |
227 | } else if ($permitted == 'forced' && $userisconfigured) { | |
228 | // An admin is forcing users to use this message processor. Use this processor unconditionally. | |
229 | $processorlist[] = $processor->name; | |
230 | } else if ($permitted == 'permitted' && $userisconfigured && !$recipient->emailstop) { | |
231 | // User has not disabled notifications. | |
232 | // See if user set any notification preferences, otherwise use site default ones. | |
233 | $userpreferencename = 'message_provider_'.$preferencebase.'_'.$userstate; | |
234 | if ($userpreference = get_user_preferences($userpreferencename, null, $recipient)) { | |
235 | if (in_array($processor->name, explode(',', $userpreference))) { | |
236 | $processorlist[] = $processor->name; | |
237 | } | |
238 | } else if (isset($defaultpreferences->{$userpreferencename})) { | |
239 | if (in_array($processor->name, explode(',', $defaultpreferences->{$userpreferencename}))) { | |
240 | $processorlist[] = $processor->name; | |
241 | } | |
242 | } | |
243 | } | |
244 | } | |
b30f4d6d JD |
245 | // Batch up the localised event data and processor list for all users into a local buffer. |
246 | $eventprocmaps[] = [clone($localisedeventdata), $processorlist]; | |
9581bc3c | 247 | } |
b30f4d6d JD |
248 | // Then pass it off as one item of work, to be processed by send_conversation_message_to_processors(), which will |
249 | // handle all transaction buffering logic. | |
250 | self::send_conversation_message_to_processors($eventprocmaps, $eventdata, $savemessage); | |
9581bc3c JD |
251 | |
252 | return $savemessage->id; | |
253 | } | |
254 | ||
b30f4d6d JD |
255 | /** |
256 | * Takes a list of localised event data, and tries to send them to their respective member's message processors. | |
257 | * | |
258 | * Input format: | |
259 | * [CONVID => [$localisedeventdata, $savemessage, $processorlist], ]. | |
260 | * | |
261 | * @param array $eventprocmaps the array of localised event data and processors for each member of the conversation. | |
262 | * @param message $eventdata the original conversation message eventdata | |
263 | * @param \stdClass $savemessage the saved message record. | |
264 | * @throws \coding_exception | |
265 | */ | |
266 | protected static function send_conversation_message_to_processors(array $eventprocmaps, message $eventdata, | |
267 | \stdClass $savemessage) { | |
268 | global $DB; | |
269 | ||
270 | // We cannot communicate with external systems in DB transactions, | |
271 | // buffer the messages if necessary. | |
272 | if ($DB->is_transaction_started()) { | |
273 | // Buffer this group conversation message and it's record. | |
274 | self::$convmessagebuffer[] = [$eventprocmaps, $eventdata, $savemessage]; | |
275 | return; | |
276 | } | |
277 | ||
278 | // Send each localised version of the event data to each member's respective processors. | |
279 | foreach ($eventprocmaps as $eventprocmap) { | |
280 | $eventdata = $eventprocmap[0]; | |
281 | $processorlist = $eventprocmap[1]; | |
282 | self::call_processors($eventdata, $processorlist); | |
283 | } | |
284 | ||
285 | // Trigger event for sending a message or notification - we need to do this before marking as read! | |
286 | self::trigger_message_events($eventdata, $savemessage); | |
287 | } | |
288 | ||
349f98ad PS |
289 | /** |
290 | * Do the message sending. | |
291 | * | |
292 | * NOTE: to be used from message_send() only. | |
293 | * | |
cc350fd9 | 294 | * @param \core\message\message $eventdata fully prepared event data for processors |
349f98ad PS |
295 | * @param \stdClass $savemessage the message saved in 'message' table |
296 | * @param array $processorlist list of processors for target user | |
883ce421 | 297 | * @return int $messageid the id from 'messages' (false is not returned) |
349f98ad | 298 | */ |
cc4952e9 | 299 | public static function send_message(message $eventdata, \stdClass $savemessage, array $processorlist) { |
349f98ad | 300 | global $CFG; |
c6f45ef8 | 301 | |
349f98ad PS |
302 | require_once($CFG->dirroot.'/message/lib.php'); // This is most probably already included from messagelib.php file. |
303 | ||
304 | if (empty($processorlist)) { | |
376a79c2 | 305 | // Trigger event for sending a message or notification - we need to do this before marking as read! |
947d3289 | 306 | self::trigger_message_events($eventdata, $savemessage); |
349f98ad | 307 | |
883ce421 | 308 | if ($eventdata->notification or empty($CFG->messaging)) { |
349f98ad PS |
309 | // If they have deselected all processors and its a notification mark it read. The user doesn't want to be bothered. |
310 | // The same goes if the messaging is completely disabled. | |
883ce421 | 311 | if ($eventdata->notification) { |
548936a6 MN |
312 | $savemessage->timeread = null; |
313 | \core_message\api::mark_notification_as_read($savemessage); | |
883ce421 | 314 | } else { |
548936a6 | 315 | \core_message\api::mark_message_as_read($eventdata->userto->id, $savemessage); |
883ce421 | 316 | } |
349f98ad PS |
317 | } |
318 | ||
883ce421 | 319 | return $savemessage->id; |
349f98ad PS |
320 | } |
321 | ||
322 | // Let the manager do the sending or buffering when db transaction in progress. | |
323 | return self::send_message_to_processors($eventdata, $savemessage, $processorlist); | |
324 | } | |
325 | ||
326 | /** | |
327 | * Send message to message processors. | |
328 | * | |
c6f45ef8 | 329 | * @param \stdClass|\core\message\message $eventdata |
349f98ad PS |
330 | * @param \stdClass $savemessage |
331 | * @param array $processorlist | |
332 | * @return int $messageid | |
333 | */ | |
c6f45ef8 AA |
334 | protected static function send_message_to_processors($eventdata, \stdClass $savemessage, array |
335 | $processorlist) { | |
349f98ad PS |
336 | global $CFG, $DB; |
337 | ||
338 | // We cannot communicate with external systems in DB transactions, | |
339 | // buffer the messages if necessary. | |
349f98ad PS |
340 | if ($DB->is_transaction_started()) { |
341 | // We need to clone all objects so that devs may not modify it from outside later. | |
342 | $eventdata = clone($eventdata); | |
343 | $eventdata->userto = clone($eventdata->userto); | |
344 | $eventdata->userfrom = clone($eventdata->userfrom); | |
345 | ||
346 | // Conserve some memory the same was as $USER setup does. | |
347 | unset($eventdata->userto->description); | |
348 | unset($eventdata->userfrom->description); | |
349 | ||
350 | self::$buffer[] = array($eventdata, $savemessage, $processorlist); | |
351 | return $savemessage->id; | |
352 | } | |
353 | ||
947d3289 JD |
354 | // Send the message to processors. |
355 | self::call_processors($eventdata, $processorlist); | |
349f98ad | 356 | |
376a79c2 | 357 | // Trigger event for sending a message or notification - we need to do this before marking as read! |
947d3289 | 358 | self::trigger_message_events($eventdata, $savemessage); |
349f98ad | 359 | |
0d657cc9 MN |
360 | if (empty($CFG->messaging)) { |
361 | // If they have deselected all processors and its a notification mark it read. The user doesn't want to be bothered. | |
362 | // The same goes if the messaging is completely disabled. | |
883ce421 | 363 | if ($eventdata->notification) { |
548936a6 MN |
364 | $savemessage->timeread = null; |
365 | \core_message\api::mark_notification_as_read($savemessage); | |
883ce421 | 366 | } else { |
548936a6 | 367 | \core_message\api::mark_message_as_read($eventdata->userto->id, $savemessage); |
883ce421 | 368 | } |
349f98ad PS |
369 | } |
370 | ||
883ce421 | 371 | return $savemessage->id; |
349f98ad PS |
372 | } |
373 | ||
374 | /** | |
375 | * Notification from DML layer. | |
376 | * | |
377 | * Note: to be used from DML layer only. | |
378 | */ | |
379 | public static function database_transaction_commited() { | |
b30f4d6d | 380 | if (!self::$buffer && !self::$convmessagebuffer) { |
349f98ad PS |
381 | return; |
382 | } | |
383 | self::process_buffer(); | |
384 | } | |
385 | ||
386 | /** | |
387 | * Notification from DML layer. | |
388 | * | |
389 | * Note: to be used from DML layer only. | |
390 | */ | |
391 | public static function database_transaction_rolledback() { | |
392 | self::$buffer = array(); | |
9581bc3c | 393 | self::$convmessagebuffer = array(); |
349f98ad PS |
394 | } |
395 | ||
396 | /** | |
397 | * Sent out any buffered messages if necessary. | |
398 | */ | |
399 | protected static function process_buffer() { | |
b30f4d6d | 400 | // Reset the buffers first in case we get exception from processor. |
349f98ad PS |
401 | $messages = self::$buffer; |
402 | self::$buffer = array(); | |
b30f4d6d JD |
403 | $convmessages = self::$convmessagebuffer; |
404 | self::$convmessagebuffer = array(); | |
349f98ad PS |
405 | |
406 | foreach ($messages as $message) { | |
407 | list($eventdata, $savemessage, $processorlist) = $message; | |
408 | self::send_message_to_processors($eventdata, $savemessage, $processorlist); | |
409 | } | |
b30f4d6d JD |
410 | |
411 | foreach ($convmessages as $convmessage) { | |
412 | list($eventprocmap, $eventdata, $savemessage) = $convmessage; | |
413 | self::send_conversation_message_to_processors($eventprocmap, $eventdata, $savemessage); | |
414 | } | |
349f98ad | 415 | } |
947d3289 JD |
416 | |
417 | /** | |
418 | * Trigger an appropriate message creation event, based on the supplied $eventdata and $savemessage. | |
419 | * | |
420 | * @param message $eventdata the eventdata for the message. | |
421 | * @param \stdClass $savemessage the message record. | |
422 | * @throws \coding_exception | |
423 | */ | |
424 | protected static function trigger_message_events(message $eventdata, \stdClass $savemessage) { | |
9581bc3c | 425 | global $DB; |
947d3289 JD |
426 | if ($eventdata->notification) { |
427 | \core\event\notification_sent::create_from_ids( | |
428 | $eventdata->userfrom->id, | |
429 | $eventdata->userto->id, | |
430 | $savemessage->id, | |
431 | $eventdata->courseid | |
432 | )->trigger(); | |
433 | } else { // Must be a message. | |
9581bc3c JD |
434 | // If the message is a group conversation, then trigger the 'group_message_sent' event. |
435 | if ($eventdata->convid) { | |
436 | $conv = $DB->get_record('message_conversations', ['id' => $eventdata->convid], 'id, type'); | |
437 | if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP) { | |
438 | \core\event\group_message_sent::create_from_ids( | |
439 | $eventdata->userfrom->id, | |
440 | $eventdata->convid, | |
441 | $savemessage->id, | |
442 | $eventdata->courseid | |
443 | )->trigger(); | |
444 | return; | |
445 | } | |
446 | // Individual type conversations fall through to the default 'message_sent' event. | |
447 | } | |
947d3289 JD |
448 | \core\event\message_sent::create_from_ids( |
449 | $eventdata->userfrom->id, | |
450 | $eventdata->userto->id, | |
451 | $savemessage->id, | |
452 | $eventdata->courseid | |
453 | )->trigger(); | |
454 | } | |
455 | } | |
456 | ||
457 | /** | |
458 | * For each processor, call it's send_message() method. | |
459 | * | |
460 | * @param message $eventdata the message object. | |
461 | * @param array $processorlist the list of processors for a single user. | |
462 | */ | |
463 | protected static function call_processors(message $eventdata, array $processorlist) { | |
464 | foreach ($processorlist as $procname) { | |
465 | // Let new messaging class add custom content based on the processor. | |
466 | $proceventdata = ($eventdata instanceof message) ? $eventdata->get_eventobject_for_processor($procname) : $eventdata; | |
467 | $stdproc = new \stdClass(); | |
468 | $stdproc->name = $procname; | |
469 | $processor = \core_message\api::get_processed_processor_object($stdproc); | |
470 | if (!$processor->object->send_message($proceventdata)) { | |
471 | debugging('Error calling message processor ' . $procname); | |
472 | } | |
473 | } | |
474 | } | |
349f98ad | 475 | } |