Index: privatemsg.api.php =================================================================== RCS file: /cvs/drupal/contributions/modules/privatemsg/privatemsg.api.php,v retrieving revision 1.2 diff -u -p -r1.2 privatemsg.api.php --- privatemsg.api.php 30 Nov 2009 17:37:15 -0000 1.2 +++ privatemsg.api.php 19 Jun 2010 12:51:19 -0000 @@ -8,8 +8,7 @@ /** * @mainpage Privatemsg API Documentation - * This is the (currently inofficial) API documentation of the privatemsg 6.x - * API. + * This is the API documentation of Privatemsg. * * - Topics: * - @link api API functions @endlink @@ -17,6 +16,7 @@ * - @link message_hooks Message hooks @endlink * - @link generic_hooks Generic hooks @endlink * - @link theming Theming @endlink + * - @link types Types of recipients @endlink */ /** @@ -182,7 +182,7 @@ function hook_privatemsg_sql_messages_al } /** - * Load the participants of a thread. + * Alter the query that loads the participants of a thread. * * @param $fragments * Query fragments @@ -241,6 +241,8 @@ function hook_privatemsg_sql_unread_coun * going to be deleted, parameter: $message * - @link hook_privatemsg_message_view_alter view_alter @endlink: the message * is going to be displayed, parameter: $vars + * - @link hook_privatemsg_message_recipient_change recipient changed @endlink: + * a recipient is added or removed from/to a message. * * In hooks with _alter suffix, $message is by reference. * @@ -360,6 +362,29 @@ function hook_privatemsg_message_insert( } /** + * This hook is invoked when a recipient is added to a message. + * + * Since the hook might be invoked hundreds of times during batch or cron, only + * ids are passed and not complete user/message objects. + * + * @param $mid + * Id of the message. + * @param $thread_id + * Id of the thread the message belongs to. + * @param $recipient + * Recipient id, a user id if type is user or hidden. + * @param $type + * Type of the recipient. + * @param $added + * TRUE if the recipient is added, FALSE if he is removed. + */ +function hook_privatemsg_message_recipient_changed($mid, $thread_id, $recipient, $type, $added) { + if ($added && ($type == 'user' || $type == 'hidden')) { + privatemsg_filter_add_tags(array($thread_id), variable_get('privatemsg_filter_inbox_tag', ''), (object)array('uid' => $recipient)); + } +} + +/** * @} */ @@ -371,18 +396,32 @@ function hook_privatemsg_message_insert( */ /** - * Check if $author can send $recipient a message. + * Check if the author can send a message to the recipients. * - * Return a message if it is not alled, nothing if it is. This hook is executed - * for each recipient. + * This can be used to limit who can write whom based on other modules and/or + * settings. * * @param $author * Author of the message to be sent * @param $recipient * Recipient of the message - */ -function hook_privatemsg_block_message($author, $recipient) { - + * @return + * An indexed array of arrays with the keys recipient ({type}_{key}) and + * message (The reason why the recipient has been blocked). + */ +function hook_privatemsg_block_message($author, $recipients) { + $blocked = array(); + foreach($recipients as $recipient_id => $recipient) { + // Deny/block if the recipient type is role and the account does not have + // the necessary permission. + if ($recipient->type == 'role' && $recipient->recipient == 2) { + $blocked[] = array( + 'recipient' => $recipient_id, + 'message' => t('Not allowed to write private messages to the role authenticated user'), + ); + } + } + return $blocked; } /** * Add content to the view thread page. @@ -429,6 +468,106 @@ function hook_privatemsg_thread_operatio } /** + * @} + */ + +/** + * @defgroup types Types of recipients + * + * It is possible to define other types of recipients than the usual single + * user. These types are defined through a hook and a few callbacks and are + * stored in the {pm_index} table for each recipient entry. + * + * Because of that, only the combination of type and recipient (was uid in older + * versions) defines a unique recipient. + * + * This feature is usually used to define groups of recipients. Privatemsg + * comes with the privatemsg_roles sub-module, which allows to send messages to + * all members of a specific group. + * + * When sending a new message with a recipient type other than user, Privatemsg + * only inserts a single entry for that recipient type. However, when looking + * for messages for a user, Privatemsg only looks for recipient types user and + * hidden. To fill the gap, Privatemsg defines three ways how a non-user + * type is converted to hidden recipient entries. + * + * - For small recipient types (by default <100 recipients, configurable), the + * entries are added directly after saving the original private message. + * - When sending messages through the UI, bigger recipient types are handled + * with batch API. + * - For messages sent by the API, the hidden recipients are generated during + * cron runs. + * + * Once all hidden recipients are added, the original recipient type is marked + * as read so Privatemsg knows that he has been processed. + * + * Privatemsg defines the following types: + * + * - user: This is the default recipient type which is used for a single user. + * - hidden: Used to add internal recipient entries for other recipient types. + * - role: The sub-module privatemsg_roles defines an additional type called + * role. This allows to send messages to all members of a role. + * + * To implement a new type, the following hooks need to be implemented. Note + * that most of these hooks can also be used alone for other functionality than + * defining recipient types. + * + * - hook_privatemsg_recipient_type_info() - Tell Privatemsg about your + * recipient type(s). + * - hook_privatemsg_name_lookup() - Convert a string to an + * recipient object + * + * Additionaly, there is also a hook_privatemsg_recipient_type_info_alter() that + * allows to alter recipient type definitions. + */ + +/** + * @addtogroup types + * @{ + */ + + +/** + * This hook is used to tell privatemsg about the recipient types defined by a + * module. Each type consists of an array keyed by the internal recipient type + * name and the following keys must be defined. + * + * * name: Translated name of the recipient type. + * * description: A short description of how to send messages to to that + * recipient type. This is displayed below the To: field when sending a + * message. + * * load: A callback function that can load recipients based on their id, + * example: privatemsg_roles_load_multiple(). + * * format: Theme function to format the recipient before displaying. Must be + * defined with hook_theme(), example: theme_privatemsg_roles_format(). + * * autocomplete: Function callback to return possible autocomplete matches, + * example: privatemsg_roles_autocomplete(). + * * generate recipients: Function callback to return user ids which belong to a + * recipient type, example: privatemsg_roles_load_recipients(). + * * max: Function callback to return the highest user id of a recipient type, + * example: privatemsg_roles_count_recipients(). + * * write access: Optionally define a permission which controls write access + * to that recipient type. + * * view access: Optionally define a permission which controls if the user is + * able to see the recipient when he is looking at a thread. + */ +function hook_privatemsg_recipient_type_info() { + return array( + 'role' => array( + 'name' => t('Role'), + 'description' => t('Enter the name of a role to write a message to all users which have that role. Example: authenticated user.'), + 'load' => 'privatemsg_roles_load_multiple', + 'format' => 'privatemsg_roles_format', + 'autocomplete' => 'privatemsg_roles_autocomplete', + 'generate recipients' => 'privatemsg_roles_load_recipients', + 'max' => 'privatemsg_roles_count_recipients', + 'write access' => 'write privatemsg to roles', + 'view access' => 'view roles recipients', + ), + ); +} + +/** * Hook which allows to look up a user object. * * You can try to look up a user object based on the information passed to the @@ -438,14 +577,25 @@ function hook_privatemsg_thread_operatio * up the string. */ function hook_privatemsg_name_lookup($string) { - if ((int)$string > 0) { - // This is a possible uid, try to load a matching user. - if ($recipient = user_load(array('uid' => $string))) { - return $recipient; - } + $result = db_query("SELECT *, rid AS recipient FROM {role} WHERE name = '%s'", trim($role)); + if ($role = db_fetch_object($result)) { + $role->type = 'role'; + return $role; } } /** + * Allows to alter the defined recipient types. + * + * @param $types + * Array with the recipient types. + * + * @see hook_privatemsg_recipient_type_info() + */ +function hook_privatemsg_recipient_type_info_alter(&$types) { + +} + +/** * @} */ \ No newline at end of file Index: privatemsg.install =================================================================== RCS file: /cvs/drupal/contributions/modules/privatemsg/privatemsg.install,v retrieving revision 1.15 diff -u -p -r1.15 privatemsg.install --- privatemsg.install 4 Jan 2010 15:56:56 -0000 1.15 +++ privatemsg.install 19 Jun 2010 12:51:19 -0000 @@ -24,8 +24,8 @@ function privatemsg_schema() { 'not null' => TRUE, 'unsigned' => TRUE, ), - 'uid' => array( - 'description' => 'UID of either the author or the recipient', + 'recipient' => array( + 'description' => 'ID of the recipient object, typically user', 'type' => 'int', 'not null' => TRUE, 'unsigned' => TRUE, @@ -44,13 +44,20 @@ function privatemsg_schema() { 'not null' => TRUE, 'default' => 0 ), + 'type' => array( + 'description' => 'Type of recipient object', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => 'user' + ), ), 'indexes' => array( 'mid' => array('mid'), 'thread_id' => array('thread_id'), - 'uid' => array('uid'), - 'is_new' => array('mid', 'uid', 'is_new', ), + 'recipient' => array('recipient', 'type'), + 'is_new' => array('mid', 'recipient', 'type', 'is_new'), ), ); @@ -528,3 +535,29 @@ function privatemsg_update_6007() { return $ret; } + +/** + * Change schema to allow other recipients than single users. + */ +function privatemsg_update_6008() { + $ret = array(); + db_drop_index($ret, 'pm_index', 'uid'); + db_drop_index($ret, 'pm_index', 'is_new'); + db_change_field($ret, 'pm_index', 'uid', 'recipient', array( + 'description' => 'ID of the recipient object, typically user', + 'type' => 'int', + 'not null' => TRUE, + 'unsigned' => TRUE, + )); + db_add_field($ret, 'pm_index', 'type', array( + 'description' => 'Type of recipient object', + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '255', + 'default' => 'user' + ), array('indexes' => array( + 'recipient' => array('recipient', 'type'), + 'is_new' => array('mid', 'recipient', 'type', 'is_new') + ))); + return $ret; +} Index: privatemsg.module =================================================================== RCS file: /cvs/drupal/contributions/modules/privatemsg/privatemsg.module,v retrieving revision 1.132 diff -u -p -r1.132 privatemsg.module --- privatemsg.module 6 Jun 2010 15:52:50 -0000 1.132 +++ privatemsg.module 19 Jun 2010 12:51:20 -0000 @@ -44,8 +44,6 @@ function privatemsg_perm() { * Array with user objects. */ function _privatemsg_generate_user_array($userstring, $slice = NULL) { - static $user_cache = array(); - // Convert user uid list (uid1,uid2,uid3) into an array. If $slice is not NULL // pass that as argument to array_slice(). For example, -4 will only load the // last four users. @@ -57,11 +55,8 @@ function _privatemsg_generate_user_array } $participants = array(); foreach ($users as $uid) { - if (!array_key_exists($uid, $user_cache)) { - $user_cache[$uid] = user_load($uid); - } - if (is_object($user_cache[$uid])) { - $participants[$uid] = $user_cache[$uid]; + if ($account = privatemsg_user_load($uid)) { + $participants[privatemsg_recipient_key($account)] = $account; } } return $participants; @@ -86,11 +81,15 @@ function _privatemsg_format_participants $to = array(); $limited = FALSE; foreach ($part_array as $account) { + // Don't display recipients with type hidden. + if (isset($account->type) && $account->type == 'hidden') { + continue; + } if (is_int($limit) && count($to) >= $limit) { $limited = TRUE; break; } - $to[] = theme('username', $account); + $to[] = privatemsg_recipient_format($account); } $limit_string = ''; @@ -288,7 +287,7 @@ function privatemsg_view_access($thread) * TRUE if user has disabled private messaging, FALSE otherwise */ function privatemsg_is_disabled(&$account) { - if (!$account || !$account->uid) { + if (!$account || !isset($account->uid) || !$account->uid) { return FALSE; } @@ -351,11 +350,34 @@ function privatemsg_thread_load($thread_ $query = _privatemsg_assemble_query('participants', $thread_id); $participants = db_query($query['query']); $thread['participants'] = array(); + $to_load = array(); while ($participant = db_fetch_object($participants)) { - $thread['participants'][$participant->uid] = $participant; + // If the recipient is not of type user, store the recipient id for + // later loading, if the user has view access. + if ($participant->type != 'user' && $participant->type != 'hidden') { + if (privatemsg_recipient_access($participant->type, 'view access')) { + $to_load[$participant->type][] = $participant->recipient; + } + } + else { + $thread['participants'][privatemsg_recipient_key($participant)] = $participant; + } } + + // Now, load all non-user recipients. + foreach ($to_load as $type => $ids) { + $type_info = privatemsg_recipient_get_type($type); + if (isset($type_info['load']) && is_callable($type_info['load'])) { + $loaded = $type_info['load']($ids); + if (is_array($loaded)) { + $thread['participants'] += $loaded; + } + } + } + $thread['read_all'] = FALSE; - if (!array_key_exists($account->uid, $thread['participants']) && privatemsg_user_access('read all private messages', $account)) { + $exists = array_key_exists('user_' . $account->uid, $thread['participants']) || array_key_exists('hidden_' . $account->uid, $thread['participants']); + if (!$exists && privatemsg_user_access('read all private messages', $account)) { $thread['read_all'] = TRUE; } @@ -508,6 +530,46 @@ function privatemsg_cron() { $flushed++; } } + + $result = db_query_range("SELECT pmi.*, pm.author FROM {pm_index} pmi INNER JOIN {pm_message} pm ON pm.mid = pmi.mid WHERE pmi.type NOT IN ('user', 'hidden') AND pmi.is_new = 1 ORDER BY mid ASC", 0, 10); + // Number of user ids to process for this cron run. + $total_remaining = variable_get('privatemgs_cron_recipient_per_run', 1000); + $current_process = variable_get('privatemsg_cron_recipient_process', array()); + while ($recipient = db_fetch_object($result)) { + $type = privatemsg_recipient_get_type($recipient->type); + if (!$type) { + continue; + } + // Check if we already started to process this recipient. + $offset = 0; + if (!empty($current_process) && $current_process['mid'] == $recipient->mid && $current_process['recipient'] == $recipient->recipient && $current_process['type'] == $recipient->type) { + $offset = $current_process['offset']; + } + + $load_function = $type['generate recipients']; + $uids = $load_function($recipient, privatemsg_user_load($recipient->author), $total_remaining, $offset); + if (!empty($uids)) { + foreach ($uids as $uid) { + privatemsg_message_change_recipient($recipient->mid, $uid, 'hidden'); + } + } + // If less than the total remaining uids were returned, we are finished. + if (count($uids) < $total_remaining) { + $total_remaining -= count($uids); + db_query("UPDATE {pm_index} SET is_new = %d WHERE mid = %d AND recipient = %d AND type = '%s'", PRIVATEMSG_READ, $recipient->mid, $recipient->recipient, $recipient->type); + // Reset current process if necessary. + if ($offset > 0) { + variable_set('privatemsg_cron_recipient_process', array()); + } + } + else { + // We are not yet finished, save current process and break. + $current_process = (array)$recipient; + $current_process['offset'] = max($uids); + variable_set('privatemsg_cron_recipient_process', $current_process); + break; + } + } } function privatemsg_theme() { @@ -562,7 +624,7 @@ function privatemsg_preprocess_privatems $vars['mid'] = isset($message['mid']) ? $message['mid'] : NULL; $vars['thread_id'] = isset($message['thread_id']) ? $message['thread_id'] : NULL; $vars['author_picture'] = theme('user_picture', $message['author']); - $vars['author_name_link'] = theme('username', $message['author']); + $vars['author_name_link'] = privatemsg_recipient_format($message['author']); /** * @todo perhaps make this timestamp configurable via admin UI? */ @@ -610,7 +672,7 @@ function privatemsg_message_change_statu global $user; $account = $user; } - $query = "UPDATE {pm_index} SET is_new = %d WHERE mid = %d AND uid = %d"; + $query = "UPDATE {pm_index} SET is_new = %d WHERE mid = %d AND recipient = %d AND type IN ('user', 'hidden')"; db_query($query, $status, $pmid, $account->uid); } @@ -628,7 +690,7 @@ function privatemsg_unread_count($accoun global $user; $account = $user; } - if ( !isset($counts[$account->uid])) { + if (!isset($counts[$account->uid])) { $query = _privatemsg_assemble_query('unread_count', $account); $counts[$account->uid] = db_result(db_query($query['query'])); } @@ -645,9 +707,17 @@ function _privatemsg_load_thread_partici $query = _privatemsg_assemble_query('participants', $thread_id); $result = db_query($query['query']); $participants = array(); - while ($uid = db_fetch_object($result)) { - if (($recipient = user_load($uid->uid))) { - $participants[$recipient->uid] = $recipient; + while ($participant = db_fetch_object($result)) { + // Ignore hidden recipients. + if ($participant->type == 'hidden') { + continue; + } + if ($participant->type == 'user' && ($account = privatemsg_user_load($participant->recipient))) { + $participants[privatemsg_recipient_key($account)] = $account; + + } + elseif ($participant->type != 'user' && privatemsg_recipient_access($participant->type, 'write access')) { + $participants[privatemsg_recipient_key($participant)] = $participant; } } return $participants; @@ -676,22 +746,39 @@ function _privatemsg_parse_userstring($i foreach ($input as $string) { $string = trim($string); if (!empty($string)) { // We don't care about white space names. - // First, check if another module is able to resolve the string into an // user object. + + // Load recipient types. + $types = privatemsg_recipient_get_types(); + foreach (module_implements('privatemsg_name_lookup') as $module) { $function = $module . '_privatemsg_name_lookup'; if (($recipient = $function($string)) && is_object($recipient)) { - // If there is a match, continue with the next input string. - $recipients[$recipient->uid] = $recipient; - continue 2; + // Default to recipient type user. + if (!isset($recipient->type)) { + $recipient->type = 'user'; + $recipient->recipient = $recipient->uid; + } + // Only add recipient if user has the permission. + if (privatemsg_recipient_access($recipient->type, 'write access')) { + $recipients[privatemsg_recipient_key($recipient)] = $recipient; + continue 2; + } + else { + continue; + } } } // Fall back to the default username lookup. if (!$error = module_invoke('user', 'validate_name', $string)) { // String is a valid username, look it up. + // Remove optonal user specifier. + $string = str_replace('[user]', '', $string); if ($recipient = user_load(array('name' => $string))) { - $recipients[$recipient->uid] = $recipient; + $recipient->recipient = $recipient->uid; + $recipient->type = 'user'; + $recipients[privatemsg_recipient_key($recipient)] = $recipient; continue; } } @@ -745,14 +832,14 @@ function privatemsg_sql_list(&$fragments // @todo: Replace this with a single query similiar to the tag list. if ($GLOBALS['db_type'] == 'pgsql') { // PostgreSQL does not know GROUP_CONCAT, so a subquery is required. - $fragments['select'][] = "array_to_string(array(SELECT DISTINCT textin(int4out(pmia.uid)) + $fragments['select'][] = "array_to_string(array(SELECT DISTINCT textin(int4out(pmia.recipient)) FROM {pm_index} pmia - WHERE pmia.thread_id = pmi.thread_id), ',') AS participants"; + WHERE pmia.type = 'user' AND pmia.thread_id = pmi.thread_id), ',') AS participants"; } else { - $fragments['select'][] = '(SELECT GROUP_CONCAT(DISTINCT pmia.uid SEPARATOR ",") + $fragments['select'][] = "(SELECT GROUP_CONCAT(DISTINCT pmia.recipient SEPARATOR ',') FROM {pm_index} pmia - WHERE pmia.thread_id = pmi.thread_id) AS participants'; + WHERE pmia.type = 'user' AND pmia.thread_id = pmi.thread_id) AS participants"; } } if (in_array('thread_started', $fields)) { @@ -762,8 +849,8 @@ function privatemsg_sql_list(&$fragments $fragments['inner_join'][] = 'INNER JOIN {pm_index} pmi ON pm.mid = pmi.mid'; // Only load undeleted messages of the current user and group by thread. - $fragments['where'][] = 'pmi.uid = %d'; - $fragments['query_args']['where'][] = $account->uid; + $fragments['where'][] = "pmi.recipient = %d AND pmi.type IN ('user', 'hidden')"; + $fragments['query_args']['where'][] = $account->uid; $fragments['where'][] = 'pmi.deleted = 0'; $fragments['group_by'][] = 'pmi.thread_id'; @@ -801,8 +888,8 @@ function privatemsg_sql_load(&$fragments $fragments['where'][] = 'pmi.mid IN (' . db_placeholders($pmids) . ')'; $fragments['query_args']['where'] += $pmids; if ($account) { - $fragments['where'][] = 'pmi.uid = %d'; - $fragments['query_args']['where'][] = $account->uid; + $fragments['where'][] = "pmi.recipient = %d AND pmi.type IN ('user', 'hidden')"; + $fragments['query_args']['where'][] = $account->uid; } $fragments['order_by'][] = 'pm.timestamp ASC'; $fragments['order_by'][] = 'pm.mid ASC'; @@ -828,9 +915,8 @@ function privatemsg_sql_messages(&$fragm $fragments['query_args']['where'] += $threads; $fragments['inner_join'][] = 'INNER JOIN {pm_message} pm ON (pm.mid = pmi.mid)'; if ($account) { - // Only load the user's messages. - $fragments['where'][] = 'pmi.uid = %d'; - $fragments['query_args']['where'][] = $account->uid; + $fragments['where'][] = "pmi.recipient = %d AND pmi.type IN ('user', 'hidden')"; + $fragments['query_args']['where'][] = $account->uid; } if (!$load_all) { // Also load deleted messages when requested. @@ -857,16 +943,29 @@ function privatemsg_sql_messages(&$fragm * Thread id from which the participants should be loaded. */ function privatemsg_sql_participants(&$fragments, $thread_id) { + global $user; $fragments['primary_table'] = '{pm_index} pmi'; // Only load each participant once since they are listed as recipient for // every message of that thread. - $fragments['select'][] = 'DISTINCT(pmi.uid) AS uid'; - $fragments['select'][] = 'u.name AS name'; + $fragments['select'][] = 'pmi.recipient'; + $fragments['select'][] = 'u.name'; + $fragments['select'][] = 'pmi.type'; - $fragments['inner_join'][] = 'INNER JOIN {users} u ON (u.uid = pmi.uid)'; + $fragments['inner_join'][] = "LEFT JOIN {users} u ON (u.uid = pmi.recipient AND pmi.type IN ('user', 'hidden'))"; $fragments['where'][] = 'pmi.thread_id = %d'; $fragments['query_args']['where'][] = $thread_id; + + $fragments['where'][] = "(pmi.type <> 'hidden') OR (pmi.type = 'hidden' AND pmi.recipient = %d)"; + $fragments['query_args']['where'][] = $user->uid; + + // Only load recipients of messages which are visible for that user. + $fragments['where'][] = '(SELECT 1 FROM {pm_index} pmiu WHERE pmi.mid = pmiu.mid AND pmiu.recipient = %d) = 1'; + $fragments['query_args']['where'][] = $user->uid; + + $fragments['group_by'][] = 'pmi.recipient'; + $fragments['group_by'][] = 'u.name'; + $fragments['group_by'][] = 'pmi.type'; } /** @@ -885,7 +984,7 @@ function privatemsg_sql_unread_count(&$f // Only count new messages that have not been deleted. $fragments['where'][] = 'pmi.deleted = 0'; $fragments['where'][] = 'pmi.is_new = 1'; - $fragments['where'][] = 'pmi.uid = %d'; + $fragments['where'][] = "pmi.recipient = %d AND pmi.type IN ('user', 'hidden')"; $fragments['query_args']['where'][] = $account->uid; } @@ -1032,10 +1131,10 @@ function privatemsg_user($op, &$edit, &$ } // Delete recipient entries of that user. - db_query('DELETE FROM {pm_index} WHERE uid = %d', $account->uid); + db_query("DELETE FROM {pm_index} WHERE recipient = %d and type = 'user'", $account->uid); // DELETE any disable flag for user. - db_query('DELETE from {pm_disable} where uid=%d', $account->uid); + db_query("DELETE from {pm_disable} WHERE uid = %d", $account->uid); break; } } @@ -1135,7 +1234,7 @@ function privatemsg_message_change_delet } if ($account) { - db_query('UPDATE {pm_index} SET deleted = %d WHERE mid = %d AND uid = %d', $delete_value, $pmid, $account->uid); + db_query("UPDATE {pm_index} SET deleted = %d WHERE mid = %d AND recipient = %d AND type IN ('user', 'hidden')", $delete_value, $pmid, $account->uid); } else { // Mark deleted for all users. @@ -1189,10 +1288,14 @@ function privatemsg_new_thread($recipien $message = array(); $message['subject'] = $subject; $message['body'] = $body; - // Make sure that recipients are keyed by user id and are not added + // Make sure that recipients are keyed correctly and are not added // multiple times. foreach ($recipients as $recipient) { - $message['recipients'][$recipient->uid] = $recipient; + if (!isset($recipient->type)) { + $recipient->type = 'user'; + $recipient->recipient = $recipient->uid; + } + $message['recipients'][privatemsg_recipient_key($recipient)] = $recipient; } // Set custom options, if any. @@ -1209,6 +1312,9 @@ function privatemsg_new_thread($recipien $validated = _privatemsg_validate_message($message); if ($validated['success']) { $validated['message'] = _privatemsg_send($message); + if ($validated['message'] !== FALSE) { + _privatemsg_handle_recipients($validated['message']['mid'], $validated['message']['recipients'], $validated['message']['author'], FALSE); + } } return $validated; @@ -1284,11 +1390,14 @@ function privatemsg_reply($thread_id, $b $validated = _privatemsg_validate_message($message); if ($validated['success']) { $validated['message'] = _privatemsg_send($message); + if ($validated['message'] !== FALSE) { + _privatemsg_handle_recipients($validated['message']['mid'], $validated['message']['recipients'], $validated['message']['author'], FALSE); + } } return $validated; } -function _privatemsg_validate_message(&$message, $form = FALSE) { +function _privatemsg_validate_message(&$message, $form = FALSE, $display_block_messages = TRUE) { $messages = array('error' => array(), 'warning' => array()); if (!privatemsg_user_access('write privatemsg', $message['author'])) { // no need to do further checks in this case... @@ -1356,9 +1465,12 @@ function _privatemsg_validate_message(&$ if (!empty($message['recipients']) && is_array($message['recipients'])) { foreach (module_invoke_all('privatemsg_block_message', $message['author'], $message['recipients']) as $blocked) { - unset($message['recipients'][$blocked['uid']]); + unset($message['recipients'][$blocked['recipient']]); if ($form) { - drupal_set_message($blocked['message'], 'warning'); + // Don't display information about blocked users when replying. + if ($display_block_messages) { + drupal_set_message($blocked['message'], 'warning'); + } } else { $messages['warning'][] = $blocked['message']; @@ -1400,14 +1512,14 @@ function _privatemsg_send($message) { drupal_alter('privatemsg_message_presave', $message); - $index_sql = "INSERT INTO {pm_index} (mid, thread_id, uid, is_new, deleted) VALUES (%d, %d, %d, %d, 0)"; + $index_sql = "INSERT INTO {pm_index} (mid, thread_id, recipient, type, is_new, deleted) VALUES (%d, %d, %d, '%s', %d, 0)"; if (isset($message['read_all']) && $message['read_all']) { // The message was sent in read all mode, add the author as recipient to all // existing messages. $query_messages = _privatemsg_assemble_query('messages', array($message['thread_id']), NULL); $conversation = db_query($query_messages['query']); while ($result = db_fetch_array($conversation)) { - if (!db_query($index_sql, $result['mid'], $message['thread_id'], $message['author']->uid, 0)) { + if (!db_query($index_sql, $result['mid'], $message['thread_id'], $message['author']->uid, 'user', 0)) { return FALSE; } } @@ -1433,7 +1545,7 @@ function _privatemsg_send($message) { // 2) Save message to recipients. // Each recipient gets a record in the pm_index table. foreach ($message['recipients'] as $recipient) { - if (!db_query($index_sql, $mid, $message['thread_id'], $recipient->uid, 1) ) { + if (!db_query($index_sql, $mid, $message['thread_id'], $recipient->recipient, $recipient->type, 1) ) { // We assume if one insert failed then the rest may fail too against the // same table. return FALSE; @@ -1442,10 +1554,10 @@ function _privatemsg_send($message) { // When author is also the recipient, we want to set message to UNREAD. // All other times the message is set to READ. - $is_new = isset($message['recipients'][$message['author']->uid]) ? 1 : 0; + $is_new = isset($message['recipients']['user_' . $message['author']->uid]) ? 1 : 0; // Also add a record for the author to the pm_index table. - if (!db_query($index_sql, $mid, $message['thread_id'], $message['author']->uid, $is_new)) { + if (!db_query($index_sql, $mid, $message['thread_id'], $message['author']->uid, 'user', $is_new)) { return FALSE; } @@ -1493,7 +1605,7 @@ function privatemsg_get_link($recipients if (variable_get('privatemsg_display_link_self', TRUE) == FALSE && $account->uid == $recipient->uid) { continue; } - if (count(module_invoke_all('privatemsg_block_message', $account, array($recipient))) > 0) { + if (count(module_invoke_all('privatemsg_block_message', $account, array(privatemsg_recipient_key($recipient) => $recipient))) > 0) { continue; } $validated[] = $recipient->uid; @@ -1546,9 +1658,9 @@ function privatemsg_message_load_multipl $messages = array(); while ($message = db_fetch_array($result)) { // Load author of message. - if (!($message['author'] = user_load($message['author']))) { + if (!($message['author'] = privatemsg_user_load($message['author']))) { // If user does not exist, load anonymous user. - $message['author'] = user_load(array('uid' => 0)); + $message['author'] = privatemsg_user_load(0); } $returned = module_invoke_all('privatemsg_message_load', $message); if (!empty($returned)) { @@ -1722,9 +1834,9 @@ function privatemsg_thread_change_status global $user; $account = drupal_clone($user); } - // Merge status and uid with the threads list. array_merge() will not overwrite/ignore thread_id 1. + // Merge status and uid with the exising thread list. $params = array_merge(array($status, $account->uid), $threads); - db_query('UPDATE {pm_index} SET is_new = %d WHERE uid = %d AND thread_id IN ('. db_placeholders($threads) .')', $params); + db_query("UPDATE {pm_index} SET is_new = %d WHERE recipient = %d and type IN ('user', 'hidden') AND thread_id IN (" . db_placeholders($threads) . ')', $params); if ($status == PRIVATEMSG_UNREAD) { drupal_set_message(format_plural(count($threads), 'Marked 1 thread as unread.', 'Marked @count threads as unread.')); @@ -1839,15 +1951,15 @@ function privatemsg_privatemsg_block_mes $blocked = array(); if (privatemsg_is_disabled($author)) { $blocked[] = array( - 'uid' => $author->uid, + 'recipient' => 'user_' . $author->uid, 'message' => t('You have disabled private message sending and receiving.'), ); } foreach ($recipients as $recipient) { if (privatemsg_is_disabled($recipient)) { $blocked[] = array( - 'uid' => $recipient->uid, - 'message' => t('%recipient has disabled private message receiving.', array('%recipient' => $recipient->name)) + 'recipient' => 'user_' . $recipient->uid, + 'message' => t('%recipient has disabled private message receiving.', array('%recipient' => $recipient->name)), ); } } @@ -1917,7 +2029,7 @@ function privatemsg_link($type, $object, } $types = array_filter(variable_get('privatemsg_link_node_types', array())); - $url = privatemsg_get_link(user_load($object->uid)); + $url = privatemsg_get_link(privatemsg_user_load($object->uid)); if ($type == 'node' && in_array($object->type, $types) && !empty($url) && ($teaser == FALSE || variable_get('privatemsg_display_on_teaser', 1))) { $links['privatemsg_link'] = array( 'title' => t('Send author a message'), @@ -1943,4 +2055,261 @@ function privatemsg_views_api() { 'api' => 2, 'path' => drupal_get_path('module', 'privatemsg') . '/views', ); -} \ No newline at end of file +} + +/** + * Privatemsg wrapper function for user_load() with a static cache. + * + * The function additionaly also adds the privatemsg specific recipient id (uid) + * and recipient type to the user object. + * + * @param $uid + * Which uid to load. + * @return + * If existing for the passed in uid, the user object with the recipient + * and type properties. + */ +function privatemsg_user_load($uid) { + static $user_cache = array(); + if (!array_key_exists($uid, $user_cache)) { + $user_cache[$uid] = user_load($uid); + if (is_object($user_cache[$uid])) { + $user_cache[$uid]->recipient = $user_cache[$uid]->uid; + $user_cache[$uid]->type = 'user'; + } + } + return $user_cache[$uid]; +} + +/** + * Return key for a recipient object used for arrays. + * @param $recipient + * Recipient object, must have type and recipient properties. + * @return + * A string that looks like type_id. + * + * @ingroup types + */ +function privatemsg_recipient_key($recipient) { + return $recipient->type . '_' . $recipient->recipient; +} + +/** + * Returns an array of defined recipient types. + * + * @return + * Array of recipient types + * @see hook_privatemsg_recipient_type_info() + * + * @ingroup types + */ +function privatemsg_recipient_get_types() { + static $types = NULL; + + if ($types === NULL) { + $types = module_invoke_all('privatemsg_recipient_type_info'); + if (!is_array($types)) { + $types = array(); + } + drupal_alter('privatemsg_recipient_type_info', $types); + uasort($types, 'element_sort'); + } + return $types; +} +/** + * Return a single recipient type information. + * @param $type + * Name of the recipient type. + * @return + * Array with the recipient type definition. NULL if the type doesn't exist. + * + * @ingroup types + */ +function privatemsg_recipient_get_type($type) { + $types = privatemsg_recipient_get_types(); + if (!is_string($type)) { + exit; + } + if (isset($types[$type])) { + return $types[$type]; + } +} + +/** + * Add or remove a recipient to an existing message. + * + * @param $mid + * Message id for which the recipient should be added. + * @param $recipient + * Recipient id that should be added, for example uid. + * @param $type + * Type of the recipient, defaults to hidden. + * @param $add + * If TRUE, adds the recipient, if FALSE, removes it. + */ +function privatemsg_message_change_recipient($mid, $uid, $type = 'user', $add = TRUE) { + + $thread_id = db_result(db_query('SELECT thread_id FROM {pm_index} WHERE mid = %d', $mid)); + if ($add) { + // Only add the recipient if he does not block the author. + $author_uid = db_result(db_query('SELECT author FROM {pm_message} WHERE mid = %d', $mid)); + $recipient = privatemsg_user_load($uid); + $user_blocked = module_invoke_all('privatemsg_block_message', privatemsg_user_load($author_uid), array(privatemsg_recipient_key($recipient) => $recipient)); + if (count($user_blocked) <> 0) { + return; + } + + // Make sure to only add a recipient once. The types user and hidden are + // considered equal here. + if ($type == 'user' || $type == 'hidden') { + $exists = db_result(db_query("SELECT 1 FROM {pm_index} WHERE type IN ('user', 'hidden') AND recipient = %d AND mid = %d", $uid, $mid)); + } + else { + $exists = db_result(db_query("SELECT 1 FROM {pm_index} WHERE type = '%s' AND recipient = %d AND mid = %d", $type, $uid, $mid)); + } + if (!$exists) { + $add_sql = "INSERT INTO {pm_index} (mid, thread_id, recipient, type, is_new, deleted) VALUES (%d, %d, %d, '%s', 1, 0)"; + db_query($add_sql, $mid, $thread_id, $uid, $type); + } + } + else { + $delete_sql = "DELETE FROM {pm_index} WHERE mid = %d AND thread_id = %d AND recipient = %d AND type = '%s'"; + db_query($delete_sql, $mid, $thread_id, $uid, $type); + } + module_invoke_all('privatemsg_message_recipient_changed', $mid, $thread_id, $uid, $type, $add); +} + +/** + * Handle the non-user recipients of a new message. + * + * Either process them directly if they have less than a certain amount of users + * or, if enabled, add them to a batch. + * + * @param $mid + * Message id for which the recipients are processed. + * @param $recipients + * Array of recipients. + * @param $use_batch + * Use batch API to process recipients. + */ +function _privatemsg_handle_recipients($mid, $recipients, $author, $use_batch = TRUE) { + $batch = array( + 'title' => t('Processing recipients'), + 'operations' => array(), + 'file' => drupal_get_path('module', 'privatemsg') . '/privatemsg.pages.inc', + 'progress_message' => t('Processing recipients'), + ); + $small_threshold = variable_get('privatemsg_recipient_small_threshold', 100); + foreach ($recipients as $recipient) { + // Add a batch operation to press non-user recipient types. + if ($recipient->type != 'user' && $recipient->type != 'hidden') { + $type = privatemsg_recipient_get_type($recipient->type); + // Try to fetch 100 recipients, if there are 99 or less, process them + // now and don't use batch api. + $load_function = $type['generate recipients']; + if (!is_callable($load_function)) { + db_query("UPDATE {pm_index} SET is_new = %d WHERE mid = %d AND recipient = %d AND type = '%s'", PRIVATEMSG_READ, $mid, $recipient->recipient, $recipient->type); + drupal_set_message(t('Recipient type %type is not correctly implemented', array('%type' => $recipient->type)), 'error'); + continue; + } + $uids = $load_function($recipient, $author, $small_threshold, 0); + if (count($uids) < $small_threshold) { + if (!empty($uids)) { + foreach ($uids as $uid) { + privatemsg_message_change_recipient($mid, $uid, 'hidden'); + } + } + db_query("UPDATE {pm_index} SET is_new = %d WHERE mid = %d AND recipient = %d AND type = '%s'", PRIVATEMSG_READ, $mid, $recipient->recipient, $recipient->type); + continue; + } + if ($use_batch) { + $batch['operations'][] = array('privatemsg_load_recipients', array($mid, $recipient, $author)); + } + } + } + + // Set batch if there are outstanding operations. + if ($use_batch && !empty($batch['operations'])) { + batch_set($batch); + } +} + +/** + * This function is used to test if the current user has write/view access + * for a specific recipient type. + * + * @param $type_name + * The name of the recipient type. + * @param $permission + * Which permission should be checked: 'write access' or 'view access'. + * + * @return + * TRUE if the user has that permission (or not permission is defined) and + * FALSE if not. + * + * @ingroup types + */ +function privatemsg_recipient_access($type_name, $permission) { + if (($type = privatemsg_recipient_get_type($type_name)) && isset($type[$permission])) { + if (is_bool($type[$permission])) { + return $types[$permission]; + } + return user_access($type[$permission]); + } + // If no access permission is defined, access is allowed. + return TRUE; +} + +/** + * Format a single participant. + * + * @param $participant + * The participant object to format. + * + * @ingroup types. + */ +function privatemsg_recipient_format($participant) { + if ((!isset($participant->type) || $participant->type == 'user') && (!empty($participant->name) || isset($participant->uid) && $participant->uid === '0')) { + if (!isset($participant->uid)) { + $participant->uid = $participant->recipient; + } + return theme('username', $participant); + } + else { + $type = privatemsg_recipient_get_type($participant->type); + if (isset($type['format'])) { + return theme($type['format'], $participant, array()); + } + return NULL; + } +} + +/** + * Implements hook_privatemsg_recipient_types_info(). + */ +function privatemsg_privatemsg_recipient_type_info() { + return array( + 'user' => array( + 'name' => t('User'), + 'description' => t('Enter a user name to write a message to a user.'), + 'load' => 'privatemsg_user_load', + 'format' => 'username', + 'autocomplete' => 'privatemsg_user_autocomplete', + // Make sure this comes always last. + '#weight' => 50, + ), + ); +} + +/** + * Implements callback_recipient_autocomplete(). + */ +function privatemsg_user_autocomplete($fragment, $names, $limit) { + $query = _privatemsg_assemble_query('autocomplete', $fragment, $names); + $result = db_query_range($query['query'], $fragment, 0, $limit); + $matches = array(); + while ($user = db_fetch_object($result)) { + $matches[] = $user->name; + } + return $matches; +} Index: privatemsg.pages.inc =================================================================== RCS file: /cvs/drupal/contributions/modules/privatemsg/privatemsg.pages.inc,v retrieving revision 1.5 diff -u -p -r1.5 privatemsg.pages.inc --- privatemsg.pages.inc 15 Jun 2010 08:15:11 -0000 1.5 +++ privatemsg.pages.inc 19 Jun 2010 12:51:21 -0000 @@ -30,7 +30,7 @@ function privatemsg_list(&$form_state, $ if (!privatemsg_user_access('read all private messages')) { drupal_set_message(t("You do not have sufficient rights to view someone else's messages"), 'warning'); } - elseif ($account_check = user_load(array('uid' => $uid))) { + elseif ($account_check = privatemsg_user_load($uid)) { // Has rights and user_load return an array so user does exist $account = $account_check; } @@ -196,23 +196,29 @@ function privatemsg_new(&$form_state, $r $usercount = 0; $to = array(); - $to_themed = array(); $blocked = FALSE; foreach ($recipients as $recipient) { - if (in_array($recipient->name, $to)) { + if ($recipient->type == 'hidden') { + continue; + } + if (isset($to[privatemsg_recipient_key($recipient)])) { // We already added the recipient to the list, skip him. continue; } + if (!privatemsg_recipient_access($recipient->type, 'write access')) { + // User does not have access to write to this recipient, continue. + continue; + } + // Check if another module is blocking the sending of messages to the recipient by current user. - $user_blocked = module_invoke_all('privatemsg_block_message', $user, array($recipient->uid => $recipient)); - if (!count($user_blocked) <> 0 && $recipient->uid) { - if ($recipient->uid == $user->uid) { + $user_blocked = module_invoke_all('privatemsg_block_message', $user, array(privatemsg_recipient_key($recipient) => $recipient)); + if (!count($user_blocked) <> 0 && $recipient->recipient) { + if ($recipient->type == 'user' && $recipient->recipient == $user->uid) { $usercount++; // Skip putting author in the recipients list for now. continue; } - $to[] = $recipient->name; - $to_themed[$recipient->uid] = theme('username', $recipient); + $to[privatemsg_recipient_key($recipient)] = privatemsg_recipient_format($recipient); } else { // Recipient list contains blocked users. @@ -222,8 +228,7 @@ function privatemsg_new(&$form_state, $r if (empty($to) && $usercount >= 1 && !$blocked) { // Assume the user sent message to own account as if the usercount is one or less, then the user sent a message but not to self. - $to[] = $user->name; - $to_themed[$user->uid] = theme('username', $user); + $to['user_' . $user->uid] = privatemsg_recipient_format($user); } if (!empty($to)) { @@ -263,10 +268,20 @@ function privatemsg_new(&$form_state, $r '#value' => $user, ); if (is_null($thread_id)) { + + $description_array = array(); + foreach (privatemsg_recipient_get_types() as $name => $type) { + if (privatemsg_recipient_access('write access', $name)) { + $description_array[] = $type['description']; + } + } + $description = t('Enter the recipient, separate recipients with commas.'); + $description .= theme('item_list', $description_array); + $form['privatemsg']['recipient'] = array( '#type' => 'textfield', '#title' => t('To'), - '#description' => t('Separate multiple names with commas.'), + '#description' => $description, '#default_value' => $recipients_string, '#required' => TRUE, '#weight' => -10, @@ -333,7 +348,7 @@ function privatemsg_new(&$form_state, $r '#type' => 'value', '#default_value' => $subject, ); - $recipients_string_themed = implode(', ', $to_themed); + $recipients_string_themed = implode(', ', $to); $form['privatemsg']['recipient_display'] = array( '#value' => '
'. t('Reply to thread:
Recipients: !to', array('!to' => $recipients_string_themed)) .'