uid != $user->uid) { if (!privatemsg_user_access('read all private messages')) { return MENU_ACCESS_DENIED; } // Has rights and user_load return an array so user does exist. $account = $account_check; } return drupal_get_form('privatemsg_list', $argument, $account); } function privatemsg_list(&$form_state, $argument, $account) { $query = _privatemsg_assemble_query('list', $account, $argument); $result = pager_query($query['query'], variable_get('privatemsg_per_page', 25), 0, $query['count']); $threads = array(); $form = array('#list_argument' => $argument); $form['#data'] = array(); while ($row = db_fetch_array($result)) { // Store the raw row data. $form['#data'][$row['thread_id']] = $row; // store thread id for the checkboxes array $threads[$row['thread_id']] = ''; } if (!empty($form['#data'])) { $form['actions'] = _privatemsg_action_form($argument); } // Save the currently active account, used for actions. $form['account'] = array('#type' => 'value', '#value' => $account); // Define checkboxes, pager and theme $form['threads'] = array('#type' => 'checkboxes', '#options' => $threads); $form['pager'] = array('#value' => theme('pager'), '#weight' => 20); $form['#theme'] = 'privatemsg_list'; // Store the account for which the threads are displayed. $form['#account'] = $account; return $form; } /** * Process privatemsg_list form submissions. * * Execute the chosen action on the selected messages. This function is * based on node_admin_nodes_submit(). */ function privatemsg_list_submit($form, &$form_state) { // Load all available operation definitions. $operations = module_invoke_all('privatemsg_thread_operations', $form['#list_argument']); drupal_alter('privatemsg_thread_operations', $operations, $form['#list_argument']); // Default "default" operation, which won't do anything. $operation = array('callback' => 0); // Check if a valid operation has been submitted. if (isset($form_state['values']['operation']) && isset($operations[$form_state['values']['operation']])) { $operation = $operations[$form_state['values']['operation']]; } // Load all keys where the value is the current op. $keys = array_keys($form_state['values'], $form_state['values']['op']); // The first one is op itself, we need to use the second. if (isset($keys[1]) && isset($operations[$keys[1]])) { $operation = $operations[$keys[1]]; } // Only execute something if we have a valid callback and at least one checked thread. if (!empty($operation['callback'])) { privatemsg_operation_execute($operation, $form_state['values']['threads'], $form_state['values']['account']); } $form_state['rebuild'] = TRUE; $form_state['input'] = array(); } /** * Menu callback for viewing a thread. * * @param $thread * A array containing all information about a specific thread, generated by * privatemsg_thread_load(). * @return * The page content. * @see privatemsg_thread_load() */ function privatemsg_view($thread) { drupal_set_title(check_plain($thread['subject'])); if ($thread['to'] != $thread['message_count'] || !empty($thread['start'])) { // Generate paging links. $older = ''; if (isset($thread['older_start'])) { $options = array( 'query' => array('start' => $thread['older_start']), 'title' => t('Display older messages'), ); $older = l(t('<<'), privatemsg_get_dynamic_url_prefix() . '/view/' . $thread['thread_id'], $options); } $newer = ''; if (isset($thread['newer_start'])) { $options = array( 'query' => array('start' => $thread['newer_start']), 'title' => t('Display newer messages'), ); $newer = l(t('>>'), privatemsg_get_dynamic_url_prefix() . '/view/' . $thread['thread_id'], $options); } $substitutions = array('@from' => $thread['from'], '@to' => $thread['to'], '@total' => $thread['message_count'], '!previous_link' => $older, '!newer_link' => $newer); $title = t('!previous_link Displaying messages @from - @to of @total !newer_link', $substitutions); $content['pager'] = array( '#value' => trim($title), '#prefix' => '
', '#suffix' => '
', '#weight' => 3, ); } // Render the participants. $content['participants']['#value'] = theme('privatemsg_recipients', $thread); $content['participants']['#weight'] = -5; // Render the messages. $output = ''; $i = 1; $count = count($thread['messages']); foreach ($thread['messages'] as $pmid => $message) { // Add CSS classes. $message['classes'] = array('privatemsg-message', 'privatemsg-message-' . $i, $i % 2 == 1 ? 'privatemsg-message-even' : 'privatemsg-message-odd'); if (!empty($message['is_new'])) { // Mark message as read. privatemsg_message_change_status($pmid, PRIVATEMSG_READ, $thread['user']); $message['classes'][] = 'privatemsg-message-new'; } if ($i == 1) { $message['classes'][] = 'privatemsg-message-first'; } if ($i == $count) { $message['classes'][] = 'privatemsg-message-last'; } $i++; $output .= theme('privatemsg_view', $message); } $content['messages']['#value'] = $output; $content['messages']['#weight'] = 0; // Display the reply form if user is allowed to use it. if (privatemsg_user_access('write privatemsg') || privatemsg_user_access('reply only privatemsg')) { $content['reply']['#value'] = drupal_get_form('privatemsg_form_reply', $thread); $content['reply']['#weight'] = 5; } // Check after calling the privatemsg_new form so that this message is only // displayed when we are not sending a message. if ($thread['read_all']) { // User has permission to read all messages AND is not a participant of the current thread. drupal_set_message(t('This conversation is being viewed with escalated privileges and may not be the same as shown to normal users.'), 'warning'); } // Allow other modules to hook into the $content array and alter it. drupal_alter('privatemsg_view_messages', $content, $thread); return drupal_render($content); } /** * Form builder function; Write a new private message. */ function privatemsg_new(&$form_state, $recipients = '', $subject = '') { global $user; // Convert recipients to array of user objects. $unique = FALSE; if (!empty($recipients) && is_string($recipients) || is_int($recipients)) { $unique = TRUE; $recipients = _privatemsg_generate_user_array($recipients); } else { $recipients = array(); } // Subject has / encoded twice if clean urls are enabled to get it through // mod_rewrite and the menu system. Decode it once more. $subject = str_replace('%2F', '/', $subject); if (isset($form_state['values'])) { if (isset($form_state['values']['recipient'])) { $recipients_plain = $form_state['values']['recipient']; } $subject = $form_state['values']['subject']; } else { $to = _privatemsg_get_allowed_recipients($recipients); $recipients_plain = ''; if (!empty($to)) { $to_plain = array(); $to_title = array(); foreach ($to as $recipient) { $to_plain[] = privatemsg_recipient_format($recipient, array('plain' => TRUE, 'unique' => $unique)); $to_title[] = privatemsg_recipient_format($recipient, array('plain' => TRUE)); } $recipients_plain = implode(', ', $to_plain); $recipients_title = implode(', ', $to_title); } } if (!empty($recipients_title)) { drupal_set_title(t('Write new message to %recipient', array('%recipient' => $recipients_title))); } else { drupal_set_title(t('Write new message')); } $form = array( '#access' => privatemsg_user_access('write privatemsg'), ); $form += _privatemsg_form_base_fields($form_state); $description_array = array(); foreach (privatemsg_recipient_get_types() as $name => $type) { if (privatemsg_recipient_access($name, 'write')) { $description_array[] = $type['description']; } } $description = t('Enter the recipient, separate recipients with commas.'); $description .= theme('item_list', array('items' => $description_array)); $form['recipient'] = array( '#type' => 'textfield', '#title' => t('To'), '#description' => $description, '#default_value' => $recipients_plain, '#required' => TRUE, '#weight' => -10, '#size' => 50, '#autocomplete_path' => 'messages/autocomplete', // Do not hardcode #maxlength, make it configurable by number of recipients, not their name length. ); $form['subject'] = array( '#type' => 'textfield', '#title' => t('Subject'), '#size' => 50, '#maxlength' => 255, '#default_value' => $subject, '#weight' => -5, ); $url = privatemsg_get_dynamic_url_prefix(); if (isset($_REQUEST['destination'])) { $url = $_REQUEST['destination']; } $form['cancel'] = array( '#value' => l(t('Cancel'), $url, array('attributes' => array('id' => 'edit-cancel'))), '#weight' => 20, ); return $form; } /** * Form builder function; Write a reply to a thread. */ function privatemsg_form_reply(&$form_state, $thread) { $form = array( '#access' => privatemsg_user_access('write privatemsg') || privatemsg_user_access('reply only privatemsg'), ); $to = _privatemsg_get_allowed_recipients($thread['participants'], $thread['thread_id']); if (!empty($to)) { $recipients = _privatemsg_format_participants($to); } else { // Display a message if some users are blocked. // @todo: Move this check out of the form, don't use the form in that case. if (count(_privatemsg_blocked_messages())) { $blocked = t('You can not reply to this conversation because all recipients are blocked.'); $blocked .= theme('item_list', _privatemsg_blocked_messages()); $form['blocked']['#value'] = $blocked; } else { $form['#access'] = FALSE; } return $form; } $form += _privatemsg_form_base_fields($form_state); $form['cancel'] = array( '#value' => l(t('Clear'), $_GET['q'], array('attributes' => array('id' => 'edit-cancel'))), '#weight' => 20, ); $form['thread_id'] = array( '#type' => 'value', '#value' => $thread['thread_id'], ); $form['subject'] = array( '#type' => 'value', '#default_value' => $thread['subject'], ); $form['reply'] = array( '#value' => '

' . t('Reply') . '

', '#weight' => -10, ); $form['recipient_display'] = array( '#value' => '

'. t('Reply to thread:
Recipients: !to', array('!to' => $recipients)) .'

', '#weight' => -10, ); $form['read_all'] = array( '#type' => 'value', '#value' => $thread['read_all'], ); return $form; } /** * Returns the common fields of the reply and new form. */ function _privatemsg_form_base_fields(&$form_state) { global $user; if (isset($form_state['privatemsg_preview'])) { $form['message_header'] = array( '#type' => 'fieldset', '#title' => empty($form_state['validate_built_message']['thread_id']) ? check_plain($form_state['validate_built_message']['subject']) : t('Preview'), '#attributes' => array('class' => 'preview'), '#weight' => -10, ); $form['message_header']['message_preview'] = array( '#value' => $form_state['privatemsg_preview'], ); } $form['author'] = array( '#type' => 'value', '#value' => $user, ); $form['body'] = array( '#type' => 'textarea', '#title' => t('Message'), '#rows' => 6, '#weight' => -3, '#resizable' => TRUE, ); $format = FILTER_FORMAT_DEFAULT; // The input filter widget looses the format during preview, specify it // explicitly. if (isset($form_state['values']) && array_key_exists('format', $form_state['values'])) { $format = $form_state['values']['format']; } $form['format'] = filter_form($format); $form['format']['#access'] = privatemsg_user_access('select text format for privatemsg'); if (variable_get('privatemsg_display_preview_button', FALSE)) { $form['preview'] = array( '#type' => 'submit', '#value' => t('Preview message'), '#validate' => array('privatemsg_new_validate'), '#submit' => array('privatemsg_new_preview'), '#weight' => 10, ); } $form['submit'] = array( '#type' => 'submit', '#value' => t('Send message'), '#weight' => 15, '#validate' => array('privatemsg_new_validate'), '#submit' => array('privatemsg_new_submit'), ); return $form; } /** * Check if the current user is allowed to write these recipients. * * @param $recipients * Array of recipient objects. * * @return * Array of allowed recipient objects. */ function _privatemsg_get_allowed_recipients($recipients, $thread_id = NULL) { global $user; $usercount = 0; $valid = array(); $blocked_messages = &_privatemsg_blocked_messages(); $blocked_messages = array(); foreach ($recipients as $recipient) { // Allow to pass in normal user objects. if (empty($recipient->type)) { $recipient->type = 'user'; $recipient->recipient = $recipient->uid; } if ($recipient->type == 'hidden') { continue; } if (isset($valid[privatemsg_recipient_key($recipient)])) { // We already added the recipient to the list, skip him. continue; } if (!privatemsg_recipient_access($recipient->type, 'write', $recipient)) { // User does not have access to write to this recipient, continue. continue; } if ($recipient->type == 'user' && $recipient->recipient == $user->uid) { // Skip putting author in the recipients list for now. // Will be added if he is the only recipient. $usercount++; continue; } $valid[privatemsg_recipient_key($recipient)] = $recipient; } foreach (module_invoke_all('privatemsg_block_message', $user, $valid, array('thread_id' => $thread_id)) as $blocked) { // Unset the recipient. unset($valid[$blocked['recipient']]); // Store blocked messages. These are only displayed if all recipients // are blocked. $blocked_messages[] = $blocked['message']; } if (empty($valid) && $usercount >= 1 && empty($blocked_messages)) { // 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. $valid['user_' . $user->uid] = $user; } return $valid; } /** * Static storage for blocked messages. */ function &_privatemsg_blocked_messages() { static $blocked_messages = array(); return $blocked_messages; } function privatemsg_new_validate($form, &$form_state) { // The actual message that is being sent, we create this during validation and pass to submit to send out. $message = $form_state['values']; $message['timestamp'] = time(); $trimed_body = trim(truncate_utf8(strip_tags($message['body']), 50, TRUE, TRUE)); if (empty($message['subject']) && !empty($trimed_body)) { $message['subject'] = $trimed_body; } // Only parse the user string for a new thread. if (!isset($message['thread_id'])) { list($message['recipients'], $invalid, $duplicates, $denieds) = _privatemsg_parse_userstring($message['recipient']); } else { // Load participants. Limit recipients to visible unless read_all is TRUE. $message['recipients'] = _privatemsg_load_thread_participants($message['thread_id'], $message['read_all'] ? FALSE : $message['author']); } if (!empty($invalid)) { // Display information about invalid recipients. drupal_set_message(t('The following users will not receive this private message: @invalid.', array('@invalid' => implode(", ", $invalid))), 'error'); } if (!empty($denieds)) { // Display information about denied recipients. drupal_set_message(t('You do not have access to write these recipients: @denieds.', array('@denieds' => implode(", ", $denieds))), 'error'); } if (!empty($duplicates)) { // Add JS and CSS to allow choosing the recipient. drupal_add_js(drupal_get_path('module', 'privatemsg') . '/privatemsg-alternatives.js'); // Display information about recipients that couldn't be identified // uniquely. $js_duplicates = array(); foreach ($duplicates as $string => $duplicate) { $alternatives = array(); foreach ($duplicate as $match) { $formatted_match = privatemsg_recipient_format($match, array('plain' => TRUE, 'unique' => TRUE)); $js_duplicates[$formatted_match] = $string; $alternatives[] = '' . $formatted_match . ''; } // Build a formatted list of possible recipients. $alternatives = theme('item_list', $alternatives, NULL, 'ul', array('class' => 'action-links')); form_set_error('recipient', '
' . t('The site has multiple recipients named %string. Please choose your intended recipient: !list', array('%string' => $string, '!list' => $alternatives)) . '
'); } // Also make that information available to the javascript replacement code. drupal_add_js(array('privatemsg_duplicates' => $js_duplicates), 'setting'); } $validated = _privatemsg_validate_message($message, TRUE); foreach ($validated['messages'] as $type => $text) { drupal_set_message($text, $type); } $form_state['validate_built_message'] = $message; } /** * Submit callback for the privatemsg_new form. */ function privatemsg_new_submit($form, &$form_state) { // Clear form_state storage so that it does not rebuild. $form_state['storage'] = NULL; $status = _privatemsg_send($form_state['validate_built_message']); // Format each recipient. $recipient_names = array(); foreach ($form_state['validate_built_message']['recipients'] as $recipient) { $recipient_names[] = privatemsg_recipient_format($recipient); } if ($status !== FALSE ) { _privatemsg_handle_recipients($status['mid'], $status['recipients']); drupal_set_message(t('A message has been sent to !recipients.', array('!recipients' => implode(', ', $recipient_names)))); // Only redirect on new threads. if ($status['mid'] == $status['thread_id'] || variable_get('privatemsg_default_redirect_reply', FALSE)) { $redirect = variable_get('privatemsg_default_redirect', ''); if ($redirect == '') { // Forward to the new message in the thread. $form_state['redirect'] = array(privatemsg_get_dynamic_url_prefix() . '/view/' . $status['thread_id'], NULL, 'privatemsg-mid-' . $status['mid']); } elseif (!empty($redirect)) { $form_state['redirect'] = $redirect; } } // Replace [new-message] placeholder with actual destination. if (!empty($_REQUEST['destination']) && $_REQUEST['destination'] == '[new-message]') { // url() can not be used because it does create an path with base path and // prefix. $_REQUEST['destination'] = urlencode((privatemsg_get_dynamic_url_prefix() . '/view/' . $status['thread_id'] . '#' . 'privatemsg-mid-' . $status['mid'])); } } else { drupal_set_message(t('An attempt to send a message may have failed when sending to !recipients.', array('!recipients' => implode(', ', $recipient_names))), 'error'); } } function privatemsg_new_preview($form, &$form_state) { drupal_validate_form($form['form_id']['#value'], $form, $form_state); if (!form_get_errors()) { $form_state['validate_built_message']['classes'] = array('privatemsg-message', 'privatemsg-message-preview'); $form_state['privatemsg_preview'] = theme('privatemsg_view', $form_state['validate_built_message']); } $form_state['rebuild'] = TRUE; // this forces our form to be rebuilt instead of being submitted. } function privatemsg_delete($form_state, $thread, $message) { $form['mid'] = array( '#type' => 'value', '#value' => $message['mid'], ); $form['delete_destination'] = array( '#type' => 'value', '#value' => count($thread['messages']) > 1 ? privatemsg_get_dynamic_url_prefix() . '/view/' . $message['thread_id'] : privatemsg_get_dynamic_url_prefix(), ); if (privatemsg_user_access('read all private messages')) { $form['delete_options'] = array( '#type' => 'checkbox', '#title' => t('Delete this message for all users?'), '#description' => t('Tick the box to delete the message for all users.'), '#default_value' => FALSE, ); } return confirm_form($form, t('Are you sure you want to delete this message?'), isset($_GET['destination']) ? $_GET['destination'] : privatemsg_get_dynamic_url_prefix() . '/view/'. $message['thread_id'], t('This action cannot be undone.'), t('Delete'), t('Cancel') ); } function privatemsg_delete_submit($form, &$form_state) { global $user; $account = drupal_clone($user); if ($form_state['values']['confirm']) { if (isset($form_state['values']['delete_options']) && $form_state['values']['delete_options']) { privatemsg_message_change_delete($form_state['values']['mid'], 1); drupal_set_message(t('Message has been deleted for all users.')); } else { privatemsg_message_change_delete($form_state['values']['mid'], 1, $account); drupal_set_message(t('Message has been deleted.')); } } $form_state['redirect'] = $form_state['values']['delete_destination']; } /** * Returns a form which handles and displays thread actions. * * Additional actions can be added with the privatemsg_thread_operations hook. * It is also possible to extend this form with additional buttons or other * elements, in that case, the definitions in the above hook need no label tag, * instead, the submit button key needs to match with the key of the operation. * * @see hook_privatemsg_thread_operations() * * @return * The FAPI definitions for the thread action form. */ function _privatemsg_action_form($type) { $form = array( '#prefix' => '
', '#suffix' => '
', '#weight' => -5, ); // Display all operations which have a label. $operations = module_invoke_all('privatemsg_thread_operations', $type); drupal_alter('privatemsg_thread_operations', $operations, $type); foreach ($operations as $operation => $array) { if (!empty($array['button'])) { $form[$operation] = array( '#type' => 'submit', '#value' => $array['label'], ); } elseif (isset($array['label'])) { $options[$operation] = $array['label']; } } if (!empty($options)) { array_unshift($options, t('Actions...')); $form['operation'] = array( '#type' => 'select', '#options' => $options, '#default_value' => 0, ); $form['submit'] = array( '#prefix' => '
', '#suffix' => '
', '#type' => 'submit', '#value' => t('Execute'), '#submit' => array('privatemsg_list_submit'), '#attributes' => array('class' => 'privatemsg-action-button'), ); } // JS for hiding the execute button. drupal_add_js(drupal_get_path('module', 'privatemsg') .'/privatemsg-list.js'); return $form; } /** * Menu callback for messages/undo/action. * * This function will test if an undo callback is stored in SESSION and * execute it. */ function privatemsg_undo_action() { // Check if a undo callback for that user exists. if (isset($_SESSION['privatemsg']['undo callback']) && is_array($_SESSION['privatemsg']['undo callback'])) { $undo = $_SESSION['privatemsg']['undo callback']; // If the defined undo callback exists, execute it if (isset($undo['function']) && isset($undo['args'])) { // Load the user object. if (isset($undo['args']['account']) && $undo['args']['account'] > 0) { $undo['args']['account'] = user_load((int)$undo['args']['account']); } call_user_func_array($undo['function'], $undo['args']); } // Return back to the site defined by the destination GET param. drupal_goto(); } } /** * Return autocomplete results for usernames. * * Prevents usernames from being used and/or suggested twice. */ function privatemsg_autocomplete($string) { $names = array(); // 1: Parse $string and build list of valid user names. $fragments = explode(',', $string); foreach ($fragments as $index => $name) { if ($name = trim($name)) { $names[$name] = $name; } } // 2: Find the next user name suggestion. $fragment = array_pop($names); $matches = array(); if (!empty($fragment)) { $remaining = 10; $types = privatemsg_recipient_get_types(); foreach ($types as $name => $type) { if (isset($type['autocomplete']) && is_callable($type['autocomplete']) && privatemsg_recipient_access($name, 'write')) { $function = $type['autocomplete']; $return = $function($fragment, $names, $remaining); if (is_array($return) && !empty($return)) { $matches = array_merge($matches, $return); } $remaining = 10 - count($matches); if ($remaining <= 0) { break; } } } } // Allow modules to alter the autocomplete list. drupal_alter('privatemsg_autocomplete', $matches, $names, $fragment); // Format the suggestions. $themed_matches = array(); foreach ($matches as $key => $match) { $themed_matches[$key] = privatemsg_recipient_format($match, array('plain' => TRUE)); } // Check if there are any duplicates. if (count(array_unique($themed_matches)) != count($themed_matches)) { // Loop over matches, look for duplicates of each one. foreach ($themed_matches as $key => $themed_match) { $duplicate_keys = array_keys($themed_matches, $themed_match); if (count($duplicate_keys) > 1) { // There are duplicates, make them unique. foreach ($duplicate_keys as $duplicate_key) { // Reformat them with unique argument. $themed_matches[$duplicate_key] = privatemsg_recipient_format($matches[$duplicate_key], array('plain' => TRUE, 'unique' => TRUE)); } } } } // Prefix the matches and convert them to the correct form for the // autocomplete. $prefix = count($names) ? implode(", ", $names) .", " : ''; $suggestions = array(); foreach ($themed_matches as $match) { $suggestions[$prefix . $match . ', '] = $match; } // convert to object to prevent drupal bug, see http://drupal.org/node/175361 drupal_json((object)$suggestions); } /** * Batch processing function for rebuilding the index. */ function privatemsg_load_recipients($mid, $recipient, &$context) { // Get type information. $type = privatemsg_recipient_get_type($recipient->type); // First run, initialize sandbox. if (!isset($context['sandbox']['current_offset'])) { $context['sandbox']['current_offset'] = 0; $count_function = $type['count']; $context['sandbox']['count'] = $count_function($recipient); } // Fetch the 10 next recipients. $load_function = $type['generate recipients']; $uids = $load_function($recipient, 10, $context['sandbox']['current_offset']); if (!empty($uids)) { foreach ($uids as $uid) { privatemsg_message_change_recipient($mid, $uid, 'hidden'); } $context['sandbox']['current_offset'] += 10; // Set finished based on sandbox. $context['finished'] = empty($context['sandbox']['count']) ? 1 : ($context['sandbox']['current_offset'] / $context['sandbox']['count']); } else { // If no recipients were returned, mark as finished too. $context['sandbox']['finished'] = 1; } // If we are finished, mark the recipient as read. if ($context['finished'] >= 1) { 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); } }