MDL-36941 core: convert existing api to use new table structure

Also deprecated the following functions -

1. message_move_userfrom_unread2read - It is not necessary
   for us to mark a message as read on user deletion.
2. message_get_blocked_users - Horrible logic used to
   determine if a user is blocked via reference on some
   randomly chosen 'isblocked' variable.
3. message_get_contacts - The same as above. This can be
   done in a much nicer way.
4. message_mark_message_read - We want two functions to do
   this to avoid confusing messages and notifications.
5. message_can_delete_message - This assumed the variable
   $message contained the 'useridto' property, which
   was present in the old table structure. We do not want
   future usages where a query is done on the new table
   and is simply passed as this won't contain this property.
6. message_delete_message - Same as above.
This commit is contained in:
Mark Nelson 2018-01-04 15:01:37 +08:00
parent 4cd439887a
commit 883ce42127
29 changed files with 1714 additions and 1096 deletions

View file

@ -36,6 +36,16 @@ require_once($CFG->dirroot . '/lib/messagelib.php');
*/
class api {
/**
* The action for reading a message.
*/
const MESSAGE_ACTION_READ = 1;
/**
* The action for deleting a message.
*/
const MESSAGE_ACTION_DELETED = 2;
/**
* Handles searching for messages in the message area.
*
@ -52,47 +62,33 @@ class api {
$ufields = \user_picture::fields('u', array('lastaccess'), 'userfrom_id', 'userfrom_');
$ufields2 = \user_picture::fields('u2', array('lastaccess'), 'userto_id', 'userto_');
// Get all the messages for the user.
$sql = "SELECT m.id, m.useridfrom, m.useridto, m.subject, m.fullmessage, m.fullmessagehtml, m.fullmessageformat,
m.smallmessage, m.notification, m.timecreated, 0 as isread, $ufields, mc.blocked as userfrom_blocked,
$ufields2, mc2.blocked as userto_blocked
FROM {message} m
JOIN {user} u
ON m.useridfrom = u.id
LEFT JOIN {message_contacts} mc
ON (mc.contactid = u.id AND mc.userid = ?)
JOIN {user} u2
ON m.useridto = u2.id
LEFT JOIN {message_contacts} mc2
ON (mc2.contactid = u2.id AND mc2.userid = ?)
WHERE ((useridto = ? AND timeusertodeleted = 0)
OR (useridfrom = ? AND timeuserfromdeleted = 0))
AND notification = 0
AND u.deleted = 0
AND u2.deleted = 0
AND " . $DB->sql_like('smallmessage', '?', false) . "
UNION ALL
SELECT mr.id, mr.useridfrom, mr.useridto, mr.subject, mr.fullmessage, mr.fullmessagehtml, mr.fullmessageformat,
mr.smallmessage, mr.notification, mr.timecreated, 1 as isread, $ufields, mc.blocked as userfrom_blocked,
$ufields2, mc2.blocked as userto_blocked
FROM {message_read} mr
JOIN {user} u
ON mr.useridfrom = u.id
LEFT JOIN {message_contacts} mc
ON (mc.contactid = u.id AND mc.userid = ?)
JOIN {user} u2
ON mr.useridto = u2.id
LEFT JOIN {message_contacts} mc2
ON (mc2.contactid = u2.id AND mc2.userid = ?)
WHERE ((useridto = ? AND timeusertodeleted = 0)
OR (useridfrom = ? AND timeuserfromdeleted = 0))
AND notification = 0
$sql = "SELECT m.id, m.useridfrom, mcm.userid as useridto, m.subject, m.fullmessage, m.fullmessagehtml, m.fullmessageformat,
m.smallmessage, m.timecreated, 0 as isread, $ufields, mcont.blocked as userfrom_blocked, $ufields2,
mcont2.blocked as userto_blocked
FROM {messages} m
INNER JOIN {user} u
ON u.id = m.useridfrom
INNER JOIN {message_conversations} mc
ON mc.id = m.conversationid
INNER JOIN {message_conversation_members} mcm
ON mcm.conversationid = m.conversationid
INNER JOIN {user} u2
ON u2.id = mcm.userid
LEFT JOIN {message_contacts} mcont
ON (mcont.contactid = u.id AND mcont.userid = ?)
LEFT JOIN {message_contacts} mcont2
ON (mcont2.contactid = u2.id AND mcont2.userid = ?)
LEFT JOIN {message_user_actions} mua
ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
WHERE (m.useridfrom = ? OR mcm.userid = ?)
AND m.useridfrom != mcm.userid
AND u.deleted = 0
AND u2.deleted = 0
AND mua.id is NULL
AND " . $DB->sql_like('smallmessage', '?', false) . "
ORDER BY timecreated DESC";
$params = array($userid, $userid, $userid, $userid, '%' . $search . '%',
$userid, $userid, $userid, $userid, '%' . $search . '%');
$params = array($userid, $userid, $userid, self::MESSAGE_ACTION_DELETED, $userid, $userid, '%' . $search . '%');
// Convert the messages into searchable contacts with their last message being the message that was searched.
$conversations = array();
@ -266,173 +262,48 @@ class api {
public static function get_conversations($userid, $limitfrom = 0, $limitnum = 20) {
global $DB;
// The case statement is used to make sure the same key is generated
// whether a user sent or received a message (it's the same conversation).
// E.g. If there is a message from user 1 to user 2 and then from user 2 to user 1 the result set
// will group those into a single record, since 1 -> 2 and 2 -> 1 is the same conversation.
$case1 = $DB->sql_concat('useridfrom', "'-'", 'useridto');
$case2 = $DB->sql_concat('useridto', "'-'", 'useridfrom');
$convocase = "CASE WHEN useridfrom > useridto
THEN $case1
ELSE $case2 END";
$convosig = "$convocase AS convo_signature";
// Get the last message from each conversation that the user belongs to.
$sql = "SELECT m.id, m.conversationid, m.useridfrom, mcm2.userid as useridto, m.smallmessage, m.timecreated
FROM {messages} m
INNER JOIN (
SELECT MAX(m.id) AS messageid
FROM {messages} m
INNER JOIN (
SELECT m.conversationid, MAX(m.timecreated) as maxtime
FROM {messages} m
INNER JOIN {message_conversation_members} mcm
ON mcm.conversationid = m.conversationid
LEFT JOIN {message_user_actions} mua
ON (mua.messageid = m.id AND mua.userid = :userid AND mua.action = :action)
WHERE mua.id is NULL
AND mcm.userid = :userid2
GROUP BY m.conversationid
) maxmessage
ON maxmessage.maxtime = m.timecreated AND maxmessage.conversationid = m.conversationid
GROUP BY m.conversationid
) lastmessage
ON lastmessage.messageid = m.id
INNER JOIN {message_conversation_members} mcm
ON mcm.conversationid = m.conversationid
INNER JOIN {message_conversation_members} mcm2
ON mcm2.conversationid = m.conversationid
WHERE mcm.userid = m.useridfrom
AND mcm.id != mcm2.id
ORDER BY m.timecreated DESC";
$messageset = $DB->get_recordset_sql($sql, ['userid' => $userid, 'action' => self::MESSAGE_ACTION_DELETED,
'userid2' => $userid], $limitfrom, $limitnum);
// This is a snippet to join the message tables and filter out any messages the user has deleted
// and ignore notifications. The fields are specified by name so that the union works on MySQL.
$allmessages = "SELECT
id, useridfrom, useridto, subject, fullmessage, fullmessageformat,
fullmessagehtml, smallmessage, notification, contexturl,
contexturlname, timecreated, timeuserfromdeleted, timeusertodeleted,
component, eventtype, 0 as timeread
FROM {message}
WHERE
(useridto = ? AND timeusertodeleted = 0 AND notification = 0)
UNION ALL
SELECT
id, useridfrom, useridto, subject, fullmessage, fullmessageformat,
fullmessagehtml, smallmessage, notification, contexturl,
contexturlname, timecreated, timeuserfromdeleted, timeusertodeleted,
component, eventtype, 0 as timeread
FROM {message}
WHERE
(useridfrom = ? AND timeuserfromdeleted = 0 AND notification = 0)
UNION ALL
SELECT
id, useridfrom, useridto, subject, fullmessage, fullmessageformat,
fullmessagehtml, smallmessage, notification, contexturl,
contexturlname, timecreated, timeuserfromdeleted, timeusertodeleted,
component, eventtype, timeread
FROM {message_read}
WHERE
(useridto = ? AND timeusertodeleted = 0 AND notification = 0)
UNION ALL
SELECT
id, useridfrom, useridto, subject, fullmessage, fullmessageformat,
fullmessagehtml, smallmessage, notification, contexturl,
contexturlname, timecreated, timeuserfromdeleted, timeusertodeleted,
component, eventtype, timeread
FROM {message_read}
WHERE
(useridfrom = ? AND timeuserfromdeleted = 0 AND notification = 0)";
$allmessagesparams = [$userid, $userid, $userid, $userid];
// Create a transaction to protect against concurrency issues.
$transaction = $DB->start_delegated_transaction();
// First we need to get the list of conversations from the database ordered by the conversation
// with the most recent message first.
//
// This query will join the two message tables and then group the results by the combination
// of useridfrom and useridto (the 'convo_signature').
$conversationssql = "SELECT $convosig, max(timecreated) as timecreated
FROM ($allmessages) x
GROUP BY $convocase
ORDER BY timecreated DESC, max(id) DESC";
$conversationrecords = $DB->get_records_sql($conversationssql, $allmessagesparams, $limitfrom, $limitnum);
// This user has no conversations so we can return early here.
if (empty($conversationrecords)) {
$transaction->allow_commit();
return [];
}
// Next we need to get the max id of the messages sent at the latest time for each conversation.
// This needs to be a separate query to above because there is no guarantee that the message with
// the highest id will also have the highest timecreated value (in fact that is fairly likely due
// to the split between the message tables).
//
// E.g. if we just added max(id) to the conversation query above and ran it on data like:
// id, userfrom, userto, timecreated
// 1, 1, 2, 2
// 2, 2, 1, 1
//
// Then the result of the query would be:
// convo_signature, timecreated, id
// 2-1, 2, 2
//
// That would be incorrect since the message with id 2 actually has a lower timecreated. Hence why
// the two queries need to be split.
//
// The same result could also be achieved with an inner join in a single query however we're specifically
// avoiding multiple joins in the messaging queries because of the size of the messaging tables.
$whereclauses = [];
$createdtimes = [];
foreach ($conversationrecords as $convoid => $record) {
$whereclauses[] = "($convocase = '$convoid' AND timecreated = {$record->timecreated})";
$createdtimes[] = $record->timecreated;
}
$messageidwhere = implode(' OR ', $whereclauses);
list($timecreatedsql, $timecreatedparams) = $DB->get_in_or_equal($createdtimes);
$allmessagestimecreated = "SELECT id, useridfrom, useridto, timecreated
FROM {message}
WHERE
(useridto = ? AND timeusertodeleted = 0 AND notification = 0)
AND timecreated $timecreatedsql
UNION ALL
SELECT id, useridfrom, useridto, timecreated
FROM {message}
WHERE
(useridfrom = ? AND timeuserfromdeleted = 0 AND notification = 0)
AND timecreated $timecreatedsql
UNION ALL
SELECT id, useridfrom, useridto, timecreated
FROM {message_read}
WHERE
(useridto = ? AND timeusertodeleted = 0 AND notification = 0)
AND timecreated $timecreatedsql
UNION ALL
SELECT id, useridfrom, useridto, timecreated
FROM {message_read}
WHERE
(useridfrom = ? AND timeuserfromdeleted = 0 AND notification = 0)
AND timecreated $timecreatedsql";
$messageidsql = "SELECT $convosig, max(id) as id, timecreated
FROM ($allmessagestimecreated) x
WHERE $messageidwhere
GROUP BY $convocase, timecreated";
$messageidparams = array_merge([$userid], $timecreatedparams, [$userid], $timecreatedparams,
[$userid], $timecreatedparams, [$userid], $timecreatedparams);
$messageidrecords = $DB->get_records_sql($messageidsql, $messageidparams);
// Ok, let's recap. We've pulled a descending ordered list of conversations by latest time created
// for the given user. For each of those conversations we've grabbed the max id for messages
// created at that time.
//
// So at this point we have the list of ids for the most recent message in each of the user's most
// recent conversations. Now we need to pull all of the message and user data for each message id.
$whereclauses = [];
foreach ($messageidrecords as $record) {
$whereclauses[] = "(id = {$record->id} AND timecreated = {$record->timecreated})";
}
$messagewhere = implode(' OR ', $whereclauses);
$messagesunionsql = "SELECT
id, useridfrom, useridto, smallmessage, 0 as timeread
FROM {message}
WHERE
{$messagewhere}
UNION ALL
SELECT
id, useridfrom, useridto, smallmessage, timeread
FROM {message_read}
WHERE
{$messagewhere}";
$messagesql = "SELECT $convosig, m.smallmessage, m.id, m.useridto, m.useridfrom, m.timeread
FROM ($messagesunionsql) m";
// We need to handle the case where the $messageids contains two ids from the same conversation
// (which can happen because there can be id clashes between the read and unread tables). In
// this case we will prioritise the unread message.
$messageset = $DB->get_recordset_sql($messagesql, $allmessagesparams);
$messages = [];
foreach ($messageset as $message) {
$id = $message->convo_signature;
if (!isset($messages[$id]) || empty($message->timeread)) {
$messages[$id] = $message;
}
$messages[$message->id] = $message;
}
$messageset->close();
// If there are no messages return early.
if (empty($messages)) {
return [];
}
// We need to pull out the list of other users that are part of each of these conversations. This
// needs to be done in a separate query to avoid doing a join on the messages tables and the user
// tables because on large sites these tables are massive which results in extremely slow
@ -441,77 +312,64 @@ class api {
return ($message->useridfrom == $userid) ? $message->useridto : $message->useridfrom;
}, array_values($messages));
// Ok, let's get the other members in the conversations.
list($useridsql, $usersparams) = $DB->get_in_or_equal($otheruserids);
$userfields = \user_picture::fields('', array('lastaccess'));
$userfields = \user_picture::fields('u', array('lastaccess'));
$userssql = "SELECT $userfields
FROM {user}
WHERE id $useridsql
AND deleted = 0";
FROM {user} u
WHERE id $useridsql
AND deleted = 0";
$otherusers = $DB->get_records_sql($userssql, $usersparams);
// Similar to the above use case, we need to pull the contact information and again this has
// specifically been separated into another query to avoid having to do joins on the message
// tables.
$contactssql = "SELECT contactid, blocked
FROM {message_contacts}
WHERE userid = ? AND contactid $useridsql";
$contacts = $DB->get_records_sql($contactssql, array_merge([$userid], $otheruserids));
// Finally, let's get the unread messages count for this user so that we can add them
// to the conversation.
$unreadcountssql = 'SELECT useridfrom, count(*) as count
FROM {message}
WHERE useridto = ?
AND timeusertodeleted = 0
AND notification = 0
GROUP BY useridfrom';
$unreadcounts = $DB->get_records_sql($unreadcountssql, [$userid]);
// We can close off the transaction now.
$transaction->allow_commit();
// Now we need to order the messages back into the same order of the conversations.
$orderedconvosigs = array_keys($conversationrecords);
usort($messages, function($a, $b) use ($orderedconvosigs) {
$aindex = array_search($a->convo_signature, $orderedconvosigs);
$bindex = array_search($b->convo_signature, $orderedconvosigs);
return ($aindex < $bindex) ? -1 : 1;
});
// Preload the contexts before we construct the conversation to prevent the
// create_contact helper from needing to query the DB so often.
$ctxselect = \context_helper::get_preload_record_columns_sql('ctx');
$sql = "SELECT {$ctxselect}
FROM {context} ctx
WHERE ctx.contextlevel = ? AND
ctx.instanceid {$useridsql}";
$contexts = [];
$contexts = $DB->get_records_sql($sql, array_merge([CONTEXT_USER], $usersparams));
foreach ($contexts as $context) {
\context_helper::preload_from_record($context);
// If there are no other users (user may have been deleted), then do not continue.
if (empty($otherusers)) {
return [];
}
$contactssql = "SELECT contactid, blocked
FROM {message_contacts}
WHERE userid = ?
AND contactid $useridsql";
$contacts = $DB->get_records_sql($contactssql, array_merge([$userid], $usersparams));
// Finally, let's get the unread messages count for this user so that we can add them
// to the conversation. Remember we need to ignore the messages the user sent.
$unreadcountssql = 'SELECT m.useridfrom, count(m.id) as count
FROM {messages} m
INNER JOIN {message_conversations} mc
ON mc.id = m.conversationid
INNER JOIN {message_conversation_members} mcm
ON m.conversationid = mcm.conversationid
LEFT JOIN {message_user_actions} mua
ON (mua.messageid = m.id AND mua.userid = ? AND
(mua.action = ? OR mua.action = ?))
WHERE mcm.userid = ?
AND m.useridfrom != ?
AND mua.id is NULL
GROUP BY useridfrom';
$unreadcounts = $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, self::MESSAGE_ACTION_DELETED,
$userid, $userid]);
// Get rid of the table prefix.
$userfields = str_replace('u.', '', $userfields);
$userproperties = explode(',', $userfields);
$arrconversations = array();
// The last step now is to bring all of the data we've gathered together to create
// a conversation (or contact, as the API is named...).
foreach ($messages as $message) {
$conversation = new \stdClass();
$otheruserid = ($message->useridfrom == $userid) ? $message->useridto : $message->useridfrom;
$otheruser = isset($otherusers[$otheruserid]) ? $otherusers[$otheruserid] : null;
$contact = isset($contacts[$otheruserid]) ? $contacts[$otheruserid] : null;
// It's possible the other user was deleted, so, skip.
if (is_null($otheruser)) {
continue;
}
// Add the other user's information to the conversation, if we have one.
foreach ($userproperties as $prop) {
$conversation->$prop = ($otheruser) ? $otheruser->$prop : null;
}
// Do not process a conversation with a deleted user.
if (empty($conversation->id)) {
continue;
}
// Add the contact's information, if we have one.
$conversation->blocked = ($contact) ? $contact->blocked : null;
@ -562,6 +420,73 @@ class api {
return $arrcontacts;
}
/**
* Returns the an array of the users the given user is in a conversation
* with who are a contact and the number of unread messages.
*
* @param int $userid The user id
* @param int $limitfrom
* @param int $limitnum
* @return array
*/
public static function get_contacts_with_unread_message_count($userid, $limitfrom = 0, $limitnum = 0) {
global $DB;
$userfields = \user_picture::fields('u', array('lastaccess'));
$unreadcountssql = "SELECT $userfields, count(m.id) as messagecount
FROM {message_contacts} mc
INNER JOIN {user} u
ON u.id = mc.contactid
LEFT JOIN {messages} m
ON m.useridfrom = mc.contactid
LEFT JOIN {message_conversation_members} mcm
ON mcm.conversationid = m.conversationid AND mcm.userid = ? AND mcm.userid != m.useridfrom
LEFT JOIN {message_user_actions} mua
ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
WHERE mua.id is NULL
AND mc.userid = ?
AND mc.blocked = 0
AND u.deleted = 0
GROUP BY $userfields";
return $DB->get_records_sql($unreadcountssql, [$userid, $userid, self::MESSAGE_ACTION_READ,
$userid, $userid], $limitfrom, $limitnum);
}
/**
* Returns the an array of the users the given user is in a conversation
* with who are not a contact and the number of unread messages.
*
* @param int $userid The user id
* @param int $limitfrom
* @param int $limitnum
* @return array
*/
public static function get_non_contacts_with_unread_message_count($userid, $limitfrom = 0, $limitnum = 0) {
global $DB;
$userfields = \user_picture::fields('u', array('lastaccess'));
$unreadcountssql = "SELECT $userfields, count(m.id) as messagecount
FROM {user} u
INNER JOIN {messages} m
ON m.useridfrom = u.id
INNER JOIN {message_conversation_members} mcm
ON mcm.conversationid = m.conversationid
LEFT JOIN {message_user_actions} mua
ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
LEFT JOIN {message_contacts} mc
ON (mc.userid = ? AND mc.contactid = u.id)
WHERE mcm.userid = ?
AND mcm.userid != m.useridfrom
AND mua.id is NULL
AND mc.id is NULL
AND u.deleted = 0
GROUP BY $userfields";
return $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, $userid, $userid],
$limitfrom, $limitnum);
}
/**
* Returns the messages to display in the message area.
*
@ -710,54 +635,43 @@ class api {
* @return bool
*/
public static function delete_conversation($userid, $otheruserid) {
global $DB;
global $DB, $USER;
// We need to update the tables to mark all messages as deleted from and to the other user. This seems worse than it
// is, that's because our DB structure splits messages into two tables (great idea, huh?) which causes code like this.
// This won't be a particularly heavily used function (at least I hope not), so let's hope MDL-36941 gets worked on
// soon for the sake of any developers' sanity when dealing with the messaging system.
$now = time();
$sql = "UPDATE {message}
SET timeuserfromdeleted = :time
WHERE useridfrom = :userid
AND useridto = :otheruserid
AND notification = 0";
$DB->execute($sql, array('time' => $now, 'userid' => $userid, 'otheruserid' => $otheruserid));
$conversationid = self::get_conversation_between_users($userid, $otheruserid);
$sql = "UPDATE {message}
SET timeusertodeleted = :time
WHERE useridto = :userid
AND useridfrom = :otheruserid
AND notification = 0";
$DB->execute($sql, array('time' => $now, 'userid' => $userid, 'otheruserid' => $otheruserid));
// If there is no conversation, there is nothing to do.
if (!$conversationid) {
return true;
}
$sql = "UPDATE {message_read}
SET timeuserfromdeleted = :time
WHERE useridfrom = :userid
AND useridto = :otheruserid
AND notification = 0";
$DB->execute($sql, array('time' => $now, 'userid' => $userid, 'otheruserid' => $otheruserid));
// Get all messages belonging to this conversation that have not already been deleted by this user.
$sql = "SELECT m.*
FROM {messages} m
INNER JOIN {message_conversations} mc
ON m.conversationid = mc.id
LEFT JOIN {message_user_actions} mua
ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
WHERE mua.id is NULL
AND mc.id = ?
ORDER BY m.timecreated ASC";
$messages = $DB->get_records_sql($sql, [$userid, self::MESSAGE_ACTION_DELETED, $conversationid]);
$sql = "UPDATE {message_read}
SET timeusertodeleted = :time
WHERE useridto = :userid
AND useridfrom = :otheruserid
AND notification = 0";
$DB->execute($sql, array('time' => $now, 'userid' => $userid, 'otheruserid' => $otheruserid));
// Ok, mark these as deleted.
foreach ($messages as $message) {
$mua = new \stdClass();
$mua->userid = $userid;
$mua->messageid = $message->id;
$mua->action = self::MESSAGE_ACTION_DELETED;
$mua->timecreated = time();
$mua->id = $DB->insert_record('message_user_actions', $mua);
// Now we need to trigger events for these.
if ($messages = helper::get_messages($userid, $otheruserid, $now)) {
// Loop through and trigger a deleted event.
foreach ($messages as $message) {
$messagetable = 'message';
if (!empty($message->timeread)) {
$messagetable = 'message_read';
}
// Trigger event for deleting the message.
\core\event\message_deleted::create_from_ids($message->useridfrom, $message->useridto,
$userid, $messagetable, $message->id)->trigger();
if ($message->useridfrom == $userid) {
$useridto = $otheruserid;
} else {
$useridto = $userid;
}
\core\event\message_deleted::create_from_ids($message->useridfrom, $useridto,
$USER->id, $message->id, $mua->id)->trigger();
}
return true;
@ -777,11 +691,19 @@ class api {
$user = $USER;
}
return $DB->count_records_select(
'message',
'useridto = ? AND timeusertodeleted = 0 AND notification = 0',
[$user->id],
"COUNT(DISTINCT(useridfrom))");
$sql = "SELECT COUNT(DISTINCT(m.conversationid))
FROM {messages} m
INNER JOIN {message_conversations} mc
ON m.conversationid = mc.id
INNER JOIN {message_conversation_members} mcm
ON mc.id = mcm.conversationid
LEFT JOIN {message_user_actions} mua
ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
WHERE mcm.userid = ?
AND mcm.userid != m.useridfrom
AND mua.id is NULL";
return $DB->count_records_sql($sql, [$user->id, self::MESSAGE_ACTION_READ, $user->id]);
}
/**
@ -797,31 +719,51 @@ class api {
public static function mark_all_read_for_user($touserid, $fromuserid = 0, $type = '') {
global $DB;
$params = array();
if (!empty($touserid)) {
$params['useridto'] = $touserid;
}
$type = strtolower($type);
// Create the SQL we will be using.
$messagesql = "SELECT m.*
FROM {messages} m
INNER JOIN {message_conversations} mc
ON mc.id = m.conversationid
INNER JOIN {message_conversation_members} mcm
ON mcm.conversationid = mc.id
WHERE mcm.userid = ?";
$messageparams = [$touserid];
if (!empty($fromuserid)) {
$params['useridfrom'] = $fromuserid;
$messagesql .= " AND m.useridfrom = ?";
$messageparams[] = $fromuserid;
}
$notificationsql = "SELECT n.*
FROM {notifications} n
WHERE useridto = ?";
$notificationsparams = [$touserid];
if (!empty($fromuserid)) {
$notificationsql .= " AND useridfrom = ?";
$notificationsparams[] = $fromuserid;
}
$messages = [];
$notifications = [];
if (!empty($type)) {
if (strtolower($type) == MESSAGE_TYPE_NOTIFICATION) {
$params['notification'] = 1;
$notifications = $DB->get_recordset_sql($notificationsql, $notificationsparams);
} else if (strtolower($type) == MESSAGE_TYPE_MESSAGE) {
$params['notification'] = 0;
$messages = $DB->get_recordset_sql($messagesql, $messageparams);
}
} else { // We want both.
$messages = $DB->get_recordset_sql($messagesql, $messageparams);
$notifications = $DB->get_recordset_sql($notificationsql, $notificationsparams);
}
$messages = $DB->get_recordset('message', $params);
foreach ($messages as $message) {
message_mark_message_read($message, time());
self::mark_message_as_read($touserid, $message->id);
}
$messages->close();
foreach ($notifications as $notification) {
self::mark_notification_as_read($touserid, $notification->id);
}
}
/**
@ -1076,4 +1018,240 @@ class api {
}
return $processor;
}
/**
* Retrieve users blocked by $user1
*
* @param int $userid The user id of the user whos blocked users we are returning
* @return array the users blocked
*/
public static function get_blocked_users($userid) {
global $DB;
$userfields = \user_picture::fields('u', array('lastaccess'));
$blockeduserssql = "SELECT $userfields
FROM {message_contacts} mc
INNER JOIN {user} u
ON u.id = mc.contactid
WHERE u.deleted = 0
AND mc.userid = ?
AND mc.blocked = 1
GROUP BY $userfields
ORDER BY u.firstname ASC";
return $DB->get_records_sql($blockeduserssql, [$userid]);
}
/**
* Mark a single message as read.
*
* @param int $userid The user id who marked the message as read
* @param int $messageid The message id
* @param int|null $timeread The time the message was marked as read, if null will default to time()
*/
public static function mark_message_as_read($userid, $messageid, $timeread = null) {
global $DB;
if (is_null($timeread)) {
$timeread = time();
}
$message = $DB->get_record('messages', array('id' => $messageid), '*', MUST_EXIST);
// Check if the user has already read this message.
if (!$DB->record_exists('message_user_actions', ['userid' => $userid,
'messageid' => $messageid, 'action' => self::MESSAGE_ACTION_READ])) {
$mua = new \stdClass();
$mua->userid = $userid;
$mua->messageid = $messageid;
$mua->action = self::MESSAGE_ACTION_READ;
$mua->timecreated = $timeread;
$mua->id = $DB->insert_record('message_user_actions', $mua);
// Get the context for the user who received the message.
$context = \context_user::instance($userid, IGNORE_MISSING);
// If the user no longer exists the context value will be false, in this case use the system context.
if ($context === false) {
$context = \context_system::instance();
}
// Trigger event for reading a message.
$event = \core\event\message_viewed::create(array(
'objectid' => $mua->id,
'userid' => $userid, // Using the user who read the message as they are the ones performing the action.
'context' => $context,
'relateduserid' => $message->useridfrom,
'other' => array(
'messageid' => $messageid
)
));
$event->trigger();
}
}
/**
* Mark a single notification as read.
*
* @param int $userid The user id who marked the notification as read
* @param int $notificationid The notification id
* @param int|null $timeread The time the message was marked as read, if null will default to time()
*/
public static function mark_notification_as_read($userid, $notificationid, $timeread = null) {
global $DB;
if (is_null($timeread)) {
$timeread = time();
}
// Make sure the notification is for the user.
$notification = $DB->get_record('notifications', ['id' => $notificationid, 'useridto' => $userid], '*', MUST_EXIST);
if (is_null($notification->timeread)) {
$updatenotification = new \stdClass();
$updatenotification->id = $notification->id;
$updatenotification->timeread = $timeread;
$DB->update_record('notifications', $updatenotification);
}
}
/**
* Checks if a user can delete a message.
*
* @param int $userid the user id of who we want to delete the message for (this may be done by the admin
* but will still seem as if it was by the user)
* @param int $messageid The message id
* @return bool Returns true if a user can delete the message, false otherwise.
*/
public static function can_delete_message($userid, $messageid) {
global $DB, $USER;
$sql = "SELECT m.id, m.useridfrom, mcm.userid as useridto
FROM {messages} m
INNER JOIN {message_conversations} mc
ON m.conversationid = mc.id
INNER JOIN {message_conversation_members} mcm
ON mcm.conversationid = mc.id
WHERE mcm.userid != m.useridfrom
AND m.id = ?";
$message = $DB->get_record_sql($sql, [$messageid], MUST_EXIST);
if ($message->useridfrom == $userid) {
$userdeleting = 'useridfrom';
} else if ($message->useridto == $userid) {
$userdeleting = 'useridto';
} else {
return false;
}
$systemcontext = \context_system::instance();
// Let's check if the user is allowed to delete this message.
if (has_capability('moodle/site:deleteanymessage', $systemcontext) ||
((has_capability('moodle/site:deleteownmessage', $systemcontext) &&
$USER->id == $message->$userdeleting))) {
return true;
}
return false;
}
/**
* Deletes a message.
*
* This function does not verify any permissions.
*
* @param int $userid the user id of who we want to delete the message for (this may be done by the admin
* but will still seem as if it was by the user)
* @param int $messageid The message id
* @return bool
*/
public static function delete_message($userid, $messageid) {
global $DB;
$sql = "SELECT m.id, m.useridfrom, mcm.userid as useridto
FROM {messages} m
INNER JOIN {message_conversations} mc
ON m.conversationid = mc.id
INNER JOIN {message_conversation_members} mcm
ON mcm.conversationid = mc.id
WHERE mcm.userid != m.useridfrom
AND m.id = ?";
$message = $DB->get_record_sql($sql, [$messageid], MUST_EXIST);
// Check if the user has already deleted this message.
if (!$DB->record_exists('message_user_actions', ['userid' => $userid,
'messageid' => $messageid, 'action' => self::MESSAGE_ACTION_DELETED])) {
$mua = new \stdClass();
$mua->userid = $userid;
$mua->messageid = $messageid;
$mua->action = self::MESSAGE_ACTION_DELETED;
$mua->timecreated = time();
$mua->id = $DB->insert_record('message_user_actions', $mua);
// Trigger event for deleting a message.
\core\event\message_deleted::create_from_ids($message->useridfrom, $message->useridto,
$userid, $message->id, $mua->id)->trigger();
return true;
}
return false;
}
/**
* Returns the conversation between two users.
*
* @param int $userid1 The userid of the first user
* @param int $userid2 The userid of the second user
* @return int|bool The id of the conversation, false if not found
*/
public static function get_conversation_between_users($userid1, $userid2) {
global $DB;
$sql = "SELECT DISTINCT mc.id
FROM {message_conversations} mc
INNER JOIN {message_conversation_members} mcm
ON mcm.conversationid = mc.id
INNER JOIN {message_conversation_members} mcm2
ON mcm2.conversationid = mc.id
WHERE mcm.userid = :userid1
AND mcm2.userid = :userid2
AND mcm.id != mcm2.id";
if ($conversation = $DB->get_record_sql($sql, ['userid1' => $userid1, 'userid2' => $userid2])) {
return $conversation->id;
}
return false;
}
/**
* Creates a conversation between two users.
*
* @param int $userid1 The userid of the first user
* @param int $userid2 The userid of the second user
* @return int The id of the conversation
*/
public static function create_conversation_between_users($userid1, $userid2) {
global $DB;
$conversation = new \stdClass();
$conversation->timecreated = time();
$conversation->id = $DB->insert_record('message_conversations', $conversation);
// Add members to this conversation.
$member = new \stdClass();
$member->conversationid = $conversation->id;
$member->userid = $userid1;
$member->timecreated = time();
$DB->insert_record('message_conversation_members', $member);
$member = new \stdClass();
$member->conversationid = $conversation->id;
$member->userid = $userid2;
$member->timecreated = time();
$DB->insert_record('message_conversation_members', $member);
return $conversation->id;
}
}

View file

@ -51,46 +51,46 @@ class helper {
$sort = 'timecreated ASC', $timefrom = 0, $timeto = 0) {
global $DB;
$messageid = $DB->sql_concat("'message_'", 'id');
$messagereadid = $DB->sql_concat("'messageread_'", 'id');
$sql = "SELECT m.id, m.useridfrom, mdm.userid as useridto, m.subject, m.fullmessage, m.fullmessagehtml,
m.fullmessageformat, m.smallmessage, m.timecreated, 0 as timeread
FROM {messages} m
INNER JOIN {message_conversations} md
ON md.id = m.conversationid
INNER JOIN {message_conversation_members} mdm
ON mdm.conversationid = md.id";
$params = [];
$sql = "SELECT {$messageid} AS fakeid, id, useridfrom, useridto, subject, fullmessage, fullmessagehtml, fullmessageformat,
smallmessage, notification, timecreated, 0 as timeread
FROM {message} m
WHERE ((useridto = ? AND useridfrom = ? AND timeusertodeleted = ?)
OR (useridto = ? AND useridfrom = ? AND timeuserfromdeleted = ?))
AND notification = 0
%where%
UNION ALL
SELECT {$messagereadid} AS fakeid, id, useridfrom, useridto, subject, fullmessage, fullmessagehtml, fullmessageformat,
smallmessage, notification, timecreated, timeread
FROM {message_read} mr
WHERE ((useridto = ? AND useridfrom = ? AND timeusertodeleted = ?)
OR (useridto = ? AND useridfrom = ? AND timeuserfromdeleted = ?))
AND notification = 0
%where%
ORDER BY $sort";
$params1 = array($userid, $otheruserid, $timedeleted,
$otheruserid, $userid, $timedeleted);
if (empty($timedeleted)) {
$sql .= " LEFT JOIN {message_user_actions} mua
ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ? AND mua.timecreated is NOT NULL)";
$params[] = $userid;
$params[] = api::MESSAGE_ACTION_DELETED;
} else {
$sql .= " INNER JOIN {message_user_actions} mua
ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ? AND mua.timecreated = ?)";
$params[] = $userid;
$params[] = api::MESSAGE_ACTION_DELETED;
$params[] = $timedeleted;
}
$params2 = array($userid, $otheruserid, $timedeleted,
$otheruserid, $userid, $timedeleted);
$where = array();
$sql .= " WHERE (m.useridfrom = ? AND mdm.userid = ? OR m.useridfrom = ? AND mdm.userid = ?)";
$params = array_merge($params, [$userid, $otheruserid, $otheruserid, $userid]);
if (!empty($timefrom)) {
$where[] = 'AND timecreated >= ?';
$params1[] = $timefrom;
$params2[] = $timefrom;
$sql .= " AND m.timecreated >= ?";
$params[] = $timefrom;
}
if (!empty($timeto)) {
$where[] = 'AND timecreated <= ?';
$params1[] = $timeto;
$params2[] = $timeto;
$sql .= " AND m.timecreated <= ?";
$params[] = $timeto;
}
$sql = str_replace('%where%', implode(' ', $where), $sql);
$params = array_merge($params1, $params2);
if (empty($timedeleted)) {
$sql .= " AND mua.id is NULL";
}
$sql .= " ORDER BY m.$sort";
return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
}

View file

@ -145,9 +145,15 @@ abstract class base_message extends \core_search\base {
$userfield) {
global $DB;
if ($userfield == 'useridto') {
$userfield = 'mcm.userid';
} else {
$userfield = 'm.useridfrom';
}
// Set up basic query.
$where = $userfield . ' != :noreplyuser AND ' . $userfield .
' != :supportuser AND timecreated >= :modifiedfrom';
' != :supportuser AND m.timecreated >= :modifiedfrom';
$params = [
'noreplyuser' => \core_user::NOREPLY_USER,
'supportuser' => \core_user::SUPPORT_USER,
@ -179,6 +185,15 @@ abstract class base_message extends \core_search\base {
throw new \coding_exception('Unexpected contextlevel: ' . $context->contextlevel);
}
return $DB->get_recordset_select('message_read', $where, $params, 'timeread ASC');
$sql = "SELECT m.*, mcm.userid as useridto
FROM {messages} m
INNER JOIN {message_conversations} mc
ON m.conversationid = mc.id
INNER JOIN {message_conversation_members} mcm
ON mcm.conversationid = mc.id
WHERE mcm.userid != m.useridfrom
AND $where
ORDER BY m.timecreated ASC";
return $DB->get_recordset_sql($sql, $params);
}
}

View file

@ -70,7 +70,15 @@ class message_received extends base_message {
return \core_search\manager::ACCESS_DENIED;
}
$message = $DB->get_record('message_read', array('id' => $id));
$sql = "SELECT m.*, mcm.userid as useridto
FROM {messages} m
INNER JOIN {message_conversations} mc
ON m.conversationid = mc.id
INNER JOIN {message_conversation_members} mcm
ON mcm.conversationid = mc.id
WHERE mcm.userid != m.useridfrom
AND m.id = :id";
$message = $DB->get_record_sql($sql, array('id' => $id));
if (!$message) {
return \core_search\manager::ACCESS_DELETED;
}
@ -86,7 +94,9 @@ class message_received extends base_message {
return \core_search\manager::ACCESS_DENIED;
}
if ($message->timeusertodeleted != 0) {
$usertodeleted = $DB->record_exists('message_user_actions', ['messageid' => $id, 'userid' => $message->useridto,
'action' => \core_message\api::MESSAGE_ACTION_DELETED]);
if ($usertodeleted) {
return \core_search\manager::ACCESS_DELETED;
}

View file

@ -69,7 +69,15 @@ class message_sent extends base_message {
return \core_search\manager::ACCESS_DENIED;
}
$message = $DB->get_record('message_read', array('id' => $id));
$sql = "SELECT m.*, mcm.userid as useridto
FROM {messages} m
INNER JOIN {message_conversations} mc
ON m.conversationid = mc.id
INNER JOIN {message_conversation_members} mcm
ON mcm.conversationid = mc.id
WHERE mcm.userid != m.useridfrom
AND m.id = :id";
$message = $DB->get_record_sql($sql, array('id' => $id));
if (!$message) {
return \core_search\manager::ACCESS_DELETED;
}
@ -85,7 +93,9 @@ class message_sent extends base_message {
return \core_search\manager::ACCESS_DENIED;
}
if ($message->timeuserfromdeleted != 0) {
$userfromdeleted = $DB->record_exists('message_user_actions', ['messageid' => $id, 'userid' => $message->useridfrom,
'action' => \core_message\api::MESSAGE_ACTION_DELETED]);
if ($userfromdeleted) {
return \core_search\manager::ACCESS_DELETED;
}