Skip to content
privatemsg.pages.inc 28.9 KiB
Newer Older
<?php
// $Id$

/**
 * @file
 * User menu callbacks for Privatemsg.
 */

/**
 * List messages.
 *
 * @param $argument
 *   An argument to pass through to the query builder.
 * @param $account_check
 *   Account to check if current user has access.
function privatemsg_list_page($argument = 'list', $account_check = NULL) {
  global $user;

  // Setting default behavior...
  $account = $user;
  // Because uid is submitted by the menu system, it's a string not a integer.
  if (is_object($account_check) && $account_check->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.
  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();
  }

  // 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');

  // 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']);
  }
}


/**
 * 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' => '<div class="privatemsg-view-pager">',
      '#suffix' => '</div>',
      '#weight' => 3,
    );
  }

  // Render the participants.
  $content['participants']['#value'] = theme('privatemsg_recipients', $thread);
  $content['participants']['#weight'] = -5;

  // Render the messages.
  $output = '';
  foreach ($thread['messages'] as $pmid => $message) {
    // Set message as read and theme it.
    if (!empty($message['is_new'])) {
      privatemsg_message_change_status($pmid, PRIVATEMSG_READ, $thread['user']);
    }
    $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_new', $thread['participants'], $thread['subject'], $thread['thread_id'], $thread['read_all']);
    $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);
}


function privatemsg_new(&$form_state, $recipients = array(), $subject = '', $thread_id = NULL, $read_all = FALSE) {
  global $user;

  $recipients_string = '';
  $body      = '';

  // convert recipients to array of user objects
  if (!empty($recipients) && is_string($recipients) || is_int($recipients)) {
    $recipients = _privatemsg_generate_user_array($recipients);
  }
  elseif (is_object($recipients)) {
    $recipients = array($recipients);
  }
  elseif (empty($recipients) && is_string($recipients)) {
    $recipients = array();
  }

  $usercount = 0;
  $to = array();
  $blocked = FALSE;
  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($to[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;
    }

    // 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(privatemsg_recipient_key($recipient) => $recipient), array('thread_id' => $thread_id));
    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[privatemsg_recipient_key($recipient)] = privatemsg_recipient_format($recipient);
      $to_plain[privatemsg_recipient_key($recipient)] = privatemsg_recipient_format($recipient, array('plain' => TRUE));
    }
    else {
      // Recipient list contains blocked users.
      $blocked = TRUE;
    }
  }

  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_' . $user->uid] = privatemsg_recipient_format($user);
    $to_plain['user_' . $user->uid] = privatemsg_recipient_format($user, array('plain' => TRUE));
  }

  if (!empty($to)) {
    $recipients_string = implode(', ', $to);
    $recipients_plain = implode(', ', $to_plain);
  }
  if (isset($form_state['values'])) {
    if (isset($form_state['values']['recipient'])) {
      $recipients_plain = $form_state['values']['recipient'];
    }
    $subject   = $form_state['values']['subject'];
    $body      = $form_state['values']['body'];
  }
  if (!$thread_id && !empty($recipients_plain)) {
    drupal_set_title(t('Write new message to %recipient', array('%recipient' => $recipients_plain)));
  }
  elseif (!$thread_id) {
    drupal_set_title(t('Write new message'));
  }

  $form = array(
    '#type'               => 'fieldset',
    '#access'             => privatemsg_user_access('write privatemsg') || privatemsg_user_access('reply only privatemsg'),
  );
  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'),
    );
    $form['message_header']['message_preview'] = array(
      '#value'  => $form_state['privatemsg_preview'],
    );
  }
    '#type' => 'value',
    '#value' => $user,
  );
  if (is_null($thread_id)) {
    $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));
      '#type'               => 'textfield',
      '#title'              => t('To'),
      '#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.
    );
  }
    '#type'               => 'textfield',
    '#title'              => t('Subject'),
    '#size'               => 50,
    '#maxlength'          => 255,
    '#default_value'      => $subject,
    '#weight'             => -5,
  );
    '#type'               => 'textarea',
    '#title'              => t('Message'),
    '#rows'               => 6,
    '#default_value'      => $body,
    '#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']['#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'),
      '#submit'             => array('privatemsg_new_preview'),
      '#weight'             => 10,
    );
  }
    '#type'               => 'submit',
    '#value'              => t('Send message'),
    '#weight'             => 15,
  );
  $url = privatemsg_get_dynamic_url_prefix();
  $title = t('Cancel');
  if (isset($_REQUEST['destination'])) {
    $url = $_REQUEST['destination'];
  }
  elseif (!is_null($thread_id)) {
    $url = $_GET['q'];
    $title = t('Clear');
  }

  $form['cancel'] = array(
    '#value'  => l($title, $url, array('attributes' => array('id' => 'edit-cancel'))),
    '#weight' => 20,
      '#type' => 'value',
      '#value' => $thread_id,
    );
      '#type' => 'value',
      '#default_value' => $subject,
    );
    $form['reply'] = array(
      '#value' =>  '<h2 class="privatemsg-reply">' . t('Reply') . '</h2>',
      '#weight' => -10,
    );
    if (empty($recipients_string)) {
      // If there are no valid recipients, unset the message reply form.
  // Only set read all if it is a boolean TRUE. It might also be an integer set
  // through the URL.
  );
  return $form;
}

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']);
    // 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[] = '<span class="privatemsg-recipient-alternative">' . $formatted_match . '</span>';
      }
      // Build a formatted list of possible recipients.
      $alternatives = theme('item_list', $alternatives, NULL, 'ul', array('class' => 'action-links'));
      form_set_error('recipient', '<div class="privatemsg-alternative-description">' . t('The site has multiple recipients named %string. Please choose your intended recipient: !list', array('%string' => $string, '!list' => $alternatives)) . '</div>');
    }

    // 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']);
  $recipient_names = array();
  foreach ($form_state['validate_built_message']['recipients'] as $recipient) {
    $recipient_names[] = privatemsg_recipient_format($recipient);
    _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', '<new-message>');
      if ($redirect == '<new-message>') {
        // 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 <em>may have failed</em> 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['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) {
    '#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() {
  $form = array(
      '#type'        => 'fieldset',
      '#title'       => t('Actions'),
      '#prefix'      => '<div class="container-inline">',
      '#suffix'      => '</div>',
      '#collapsible' => TRUE,
      '#collapsed'   => FALSE,
      '#weight'      => 15,
  );
  if (privatemsg_user_access('delete privatemsg')) {
    $form['delete'] = array(
        '#type'   => 'submit',
        '#value'  => t('Delete'),
    );
  }
  // Display all operations which have a label.
  $options = array(0 => t('More actions...'));
  foreach (module_invoke_all('privatemsg_thread_operations') as $operation => $array) {
    if (isset($array['label'])) {
      $options[$operation] = $array['label'];
    }
  }
  $form['operation'] = array(
      '#type'          => 'select',
      '#options'       => $options,
      '#default_value' => 0,
  );
  $form['submit'] = array(
      '#prefix'     => '<div class="privatemsg-op-button">',
      '#suffix'    => '</div>',
      '#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;
}


/**
 * Returns a table header definition based on the submitted keys.
 *
 * Uses @link theming theme patterns @endlink to theme single headers.
 *
 * @param $has_posts
 *   TRUE when there is at least one row. Decides if the select all checkbox
 *   should be displayed.
 * @param $keys
 *   Array with the keys which are present in the query/should be displayed.
 * @return
 *   Array with header defintions for tablesort_sql and theme('table').
 */
function _privatemsg_list_headers($has_posts, $keys) {
  $select_header = $has_posts ? theme('table_select_header_cell') : '';
  $select_header['#weight'] = -50;

  // theme() doesn't include the theme file for patterns, we need to do it manually.
  include_once drupal_get_path('module', 'privatemsg') .'/privatemsg.theme.inc';

  $header = array($select_header);
  foreach ($keys as $key) {
    // First, try to load a specific theme for that header, if not present, use the default.
    if ($return = theme(array('privatemsg_list_header__'. $key, 'privatemsg_list_header'))) {
      // The default theme returns nothing, only store the value if we have something.
      $header[$key] = $return;
    }
  }
  if (count($header) == 1) {
    // No header definition returned, fallback to the default.
    $header += _privatemsg_list_headers_fallback($keys);
  }
  return $header;
}

/**
 * Table header definition for themes that don't support theme patterns.
 *
 * @return
 *   Array with the correct headers.
 */
function _privatemsg_list_headers_fallback($keys) {
  $header = array();
  foreach ($keys as $key) {
    $theme_function = 'phptemplate_privatemsg_list_header__' . $key;
    if (function_exists($theme_function)) {
      $header[$key] = $theme_function();
    }
  }

  return $header;
}

/**
 * Formats a row in the message list.
 *
 * Uses @link theming theme patterns @endlink to theme single fields.
 *
 * @param $thread
 *   Array with the row data returned by the database.
 * @return
 *   Row definition for use with theme('table')
 */
function _privatemsg_list_thread($thread) {
  $row = array('data' => array());

  if (!empty($thread['is_new'])) {
    // Set the css class in the tr tag.
    $row['class'] = 'privatemsg-unread';
  }
  foreach ($thread as $key => $data) {
    // First, try to load a specific theme for that field, if not present, use the default.
    if ($return = theme(array('privatemsg_list_field__'. $key, 'privatemsg_list_field'), $thread)) {
      // The default theme returns nothing, only store the value if we have something.
      $row['data'][$key] = $return;
    }
  }
  if (empty($row['data'])) {
    $row['data'] = _privatemsg_list_thread_fallback($thread);
  }
  return $row;
}

/**
 * Table row definition for themes that don't support theme patterns.
 *
 * @return
 *   Array with row data.
 */
function _privatemsg_list_thread_fallback($thread) {
  $row_data = array();
  foreach ($thread as $key => $data) {
    $theme_function = 'phptemplate_privatemsg_list_field__' . $key;
    if (function_exists($theme_function)) {
      $row_data[$key] = $theme_function($thread);
    }
  }

  return $row_data;
}

/**
 * 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.
 */
  $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;
        }
      }
  // 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();
  // 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);
  }