privatemsg.module 116 KB
Newer Older
litwol's avatar
litwol committed
1 2 3 4 5 6
<?php

/**
 * @file
 * Allows users to send private messages to other users.
 */
7

8 9 10 11 12 13 14 15
/**
 * Status constant for read messages.
 */
define('PRIVATEMSG_READ', 0);
/**
 * Status constant for unread messages.
 */
define('PRIVATEMSG_UNREAD', 1);
16 17 18 19
/**
 * Show unlimited messages in a thread.
 */
define('PRIVATEMSG_UNLIMITED', 'unlimited');
marco's avatar
marco committed
20

21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
/**
 * Implements hook_help()
 */
function privatemsg_help($path, $arg) {
  $output = '';
  switch ($path) {
    case 'admin/help#privatemsg':
      $output .= '';
      $output .= '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('The Privatemsg module is designed to be a flexible and powerful system for sending and receiving internal messages. This includes user-to-user messages, user-to-role messages, messages from the site administrator, and much more. If you want some or all users on your site to have their own "mailbox"--and other users with the proper permissions to be able to message them through this mailbox--then this is the module for you.') . '</p>';
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<p>' . t('One of the strengths of Privatemsg is that it has a broad feature set and a modular architecture. The core Private Message module includes features such as threaded conversations (making it easier to keep track of messages and replies), search capability, new message alerts (via Drupal messages and blocks), and message tokens (similar to a mail merge).') . '</p>';
      $output .= '<dl>';
      $output .= '<dt>' . t('<h6>Configuration Steps</h6>') . '</dt>';
      $output .= '<dd>' . t('1. Go to People > Permissions (admin/people/permissions) and find the relevant module permissions underneath the "Private messages" section. If you are not logged in as user #1, you must give at least one role (probably the administrator role) the "Administer privatemsg" permission to configure this module.') . '</dd>';
      $output .= '<dd>' . t('2. On this same Permissions page, give at least one role the "Read private messages" permission and the "Write new private messages" permission.  This will allow members of that role to read and write private messages.') . '</dd>';
      $output .= '<dd>' . t('3. Go to Configuration > Private messages (admin/config/messaging/privatemsg) and configure the module settings per your requirements. If you have various sub-modules enabled, their settings pages may appear as tabs on this page. ') . '</dd>';
      $output .= '<dd>' . t('4. Login as a user with the role we specified in Step #2. Visit /messages to see the user&apos;s mailbox. Visit /messages/new to write a new message.') . '</dd>';
      $output .= '</dl>';
  }
  return $output;
}

Zen's avatar
Zen committed
44
/**
45
 * Implements hook_permission().
Zen's avatar
Zen committed
46
 */
47
function privatemsg_permission() {
litwol's avatar
litwol committed
48
  return array(
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
    'administer privatemsg settings' => array(
      'title' => t('Administer privatemsg'),
      'description' => t('Perform maintenance tasks for privatemsg'),
    ),
    'read privatemsg' => array(
      'title' => t('Read private messages'),
      'description' => t('Read private messages'),
    ),
    'read all private messages' => array(
      'title' => t('Read all private messages'),
      'description' => t('Includes messages of other users'),
    ),
    'write privatemsg' => array(
      'title' => t('Write new private messages'),
      'description' => t('Write new private messages'),
    ),
65 66
    'delete privatemsg' => array(
      'title' => t('Delete private messages'),
67
      'description' => t('Allows users to delete messages they can read'),
68
    ),
69
    'allow disabling privatemsg' => array(
70
      'title' => t('Allow disabling private messages'),
71
      'description' => t("Allows user to disable privatemsg so that they can't receive or send any private messages.")
72 73 74 75
    ),
    'reply only privatemsg' => array(
      'title' => t('Reply to private messages'),
      'description' => t('Allows to reply to private messages but not send new ones. Note that the write new private messages permission includes replies.')
76
    ),
77 78 79 80
    'use tokens in privatemsg' => array(
      'title' => t('Use tokens in private messages'),
      'description' => t("Allows user to use available tokens when sending private messages.")
    ),
81 82 83 84
    'select text format for privatemsg' => array(
      'title' => t('Select text format for private messages'),
      'description' => t('Allows to choose the text format when sending private messages. Otherwise, the default is used.'),
    ),
85 86 87 88
    'report private messages to mollom' => array(
      'title' => t('Reporte private messages to mollom'),
      'description' => t('Allows users to report messages as spam or unwanted content when they delete then, when Mollom is set up to check private messages.'),
    ),
89 90 91
  );
}

92
/**
93
 * Generate array of user objects based on a string.
94 95 96
 *
 *
 * @param $userstring
97
 *   A string with user id, for example 1,2,4. Returned by the list query.
98 99 100 101
 *
 * @return
 *   Array with user objects.
 */
102
function _privatemsg_generate_user_array($string, $slice = NULL) {
103 104 105
  // 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.
106 107
  // This is done to avoid loading user objects that are not displayed, for
  // obvious performance reasons.
108
  $users = explode(',', $string);
109 110 111
  if (!is_null($slice)) {
    $users = array_slice($users, $slice);
  }
112 113
  $participants = array();
  foreach ($users as $uid) {
114
    // If it is an integer, it is a user id.
115
    if ((int)$uid > 0) {
116
    $user_ids = privatemsg_user_load_multiple(array($uid));
117 118 119
      if ($account = array_shift($user_ids)) {
        $participants[privatemsg_recipient_key($account)] = $account;
      }
120
    }
121
    elseif (strpos($uid, '_') !== FALSE) {
122 123 124
      list($type, $id) = explode('_', $uid);
      $type_info = privatemsg_recipient_get_type($type);
      if ($type_info && isset($type_info['load']) && is_callable($type_info['load'])) {
125
        $temp_load = $type_info['load'](array($id), $type);
126
        if ($participant = array_shift($temp_load)) {
127 128 129 130
          $participants[privatemsg_recipient_key($participant)] = $participant;
        }
      }
    }
131 132
  }
  return $participants;
133 134
}

135 136 137 138
/**
 * Format an array of user objects.
 *
 * @param $part_array
139
 *   Array with user objects, for example the one returned by
140 141 142 143 144 145 146
 *   _privatemsg_generate_user_array.
 *
 * @param $limit
 *   Limit the number of user objects which should be displayed.
 * @param $no_text
 *   When TRUE, don't display the Participants/From text.
 * @return
147
 *   String with formatted user objects, like user1, user2.
148
 */
149
function _privatemsg_format_participants($part_array, $limit = NULL, $no_text = FALSE) {
150
  global $user;
151
  if (count($part_array) > 0) {
152
    $to = array();
153 154
    $limited = FALSE;
    foreach ($part_array as $account) {
155 156 157

      // Directly address the current user.
      if (isset($account->type) && in_array($account->type, array('hidden', 'user')) && $account->recipient == $user->uid) {
158
        array_unshift($to, $no_text ? t('You', array(), array('context' => 'Dative')) : t('you', array(), array('context' => 'Dative')));
159 160 161
        continue;
      }

162 163 164 165
      // Don't display recipients with type hidden.
      if (isset($account->type) && $account->type == 'hidden') {
        continue;
      }
166
      if (is_int($limit) && count($to) >= $limit) {
167
        $limited = TRUE;
168 169
        break;
      }
170
      $to[] = privatemsg_recipient_format($account);
171 172 173 174 175 176 177 178
    }

    $limit_string = '';
    if ($limited) {
      $limit_string = t(' and others');
    }


179
    if ($no_text) {
180 181 182 183 184 185 186
      return implode(', ', $to) . $limit_string;
    }

    $last = array_pop($to);
    if (count($to) == 0) { // Only one participant
      return t("From !last", array('!last' => $last));
    }
187
    else { // Multiple participants..
188
      $participants = implode(', ', $to);
189
      return t('Between !participants and !last', array('!participants' => $participants, '!last' => $last));
190 191 192 193 194
    }
  }
  return '';
}

195
/**
196
 * Implements hook_menu().
197
 */
litwol's avatar
litwol committed
198
function privatemsg_menu() {
litwol's avatar
litwol committed
199 200
  $items['messages'] = array(
    'title'            => 'Messages',
201
    'title callback'   => 'privatemsg_title_callback',
202 203
    'page callback'    => 'privatemsg_list_page',
    'page arguments'   => array('list'),
204
    'file'             => 'privatemsg.pages.inc',
205
    'access callback'  => 'privatemsg_user_access',
litwol's avatar
litwol committed
206
    'type'             => MENU_NORMAL_ITEM,
207
    'menu_name'        => 'user-menu',
litwol's avatar
litwol committed
208
  );
209 210
  $items['messages/list'] = array(
    'title'            => 'Messages',
211 212
    'page callback'    => 'privatemsg_list_page',
    'page arguments'   => array('list'),
213
    'file'             => 'privatemsg.pages.inc',
214
    'access callback'  => 'privatemsg_user_access',
litwol's avatar
litwol committed
215 216
    'type'             => MENU_DEFAULT_LOCAL_TASK,
    'weight'           => -10,
217
    'menu_name'        => 'user-menu',
litwol's avatar
litwol committed
218
  );
219
  $items['messages/view/%privatemsg_thread'] = array(
220 221 222
    // Set the third argument to TRUE so that we can show access denied instead
    // of not found.
    'load arguments'   => array(NULL, NULL, TRUE),
223
    'title'            => 'Read message',
litwol's avatar
litwol committed
224 225
    'page callback'    => 'privatemsg_view',
    'page arguments'   => array(2),
226
    'file'             => 'privatemsg.pages.inc',
227
    'access callback'  => 'privatemsg_view_access',
228
    'access arguments' => array(2),
229
    'type'             => MENU_LOCAL_TASK,
230
    'weight'           => -5,
231
    'menu_name'        => 'user-menu',
litwol's avatar
litwol committed
232
  );
233
  $items['messages/delete/%privatemsg_thread/%privatemsg_message'] = array(
litwol's avatar
litwol committed
234 235
    'title'            => 'Delete message',
    'page callback'    => 'drupal_get_form',
236
    'page arguments'   => array('privatemsg_delete', 2, 3),
237
    'file'             => 'privatemsg.pages.inc',
238
    'access callback'  => 'privatemsg_user_access',
239
    'access arguments' => array('delete privatemsg'),
litwol's avatar
litwol committed
240 241
    'type'             => MENU_CALLBACK,
    'weight'           => -10,
242
    'menu_name'        => 'user-menu',
litwol's avatar
litwol committed
243 244 245 246
  );
  $items['messages/new'] = array(
    'title'            => 'Write new message',
    'page callback'    => 'drupal_get_form',
247
    'page arguments'   => array('privatemsg_new', 2, 3, NULL),
248
    'file'             => 'privatemsg.pages.inc',
249
    'access callback'  => 'privatemsg_user_access',
litwol's avatar
litwol committed
250
    'access arguments' => array('write privatemsg'),
251
    'type'             => MENU_LOCAL_ACTION,
252
    'weight'           => -3,
253
    'menu_name'        => 'user-menu',
litwol's avatar
litwol committed
254 255
  );
  // Auto-completes available user names & removes duplicates.
256 257
  $items['messages/autocomplete'] = array(
    'page callback'    => 'privatemsg_autocomplete',
258
    'file'             => 'privatemsg.pages.inc',
259
    'access callback'  => 'privatemsg_user_access',
litwol's avatar
litwol committed
260
    'access arguments' => array('write privatemsg'),
litwol's avatar
litwol committed
261 262
    'type'             => MENU_CALLBACK,
  );
263 264 265 266 267 268 269 270 271
  $items['admin/config/messaging'] = array(
    'title' => 'Messaging',
    'description' => 'Messaging systems.',
    'page callback' => 'system_admin_menu_block_page',
    'access arguments' => array('access administration pages'),
    'file' => 'system.admin.inc',
    'file path' => drupal_get_path('module', 'system'),
  );
  $items['admin/config/messaging/privatemsg'] = array(
272
    'title'            => 'Private message settings',
litwol's avatar
litwol committed
273 274
    'description'      => 'Configure private messaging settings.',
    'page callback'    => 'drupal_get_form',
275 276
    'page arguments'   => array('privatemsg_admin_settings'),
    'file'             => 'privatemsg.admin.inc',
litwol's avatar
litwol committed
277 278
    'access arguments' => array('administer privatemsg settings'),
    'type'             => MENU_NORMAL_ITEM,
279
  );
280 281
  $items['admin/config/messaging/privatemsg/settings'] = array(
    'title'            => 'Private message settings',
282 283
    'description'      => 'Configure private messaging settings.',
    'page callback'    => 'drupal_get_form',
284 285
    'page arguments'   => array('privatemsg_admin_settings'),
    'file'             => 'privatemsg.admin.inc',
286 287 288 289
    'access arguments' => array('administer privatemsg settings'),
    'type'             => MENU_DEFAULT_LOCAL_TASK,
    'weight'           => -10,
  );
290 291 292 293 294 295 296 297 298 299
  if (module_exists('devel_generate')) {
    $items['admin/config/development/generate/privatemsg'] = array(
      'title' => 'Generate private messages',
      'description' => 'Generate a given number of private messages. Optionally delete current private messages.',
      'page callback' => 'drupal_get_form',
      'page arguments' => array('privatemsg_devel_generate_form'),
      'access arguments' => array('administer privatemsg settings'),
      'file' => 'privatemsg.devel_generate.inc',
    );
  }
300 301 302 303
  $items['messages/undo/action'] = array(
    'title'            => 'Private messages',
    'description'      => 'Undo last thread action',
    'page callback'    => 'privatemsg_undo_action',
304
    'file'             => 'privatemsg.pages.inc',
305 306
    'access arguments' => array('read privatemsg'),
    'type'             => MENU_CALLBACK,
307
    'menu'             => 'user-menu',
308
  );
309 310
  $items['user/%/messages'] = array(
    'title' => 'Messages',
311 312
    'page callback'    => 'privatemsg_list_page',
    'page arguments'   => array('list', 1),
313
    'file'             => 'privatemsg.pages.inc',
314 315
    'access callback'  => 'privatemsg_user_access',
    'access arguments' => array('read all private messages'),
316 317
    'type' => MENU_LOCAL_TASK,
  );
litwol's avatar
litwol committed
318
  return $items;
319
}
320

321 322 323 324 325
/**
 * Implements hook_menu_local_tasks_alter().
 */
function privatemsg_menu_local_tasks_alter(&$data, $router_item, $root_path) {
  // Add action link to 'messages/new' on 'messages' page.
326
  $add_to_array = array('messages/list', 'messages/inbox', 'messages/sent');
327 328 329 330 331 332 333 334 335 336 337 338 339 340
  foreach ($add_to_array as $add_to) {
    if (strpos($root_path, $add_to) !== FALSE) {
    $item = menu_get_item('messages/new');
    if ($item['access']) {
      $data['actions']['output'][] = array(
        '#theme' => 'menu_local_action',
        '#link' => $item,
      );
    }
    break;
    }
  }
}

341
/**
342 343 344
 * Privatemsg  wrapper for user_access.
 *
 * Never allows anonymous user access as that doesn't makes sense.
345
 *
346 347
 * @param $permission
 *   Permission string, defaults to read privatemsg
348
 *
349 350
 * @return
 *   TRUE if user has access, FALSE if not
351
 *
352
 * @ingroup api
353 354
 */
function privatemsg_user_access($permission = 'read privatemsg', $account = NULL) {
355
  static $disabled_displayed = FALSE;
356 357 358 359 360 361 362
  if ( $account === NULL ) {
    global $user;
    $account = $user;
  }
  if (!$account->uid) { // Disallow anonymous access, regardless of permissions
    return FALSE;
  }
363
  if (privatemsg_is_disabled($account) && ($permission == 'write privatemsg') ) {
364 365 366 367
    if (arg(0) == 'messages' && variable_get('privatemsg_display_disabled_message', TRUE) && !$disabled_displayed) {
      $disabled_displayed = TRUE;
      drupal_set_message(t('You have disabled Privatemsg and are not allowed to write messages. Go to your <a href="@settings_url">Account settings</a> to enable it again.', array('@settings_url' => url('user/' . $account->uid . '/edit'))), 'warning');
    }
368 369
    return FALSE;
  }
370 371 372 373 374 375 376
  if (!user_access($permission, $account)) {
    return FALSE;
  }
  return TRUE;
}

/**
377 378 379 380 381 382
 * Check access to the view messages page.
 *
 * Function to restrict the access of the view messages page to just the
 * messages/view/% pages and not to leave tabs artifact on other lower
 * level pages such as the messages/new/%.
 *
383 384 385 386
 * @param $thread
 *   A array containing all information about a specific thread, generated by
 *   privatemsg_thread_load().
 *
387
 * @ingroup api
388
 */
389 390 391 392 393 394
function privatemsg_view_access($thread) {
  // Do not allow access to threads without messages.
  if (empty($thread['messages'])) {
    // Count all messages, if there
    return FALSE;
  }
395 396 397 398 399 400
  if (privatemsg_user_access('read privatemsg') && arg(1) == 'view') {
    return TRUE;
  }
  return FALSE;
}

401 402 403 404 405 406 407
/**
 * Checks the status of private messaging for provided user.
 *
 * @param user object to check
 * @return TRUE if user has disabled private messaging, FALSE otherwise
 */
function privatemsg_is_disabled($account) {
408
  if (!$account || !isset($account->uid) || !$account->uid) {
409 410 411
    return FALSE;
  }

412 413 414 415 416 417 418
  // Make sure we have a fully loaded user object and try to load it if not.
  if ((!empty($account->roles) || $account = user_load($account->uid)) && user_access('allow disabling privatemsg', $account)) {
    $ids = privatemsg_get_default_setting_ids($account);
    return (bool)privatemsg_get_setting('disabled', $ids);
  }
  else {
    return FALSE;
419 420 421
  }
}

422
/**
423 424 425 426
 * Load a thread with all the messages and participants.
 *
 * This function is called by the menu system through the %privatemsg_thread
 * wildcard.
427
 *
428 429
 * @param $thread_id
 *   Thread id, pmi.thread_id or pm.mid of the first message in that thread.
430 431 432
 * @param $account
 *   User object for which the thread should be loaded, defaults to
 *   the current user.
433 434
 * @param $start
 *   Message offset from the start of the thread.
435 436 437 438
 * @param $useAccessDenied
 *   Set to TRUE if the function should forward to the access denied page
 *   instead of not found. This is used by the menu system because that does
 *   load arguments before access checks are made. Defaults to FALSE.
439
 *
440 441 442 443 444 445 446 447
 * @return
 *   $thread object, with keys messages, participants, title and user. messages
 *   contains an array of messages, participants an array of user, subject the
 *   subject of the thread and user the user viewing the thread.
 *
 *   If no messages are found, or the thread_id is invalid, the function returns
 *   FALSE.

448
 * @ingroup api
449
 */
450
function privatemsg_thread_load($thread_id, $account = NULL, $start = NULL, $useAccessDenied = FALSE) {
451
  $threads = &drupal_static(__FUNCTION__, array());
452 453
  $thread_id = (int)$thread_id;
  if ($thread_id > 0) {
454
    $thread = array('thread_id' => $thread_id);
455 456 457

    if (is_null($account)) {
      global $user;
458
      $account = clone $user;
459 460
    }

461 462 463
    if (!isset($threads[$account->uid])) {
      $threads[$account->uid] = array();
    }
464

465
    if (!array_key_exists($thread_id, $threads[$account->uid])) {
466
      // Load the list of participants.
467
      $thread['participants'] = _privatemsg_load_thread_participants($thread_id, $account, FALSE, 'view');
468
      $thread['read_all'] = FALSE;
469
      if (empty($thread['participants']) && privatemsg_user_access('read all private messages', $account)) {
470
        $thread['read_all'] = TRUE;
471 472
        // Load all participants.
        $thread['participants'] = _privatemsg_load_thread_participants($thread_id, FALSE, FALSE, 'view');
473 474
      }

475
      // Load messages returned by the messages query with privatemsg_message_load_multiple().
476
      $query = _privatemsg_assemble_query('messages', array($thread_id), $thread['read_all'] ? NULL : $account);
477 478 479 480
      // Use subquery to bypass group by since it is not possible to alter
      // existing GROUP BY statements.
      $countQuery = db_select($query);
      $countQuery->addExpression('COUNT(*)');
481 482 483 484
      $thread['message_count'] = $thread['to'] = $countQuery->execute()->fetchField();
      $thread['from'] = 1;
      // Check if we need to limit the messages.
      $max_amount = variable_get('privatemsg_view_max_amount', 20);
485 486 487 488 489 490

      // If there is no start value, select based on get params.
      if (is_null($start)) {
        if (isset($_GET['start']) && $_GET['start'] < $thread['message_count']) {
          $start = $_GET['start'];
        }
491 492 493
        elseif (!variable_get('privatemsg_view_use_max_as_default', FALSE) && $max_amount == PRIVATEMSG_UNLIMITED) {
          $start = PRIVATEMSG_UNLIMITED;
        }
494
        else {
495
          $start = $thread['message_count'] - (variable_get('privatemsg_view_use_max_as_default', FALSE) ? variable_get('privatemsg_view_default_amount', 10) : $max_amount);
496 497 498 499
        }
      }

      if ($start != PRIVATEMSG_UNLIMITED) {
500
        if ($max_amount == PRIVATEMSG_UNLIMITED) {
501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521
          $last_page = 0;
          $max_amount = $thread['message_count'];
        }
        else {
          // Calculate the number of messages on the "last" page to avoid
          // message overlap.
          // Note - the last page lists the earliest messages, not the latest.
          $paging_count = variable_get('privatemsg_view_use_max_as_default', FALSE) ? $thread['message_count'] - variable_get('privatemsg_view_default_amount', 10) : $thread['message_count'];
          $last_page = $paging_count % $max_amount;
        }

        // Sanity check - we cannot start from a negative number.
        if ($start < 0) {
          $start = 0;
        }
        $thread['start'] = $start;

        //If there are newer messages on the page, show pager link allowing to go to the newer messages.
        if (($start + $max_amount + 1) < $thread['message_count']) {
          $thread['to'] = $start + $max_amount;
          $thread['newer_start'] = $start + $max_amount;
522
        }
523 524
        if ($start - $max_amount >= 0) {
          $thread['older_start'] = $start - $max_amount;
525
        }
526 527 528 529 530 531
        elseif ($start > 0) {
          $thread['older_start'] = 0;
        }

        // Do not show messages on the last page that would show on the page
        // before. This will only work when using the visual pager.
532
        if ($start < $last_page && $max_amount != PRIVATEMSG_UNLIMITED && $max_amount < $thread['message_count']) {
533 534 535 536 537
          unset($thread['older_start']);
          $thread['to'] = $thread['newer_start'] = $max_amount = $last_page;
          // Start from the first message - this is a specific hack to make sure
          // the message display has sane paging on the last page.
          $start = 0;
538
        }
539 540 541
        // Visual counts start from 1 instead of zero, so plus one.
        $thread['from'] = $start + 1;
        $query->range($start, $max_amount);
542
      }
543 544 545 546
      $conditions = array();
      if (!$thread['read_all']) {
        $conditions['account'] = $account;
      }
547 548 549 550 551 552

      // #2033161 privatemsg_message_load_multiple will load all threads if empty
      $ids = $query->execute()->fetchCol();
      if (count($ids)) {
        $thread['messages'] = privatemsg_message_load_multiple($ids, $conditions);
      }
553 554

      // If there are no messages, don't allow access to the thread.
555
      if (empty($thread['messages'])) {
556 557 558 559 560 561 562 563 564 565 566 567
        if ($useAccessDenied) {
          // Generate new query with read all to see if the thread does exist.
          $query = _privatemsg_assemble_query('messages', array($thread_id), NULL);
          $exists = $query->countQuery()->execute()->fetchField();
          if (!$exists) {
            // Thread does not exist, display 404.
            $thread = FALSE;
          }
        }
        else {
          $thread = FALSE;
        }
568 569
      }
      else {
570
        // General data, assume subject is the same for all messages of that thread.
571 572
        $thread['user'] = $account;
        $message = current($thread['messages']);
573
        $thread['subject'] = $thread['subject-tokenized'] = $message->subject;
574
        if ($message->has_tokens) {
575
          $thread['subject-tokenized'] = privatemsg_token_replace($thread['subject'], array('privatemsg_message' => $message), array('sanitize' => TRUE, 'privatemsg-show-span' => FALSE));
576
        }
577 578
      }
      $threads[$account->uid][$thread_id] = $thread;
579
    }
580
    return $threads[$account->uid][$thread_id];
581 582 583
  }
  return FALSE;
}
litwol's avatar
litwol committed
584

585
/**
586
 * Implements hook_privatemsg_view_template().
litwol's avatar
litwol committed
587 588 589 590
 *
 * Allows modules to define different message view template.
 *
 * This hook returns information about available themes for privatemsg viewing.
litwol's avatar
litwol committed
591 592 593 594 595 596 597 598
 *
 * array(
 *  'machine_template_name' => 'Human readable template name',
 *  'machine_template_name_2' => 'Human readable template name 2'
 * };
 */
function privatemsg_privatemsg_view_template() {
  return array(
litwol's avatar
litwol committed
599
    'privatemsg-view' => 'Default view',
600
  );
litwol's avatar
litwol committed
601
}
litwol's avatar
litwol committed
602

603
/**
604
 * Implements hook_cron().
605 606 607 608 609 610 611 612 613
 *
 * If the flush feature is enabled, a given amount of deleted messages that are
 * old enough are flushed.
 */
function privatemsg_cron() {
  if (variable_get('privatemsg_flush_enabled', FALSE)) {
    $query = _privatemsg_assemble_query('deleted', variable_get('privatemsg_flush_days', 30), variable_get('privatemsg_flush_max', 200));

    foreach ($query->execute()->fetchCol() as $mid) {
614
      $message = privatemsg_message_load($mid);
615 616 617 618 619 620 621 622 623 624 625 626
      module_invoke_all('privatemsg_message_flush', $message);

      // Delete recipients of the message.
      db_delete('pm_index')
        ->condition('mid', $mid)
        ->execute();
      // Delete message itself.
      db_delete('pm_message')
        ->condition('mid', $mid)
        ->execute();
    }
  }
627 628

  // Number of user ids to process for this cron run.
629
  $total_remaining = variable_get('privatemsg_cron_recipient_per_run', 1000);
630
  $current_process = variable_get('privatemsg_cron_recipient_process', array());
631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649

  // Instead of doing the order by in the database, which can be slow, we load
  // all results and the do the handling there. Additionally, explicitly specify
  // the desired types. If there are more than a few dozen results the site is
  // unhealthy anyway because this cron is unable to keep up with the
  // unprocessed recipients.
  $rows = array();

  // Get all type keys except user.
  $types = privatemsg_recipient_get_types();
  unset($types['user']);
  $types = array_keys($types);

  // If there are no other recipient types, there is nothing to do.
  if (empty($types)) {
    return;
  }

  $result = db_query("SELECT pmi.recipient, pmi.type, pmi.mid FROM {pm_index} pmi WHERE pmi.type IN (:types) AND pmi.is_new = 1", array(':types' => $types));
650
  foreach ($result as $row) {
651 652 653 654 655 656 657
    // If this is equal to the row that is currently processed, add it first in
    // the array.
    if (!empty($current_process) && $current_process['mid'] == $row->mid && $current_process['type'] == $row->type && $current_process['recipient'] == $row->recipient) {
      array_unshift($rows, $row);
    }
    else {
      $rows[] = $row;
658
    }
659 660 661 662
  }

  foreach ($rows as $row) {
    $type = privatemsg_recipient_get_type($row->type);
663
    if (isset($type['load']) && is_callable($type['load'])) {
664
      $loaded = $type['load'](array($row->recipient), $row->type);
665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706
      if (empty($loaded)) {
        continue;
      }
      $recipient = reset($loaded);
    }

    // Check if we already started to process this recipient.
    $offset = 0;
    if (!empty($current_process) && $current_process['mid'] == $row->mid && $current_process['recipient'] == $row->recipient && $current_process['type'] == $row->type) {
      $offset = $current_process['offset'];
    }

    $load_function = $type['generate recipients'];
    $uids = $load_function($recipient, $total_remaining, $offset);
    if (!empty($uids)) {
      foreach ($uids as $uid) {
        privatemsg_message_change_recipient($row->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_update('pm_index')
        ->fields(array('is_new' => PRIVATEMSG_READ))
        ->condition('mid', $row->mid)
        ->condition('recipient', $row->recipient)
        ->condition('type', $row->type)
        ->execute();
      // 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.
      $existing_offset = isset($current_process['offset']) ? $current_process['offset'] : 0;
      $current_process = (array)$row;
      $current_process['offset'] = $existing_offset + count($uids);
      variable_set('privatemsg_cron_recipient_process', $current_process);
      break;
    }
  }
707 708
}

litwol's avatar
litwol committed
709
function privatemsg_theme() {
710
  $templates = array(
litwol's avatar
litwol committed
711
    'privatemsg_view'    => array(
712
      'variables'        => array('message' => NULL),
713
      'template'         => variable_get('private_message_view_template', 'privatemsg-view'), // 'privatemsg',
litwol's avatar
litwol committed
714 715
    ),
    'privatemsg_from'    => array(
716
      'variables'        => array('author' => NULL),
litwol's avatar
litwol committed
717
      'template'         => 'privatemsg-from',
litwol's avatar
litwol committed
718
    ),
719
    'privatemsg_recipients' => array(
720
      'variables'        => array('message' => NULL),
litwol's avatar
litwol committed
721
      'template'         => 'privatemsg-recipients',
litwol's avatar
litwol committed
722
    ),
litwol's avatar
litwol committed
723
    'privatemsg_between' => array(
724
      'variables'        => array('recipients' => NULL),
litwol's avatar
litwol committed
725 726
      'template'         => 'privatemsg-between',
    ),
727
    // Define pattern for field templates. The theme system will register all
728 729 730 731 732
    // theme functions that start with the defined pattern.
    'privatemsg_list_field'   => array(
      'file'                  => 'privatemsg.theme.inc',
      'path'                  => drupal_get_path('module', 'privatemsg'),
      'pattern'               => 'privatemsg_list_field__',
733
      'variables'             => array('thread' => array()),
litwol's avatar
litwol committed
734
    ),
735
    'privatemsg_new_block'  => array(
736 737
      'file'                  => 'privatemsg.theme.inc',
      'path'                  => drupal_get_path('module', 'privatemsg'),
738
      'variables'             => array('count'),
739
    ),
740 741 742 743 744
    'privatemsg_username'  => array(
      'file'                  => 'privatemsg.theme.inc',
      'path'                  => drupal_get_path('module', 'privatemsg'),
      'variables'             => array('recipient' => NULL, 'options' => array()),
    ),
745 746 747 748 749 750
    // Admin settings theme callbacks.
    'privatemsg_admin_settings_display_fields' => array(
      'file'                  => 'privatemsg.theme.inc',
      'path'                  => drupal_get_path('module', 'privatemsg'),
      'render element'        => 'element',
    ),
751
  );
752 753 754 755
  // Include the theme file to load the theme suggestions.
  module_load_include('inc', 'privatemsg', 'privatemsg.theme');
  $templates += drupal_find_theme_functions($templates, array('theme'));
  return $templates;
756
}
litwol's avatar
litwol committed
757

758 759 760
/**
 * Implements hook_preprocess_THEME().
 */
761
function template_preprocess_privatemsg_view(&$vars) {
762
  global $user;
litwol's avatar
litwol committed
763

764
  $message = $vars['message'];
765
  $vars['mid'] = isset($message->mid) ? $message->mid : NULL;
Berdir's avatar
Berdir committed
766
  $vars['message_classes'] = isset($message->classes) ? $message->classes : array();
767 768
  $vars['thread_id'] = isset($message->thread_id) ? $message->thread_id : NULL;
  $vars['author_picture'] = theme('user_picture', array('account' => $message->author));
769
  // Directly address the current user if he is the author.
770
  if ($user->uid == $message->author->uid) {
771 772 773 774 775
    $vars['author_name_link'] = t('You');
  }
  else {
    $vars['author_name_link'] = privatemsg_recipient_format($message->author);
  }
776
  $vars['message_timestamp'] = privatemsg_format_date($message->timestamp);
777 778 779 780 781 782 783 784 785

  $message->content = array(
    '#view_mode' => 'message',
    'body' => array(
      '#markup' => check_markup($message->body, $message->format),
      '#weight' => -4,
    ),
  );

786 787 788 789 790 791
  if ($message->has_tokens) {
    // Replace tokens including option to add a notice if the user is not a
    // recipient.
    $message->content['body']['#markup'] = privatemsg_token_replace($message->content['body']['#markup'], array('privatemsg_message' => $message), array('privatemsg-token-notice' => TRUE, 'sanitize' => TRUE));
  }

792 793
  // Build fields content.
  field_attach_prepare_view('privatemsg_message', array($vars['mid'] => $message), 'message');
794
  $message->content += field_attach_view('privatemsg_message', $message, 'message');
795

796 797
  // Render message body.
  $vars['message_body'] =  drupal_render($message->content);
798
  if (isset($vars['mid']) && isset($vars['thread_id']) && privatemsg_user_access('delete privatemsg')) {
799
    $vars['message_actions'][] = array('title' => t('Delete'), 'href' => 'messages/delete/' . $vars['thread_id'] . '/' . $vars['mid']);
800 801
  }
  $vars['message_anchors'][] = 'privatemsg-mid-' . $vars['mid'];
802
  if (!empty($message->is_new)) {
803
    $vars['message_anchors'][] = 'new';
804
    $vars['new'] = drupal_ucfirst(t('new'));
litwol's avatar
litwol committed
805
  }
806 807 808

  // call hook_privatemsg_message_view_alter
  drupal_alter('privatemsg_message_view', $vars);
809

810
  $vars['message_actions'] = !empty($vars['message_actions']) ? theme('links', array('links' => $vars['message_actions'], 'attributes' => array('class' => array('privatemsg-message-actions', 'links', 'inline')))) : '';
811

812 813
  $vars['anchors'] = '';
  foreach ($vars['message_anchors'] as $anchor) {
814
    $vars['anchors'] .= '<a name="' . $anchor . '"></a>';
815
  }
816
}
litwol's avatar
litwol committed
817

818
function template_preprocess_privatemsg_recipients(&$vars) {
819
  $vars['participants'] = ''; // assign a default empty value
820 821
  if (isset($vars['thread']['participants'])) {
    $vars['participants'] = _privatemsg_format_participants($vars['thread']['participants']);
822 823
  }
}
824

litwol's avatar
litwol committed
825
/**
826
 * Changes the read/new status of a single message.
litwol's avatar
litwol committed
827
 *
828 829 830 831 832 833
 * @param $pmid
 *   Message id
 * @param $status
 *   Either PRIVATEMSG_READ or PRIVATEMSG_UNREAD
 * @param $account
 *   User object, defaults to the current user
litwol's avatar
litwol committed
834
 */
835 836 837 838
function privatemsg_message_change_status($pmid, $status, $account = NULL) {
  if (!$account) {
    global $user;
    $account = $user;
litwol's avatar
litwol committed
839
  }
840 841 842
  db_update('pm_index')
    ->fields(array('is_new' => $status))
    ->condition('mid', $pmid)
843 844
    ->condition('recipient', $account->uid)
    ->condition('type', array('hidden', 'user'))
845
    ->execute();
846 847 848

  // Allows modules to respond to the status change.
  module_invoke_all('privatemsg_message_status_changed', $pmid, $status, $account);
litwol's avatar
litwol committed
849 850 851 852
}

/**
 * Return number of unread messages for an account.
853 854
 *
 * @param $account
855
 *   Specify the user for which the unread count should be loaded.
856 857
 *
 * @ingroup api
litwol's avatar
litwol committed
858 859
 */
function privatemsg_unread_count($account = NULL) {
860
  $counts = &drupal_static(__FUNCTION__, array());
litwol's avatar
litwol committed
861 862
  if (!$account || $account->uid == 0) {
    global $user;
litwol's avatar
litwol committed
863
    $account = $user;
litwol's avatar
litwol committed
864
  }
865 866 867 868
  if (!isset($counts[$account->uid])) {
    $counts[$account->uid] = _privatemsg_assemble_query('unread_count', $account)
      ->execute()
      ->fetchField();
litwol's avatar
litwol committed
869 870 871 872
  }
  return $counts[$account->uid];
}

873 874 875 876
/**
 * Load all participants of a thread.
 *
 * @param $thread_id
877
 *   Thread ID for which the participants should be loaded.
878 879 880 881 882 883 884 885 886
 * @param $account
 *   For which account should the messages be loaded. *
 * @param $ignore_hidden
 *   Ignores hidden participants.
 * @param $access
 *   Which access permission should be checked (write or view).
 *
 * @return
 *   Array with all visible/writable participants for that thread.
887
 */
888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904
function _privatemsg_load_thread_participants($thread_id, $account, $ignore_hidden = TRUE, $access = 'write') {
  $query = _privatemsg_assemble_query('participants', $thread_id, $account);
  $participants = array();
  $to_load = array();
  foreach ($query->execute() as $participant) {
    if ($ignore_hidden && $participant->type == 'hidden') {
      continue;
    }
    elseif (privatemsg_recipient_access($participant->type, $access, $participant)) {
      $to_load[$participant->type][] = $participant->recipient;
    }
  }

  // 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'])) {
905
      $loaded = $type_info['load']($ids, $type);
906 907 908 909 910 911 912 913 914 915 916 917 918
      if (is_array($loaded)) {
        $participants += $loaded;
      }
    }
  }
  if ($access == 'write' && $account) {
    // Remove author if loading participants for writing and when he is not the
    // only recipient.
    if (isset($participants['user_' . $account->uid]) && count($participants) > 1) {
      unset($participants['user_' . $account->uid]);
    }
  }
  return $participants;
919 920
}

921 922 923 924 925 926 927 928
/**
 * Extract the valid usernames of a string and loads them.
 *
 * This function is used to parse a string supplied by a username autocomplete
 * field and load all user objects.
 *
 * @param $string
 *   A string in the form "usernameA, usernameB, ...".
929 930 931
 * @return $type
 *   Array of recipient types this should be limited to.
 *
932 933 934 935 936
 * @return
 *   Array, first element is an array of loaded user objects, second an array
 *   with invalid names.
 *
 */
937
function _privatemsg_parse_userstring($input, $types_limitations = array()) {
938 939 940 941 942
  if (is_string($input)) {
    $input = explode(',', $input);
  }

  // Start working through the input array.
litwol's avatar
litwol committed
943
  $invalid = array();
944
  $recipients = array();
945 946
  $duplicates = array();
  $denieds = array();
947 948
  foreach ($input as $string) {
    $string = trim($string);
949 950 951 952 953 954 955 956
    // Ignore spaces.
    if (!empty($string)) {

      // First, collect all matches.
      $matches = array();

      // Remember if a possible match denies access.
      $access_denied = FALSE;
957

958
      // Collect matches from hook implementations.
959 960
      foreach (module_implements('privatemsg_name_lookup') as $module) {
        $function = $module . '_privatemsg_name_lookup';
961 962
        $return = $function($string);
        if (isset($return) && is_array($return)) {
963 964 965 966 967 968 969 970 971
          foreach ($return as $recipient) {
            // Save recipients under their key to merge recipients which were
            // loaded multiple times.
            if (empty($recipient->type)) {
              $recipient->type = 'user';
              $recipient->recipient = $recipient->uid;
            }
            $matches[privatemsg_recipient_key($recipient)] = $recipient;
          }
972
        }
973
      }
974

975
      foreach ($matches as $key => $recipient) {
976 977
        // Check permissions, remove any recipients the user doesn't have write
        // access for.
978 979 980
        if (!privatemsg_recipient_access($recipient->type, 'write', $recipient)) {
          unset($matches[$key]);
          $access_denied = TRUE;
981
        }
982
        // Apply limitations.
983 984 985
        if (!empty($types_limitations) && !in_array($recipient->type, $types_limitations)) {
          unset($matches[$key]);
        }
986
      }
987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015

      // Allow modules to alter the found matches.
      drupal_alter('privatemsg_name_lookup_matches', $matches, $string);

      // Check if there are any matches.
      $number_of_matches = count($matches);
      switch ($number_of_matches) {
        case 1:
          // Only a single match found, add to recipients.
          $recipients += $matches;
          break;
        case 0:
          // No match found, check if access was denied.
          if ($access_denied) {
            // There were possible matches, but access was denied.
            $denieds[$string] = $string;
          }
          else {
            // The string does not contain any valid recipients.
            $invalid[$string] = $string;
          }
          break;

        default:
          // Multiple matches were found. The user has to specify which one he
          // meant.
          $duplicates[$string] = $matches;
          break;
      }
1016
    }
1017
  }
1018 1019 1020
  // Todo: Provide better API.
  return array($recipients, $invalid, $duplicates, $denieds);
}
litwol's avatar
litwol committed
1021

1022 1023 1024 1025
/**
 * Implements hook_privatemsg_name_lookup().
 */
function privatemsg_privatemsg_name_lookup($string) {
1026
  // Remove optional user specifier.
1027
  $string = trim(str_replace('[user]', '', $string));
1028 1029 1030 1031 1032 1033 1034 1035 1036
  // Fall back to the default username lookup.
  if (!$error = module_invoke('user', 'validate_name', $string)) {
    // String is a valid username, look it up.
    if ($recipient = user_load_by_name($string)) {
      $recipient->recipient = $recipient->uid;
      $recipient->type = 'user';
      return array(privatemsg_recipient_key($recipient) => $recipient);
    }
  }
litwol's avatar
litwol committed
1037
}
litwol's avatar
litwol committed
1038

1039 1040 1041 1042 1043
/**
 * @addtogroup sql
 * @{
 */

1044 1045 1046 1047 1048 1049 1050
/**
 * Query definition to load a list of threads.
 *
 * @param $account
 *  User object for which the messages are being loaded.
 * @param $argument
 *  string argument which can be used in the query builder to modify the thread listing.
1051
 *
1052
 * @see hook_query_privatemsg_list_alter()
1053
 */
1054 1055 1056 1057 1058 1059 1060 1061 1062
function privatemsg_sql_list($account, $argument = 'list') {
  $query = db_select('pm_message', 'pm')->extend('TableSort')->extend('PagerDefault');
  $query->join('pm_index', 'pmi', 'pm.mid = pmi.mid');

  // Create count query;
  $count_query = db_select('pm_message', 'pm');
  $count_query->addExpression('COUNT(DISTINCT pmi.thread_id)', 'count');
  $count_query->join('pm_index', 'pmi', 'pm.mid = pmi.mid');
  $count_query
1063 1064
    ->condition('pmi.recipient', $account->uid)
    ->condition('pmi.type', array('hidden', 'user'))
1065 1066
    ->condition('pmi.deleted', 0);
  $query->setCountQuery($count_query);
litwol's avatar
litwol committed
1067

1068 1069

  // Required columns
1070 1071 1072
  $query->addField('pmi', 'thread_id');
  $query->addExpression('MIN(pm.subject)', 'subject');
  $query->addExpression('MAX(pm.timestamp)', 'last_updated');
1073
  $query->addExpression('MAX(pm.has_tokens)', 'has_tokens');
1074 1075
  $query->addExpression('SUM(pmi.is_new)', 'is_new');

1076 1077 1078
  // Needed to for tracking replies.
  $query->addExpression('MAX(pm.reply_to_mid)', 'last_reply_to_mid');

1079
  // Load enabled columns
1080
  $fields = privatemsg_get_enabled_headers();
1081

1082
  if (in_array('count', $fields)) {
1083 1084
    // We only want the distinct number of messages in this thread.
    $query->addExpression('COUNT(distinct pmi.mid)', 'count');
1085
  }
1086
  if (in_array('participants', $fields)) {
1087
    // Query for a string with uids, for example "1,6,7". This needs a subquery on PostgreSQL.
1088
    if (db_driver() == 'pgsql') {
1089
      $query->addExpression("array_to_string(array(SELECT DISTINCT pmia.type || '_' || pmia.recipient
1090
                                                          FROM {pm_index} pmia
1091
                                                          WHERE pmia.type <> 'hidden' AND pmia.thread_id = pmi.thread_id AND pmia.recipient <> :current), ',')", 'participants', array(':current' => $account->uid));
1092 1093
    }
    else {
1094
      $query->addExpression("(SELECT GROUP_CONCAT(DISTINCT CONCAT(pmia.type, '_', pmia.recipient))
1095
                                     FROM {pm_index} pmia
1096
                                     WHERE pmia.type <> 'hidden' AND pmia.thread_id = pmi.thread_id AND pmia.recipient <> :current)", 'participants', array(':current' => $account->uid));
1097 1098 1099
    }
  }
  if (in_array('thread_started', $fields)) {
1100 1101 1102
    $query->addExpression('MIN(pm.timestamp)', 'thread_started');
  }
  return $query
1103 1104
    ->condition('pmi.recipient', $account->uid)
    ->condition('pmi.type', array('hidden', 'user'))
1105 1106
    ->condition('pmi.deleted', 0)
    ->groupBy('pmi.thread_id')
1107
    ->orderByHeader(privatemsg_get_headers())
1108
    ->limit(variable_get('privatemsg_per_page', 25));
1109 1110
}

1111 1112 1113 1114
/**
 * Query definition to load messages of one or multiple threads.
 *
 * @param $threads
1115
 *   Array with one or multiple thread id's.
1116
 * @param $account
1117
 *   User object for which the messages are being loaded.
1118
 * @param $load_all
1119
 *   Deleted messages are only loaded if this is set to TRUE.
1120
 *
1121
 * @see hook_query_privatemsg_messages_alter()
1122
 */
1123
function privatemsg_sql_messages($threads, $account = NULL, $load_all = FALSE) {
1124
  $query = db_select('pm_index', 'pmi');
1125
  $query->addField('pmi', 'mid');
1126
  $query->join('pm_message', 'pm', 'pm.mid = pmi.mid');
1127
  if (!$load_all) {
1128
    $query->condition('pmi.deleted', 0);
1129
  }
1130 1131
  // If there are multiple inserts during the same second (tests, for example)
  // sort by mid second to have them in the same order as they were saved.
1132
  $query
1133
    ->condition('pmi.thread_id', $threads)
1134 1135
    ->groupBy('pm.timestamp')
    ->groupBy('pmi.mid')
1136 1137 1138 1139 1140
    // Order by timestamp first.
    ->orderBy('pm.timestamp', 'ASC')
    // If there are multiple inserts during the same second (tests, for example)
    // sort by mid second to have them in the same order as they were saved.
    ->orderBy('pmi.mid', 'ASC');