Projet

Général

Profil

Paste
Télécharger (103 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / privatemsg / privatemsg.module @ 87dbc3bf

1
<?php
2

    
3
/**
4
 * @file
5
 * Allows users to send private messages to other users.
6
 */
7

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

    
21
/**
22
 * Implements hook_permission().
23
 */
24
function privatemsg_permission() {
25
  return array(
26
    'administer privatemsg settings' => array(
27
      'title' => t('Administer privatemsg'),
28
      'description' => t('Perform maintenance tasks for privatemsg'),
29
    ),
30
    'read privatemsg' => array(
31
      'title' => t('Read private messages'),
32
      'description' => t('Read private messages'),
33
    ),
34
    'read all private messages' => array(
35
      'title' => t('Read all private messages'),
36
      'description' => t('Includes messages of other users'),
37
    ),
38
    'write privatemsg' => array(
39
      'title' => t('Write new private messages'),
40
      'description' => t('Write new private messages'),
41
    ),
42
    'delete privatemsg' => array(
43
      'title' => t('Delete private messages'),
44
      'description' => t('Delete private messages'),
45
    ),
46
    'allow disabling privatemsg' => array(
47
      'title' => t('Allow disabling private messages'),
48
      'description' => t("Allows user to disable privatemsg so that they can't receive or send any private messages.")
49
    ),
50
    'reply only privatemsg' => array(
51
      'title' => t('Reply to private messages'),
52
      'description' => t('Allows to reply to private messages but not send new ones. Note that the write new private messages permission includes replies.')
53
    ),
54
    'use tokens in privatemsg' => array(
55
      'title' => t('Use tokens in private messages'),
56
      'description' => t("Allows user to use available tokens when sending private messages.")
57
    ),
58
    'select text format for privatemsg' => array(
59
      'title' => t('Select text format for private messages'),
60
      'description' => t('Allows to choose the text format when sending private messages. Otherwise, the default is used.'),
61
    ),
62
  );
63
}
64

    
65
/**
66
 * Generate array of user objects based on a string.
67
 *
68
 *
69
 * @param $userstring
70
 *   A string with user id, for example 1,2,4. Returned by the list query.
71
 *
72
 * @return
73
 *   Array with user objects.
74
 */
75
function _privatemsg_generate_user_array($string, $slice = NULL) {
76
  // Convert user uid list (uid1,uid2,uid3) into an array. If $slice is not NULL
77
  // pass that as argument to array_slice(). For example, -4 will only load the
78
  // last four users.
79
  // This is done to avoid loading user objects that are not displayed, for
80
  // obvious performance reasons.
81
  $users = explode(',', $string);
82
  if (!is_null($slice)) {
83
    $users = array_slice($users, $slice);
84
  }
85
  $participants = array();
86
  foreach ($users as $uid) {
87
    // If it is an integer, it is a user id.
88
    if ((int)$uid > 0) {
89
    $user_ids = privatemsg_user_load_multiple(array($uid));
90
      if ($account = array_shift($user_ids)) {
91
        $participants[privatemsg_recipient_key($account)] = $account;
92
      }
93
    }
94
    elseif (strpos($uid, '_') !== FALSE) {
95
      list($type, $id) = explode('_', $uid);
96
      $type_info = privatemsg_recipient_get_type($type);
97
      if ($type_info && isset($type_info['load']) && is_callable($type_info['load'])) {
98
        $temp_load = $type_info['load'](array($id));
99
        if ($participant = array_shift($temp_load)) {
100
          $participants[privatemsg_recipient_key($participant)] = $participant;
101
        }
102
      }
103
    }
104
  }
105
  return $participants;
106
}
107

    
108
/**
109
 * Format an array of user objects.
110
 *
111
 * @param $part_array
112
 *   Array with user objects, for example the one returned by
113
 *   _privatemsg_generate_user_array.
114
 *
115
 * @param $limit
116
 *   Limit the number of user objects which should be displayed.
117
 * @param $no_text
118
 *   When TRUE, don't display the Participants/From text.
119
 * @return
120
 *   String with formatted user objects, like user1, user2.
121
 */
122
function _privatemsg_format_participants($part_array, $limit = NULL, $no_text = FALSE) {
123
  global $user;
124
  if (count($part_array) > 0) {
125
    $to = array();
126
    $limited = FALSE;
127
    foreach ($part_array as $account) {
128

    
129
      // Directly address the current user.
130
      if (isset($account->type) && in_array($account->type, array('hidden', 'user')) && $account->recipient == $user->uid) {
131
        array_unshift($to, $no_text ? t('You') : t('you'));
132
        continue;
133
      }
134

    
135
      // Don't display recipients with type hidden.
136
      if (isset($account->type) && $account->type == 'hidden') {
137
        continue;
138
      }
139
      if (is_int($limit) && count($to) >= $limit) {
140
        $limited = TRUE;
141
        break;
142
      }
143
      $to[] = privatemsg_recipient_format($account);
144
    }
145

    
146
    $limit_string = '';
147
    if ($limited) {
148
      $limit_string = t(' and others');
149
    }
150

    
151

    
152
    if ($no_text) {
153
      return implode(', ', $to) . $limit_string;
154
    }
155

    
156
    $last = array_pop($to);
157
    if (count($to) == 0) { // Only one participant
158
      return t("From !last", array('!last' => $last));
159
    }
160
    else { // Multiple participants..
161
      $participants = implode(', ', $to);
162
      return t('Between !participants and !last', array('!participants' => $participants, '!last' => $last));
163
    }
164
  }
165
  return '';
166
}
167

    
168
/**
169
 * Implements hook_menu().
170
 */
171
function privatemsg_menu() {
172
  $items['messages'] = array(
173
    'title'            => 'Messages',
174
    'title callback'   => 'privatemsg_title_callback',
175
    'page callback'    => 'privatemsg_list_page',
176
    'page arguments'   => array('list'),
177
    'file'             => 'privatemsg.pages.inc',
178
    'access callback'  => 'privatemsg_user_access',
179
    'type'             => MENU_NORMAL_ITEM,
180
    'menu_name'        => 'user-menu',
181
  );
182
  $items['messages/list'] = array(
183
    'title'            => 'Messages',
184
    'page callback'    => 'privatemsg_list_page',
185
    'page arguments'   => array('list'),
186
    'file'             => 'privatemsg.pages.inc',
187
    'access callback'  => 'privatemsg_user_access',
188
    'type'             => MENU_DEFAULT_LOCAL_TASK,
189
    'weight'           => -10,
190
    'menu_name'        => 'user-menu',
191
  );
192
  $items['messages/view/%privatemsg_thread'] = array(
193
    // Set the third argument to TRUE so that we can show access denied instead
194
    // of not found.
195
    'load arguments'   => array(NULL, NULL, TRUE),
196
    'title'            => 'Read message',
197
    'page callback'    => 'privatemsg_view',
198
    'page arguments'   => array(2),
199
    'file'             => 'privatemsg.pages.inc',
200
    'access callback'  => 'privatemsg_view_access',
201
    'access arguments' => array(2),
202
    'type'             => MENU_LOCAL_TASK,
203
    'weight'           => -5,
204
    'menu_name'        => 'user-menu',
205
  );
206
  $items['messages/delete/%privatemsg_thread/%privatemsg_message'] = array(
207
    'title'            => 'Delete message',
208
    'page callback'    => 'drupal_get_form',
209
    'page arguments'   => array('privatemsg_delete', 2, 3),
210
    'file'             => 'privatemsg.pages.inc',
211
    'access callback'  => 'privatemsg_user_access',
212
    'access arguments' => array('delete privatemsg'),
213
    'type'             => MENU_CALLBACK,
214
    'weight'           => -10,
215
    'menu_name'        => 'user-menu',
216
  );
217
  $items['messages/new'] = array(
218
    'title'            => 'Write new message',
219
    'page callback'    => 'drupal_get_form',
220
    'page arguments'   => array('privatemsg_new', 2, 3, NULL),
221
    'file'             => 'privatemsg.pages.inc',
222
    'access callback'  => 'privatemsg_user_access',
223
    'access arguments' => array('write privatemsg'),
224
    'type'             => MENU_LOCAL_ACTION,
225
    'weight'           => -3,
226
    'menu_name'        => 'user-menu',
227
  );
228
  // Auto-completes available user names & removes duplicates.
229
  $items['messages/autocomplete'] = array(
230
    'page callback'    => 'privatemsg_autocomplete',
231
    'file'             => 'privatemsg.pages.inc',
232
    'access callback'  => 'privatemsg_user_access',
233
    'access arguments' => array('write privatemsg'),
234
    'type'             => MENU_CALLBACK,
235
  );
236
  $items['admin/config/messaging'] = array(
237
    'title' => 'Messaging',
238
    'description' => 'Messaging systems.',
239
    'page callback' => 'system_admin_menu_block_page',
240
    'access arguments' => array('access administration pages'),
241
    'file' => 'system.admin.inc',
242
    'file path' => drupal_get_path('module', 'system'),
243
  );
244
  $items['admin/config/messaging/privatemsg'] = array(
245
    'title'            => 'Private message settings',
246
    'description'      => 'Configure private messaging settings.',
247
    'page callback'    => 'drupal_get_form',
248
    'page arguments'   => array('privatemsg_admin_settings'),
249
    'file'             => 'privatemsg.admin.inc',
250
    'access arguments' => array('administer privatemsg settings'),
251
    'type'             => MENU_NORMAL_ITEM,
252
  );
253
  $items['admin/config/messaging/privatemsg/settings'] = array(
254
    'title'            => 'Private message settings',
255
    'description'      => 'Configure private messaging settings.',
256
    'page callback'    => 'drupal_get_form',
257
    'page arguments'   => array('privatemsg_admin_settings'),
258
    'file'             => 'privatemsg.admin.inc',
259
    'access arguments' => array('administer privatemsg settings'),
260
    'type'             => MENU_DEFAULT_LOCAL_TASK,
261
    'weight'           => -10,
262
  );
263
  $items['messages/undo/action'] = array(
264
    'title'            => 'Private messages',
265
    'description'      => 'Undo last thread action',
266
    'page callback'    => 'privatemsg_undo_action',
267
    'file'             => 'privatemsg.pages.inc',
268
    'access arguments' => array('read privatemsg'),
269
    'type'             => MENU_CALLBACK,
270
    'menu'             => 'user-menu',
271
  );
272
  $items['user/%/messages'] = array(
273
    'title' => 'Messages',
274
    'page callback'    => 'privatemsg_list_page',
275
    'page arguments'   => array('list', 1),
276
    'file'             => 'privatemsg.pages.inc',
277
    'access callback'  => 'privatemsg_user_access',
278
    'access arguments' => array('read all private messages'),
279
    'type' => MENU_LOCAL_TASK,
280
  );
281

    
282
  return $items;
283
}
284

    
285
/**
286
 * Implements hook_menu_local_tasks_alter().
287
 */
288
function privatemsg_menu_local_tasks_alter(&$data, $router_item, $root_path) {
289
  // Add action link to 'messages/new' on 'messages' page.
290
  $add_to_array = array('messages/list', 'messages/inbox', 'messages/sent');
291
  foreach ($add_to_array as $add_to) {
292
    if (strpos($root_path, $add_to) !== FALSE) {
293
    $item = menu_get_item('messages/new');
294
    if ($item['access']) {
295
      $data['actions']['output'][] = array(
296
        '#theme' => 'menu_local_action',
297
        '#link' => $item,
298
      );
299
    }
300
    break;
301
    }
302
  }
303
}
304

    
305
/**
306
 * Privatemsg  wrapper for user_access.
307
 *
308
 * Never allows anonymous user access as that doesn't makes sense.
309
 *
310
 * @param $permission
311
 *   Permission string, defaults to read privatemsg
312
 *
313
 * @return
314
 *   TRUE if user has access, FALSE if not
315
 *
316
 * @ingroup api
317
 */
318
function privatemsg_user_access($permission = 'read privatemsg', $account = NULL) {
319
  static $disabled_displayed = FALSE;
320
  if ( $account === NULL ) {
321
    global $user;
322
    $account = $user;
323
  }
324
  if (!$account->uid) { // Disallow anonymous access, regardless of permissions
325
    return FALSE;
326
  }
327
  if (privatemsg_is_disabled($account) && ($permission == 'write privatemsg') ) {
328
    if (arg(0) == 'messages' && variable_get('privatemsg_display_disabled_message', TRUE) && !$disabled_displayed) {
329
      $disabled_displayed = TRUE;
330
      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');
331
    }
332
    return FALSE;
333
  }
334
  if (!user_access($permission, $account)) {
335
    return FALSE;
336
  }
337
  return TRUE;
338
}
339

    
340
/**
341
 * Check access to the view messages page.
342
 *
343
 * Function to restrict the access of the view messages page to just the
344
 * messages/view/% pages and not to leave tabs artifact on other lower
345
 * level pages such as the messages/new/%.
346
 *
347
 * @param $thread
348
 *   A array containing all information about a specific thread, generated by
349
 *   privatemsg_thread_load().
350
 *
351
 * @ingroup api
352
 */
353
function privatemsg_view_access($thread) {
354
  // Do not allow access to threads without messages.
355
  if (empty($thread['messages'])) {
356
    // Count all messages, if there
357
    return FALSE;
358
  }
359
  if (privatemsg_user_access('read privatemsg') && arg(1) == 'view') {
360
    return TRUE;
361
  }
362
  return FALSE;
363
}
364

    
365
/**
366
 * Checks the status of private messaging for provided user.
367
 *
368
 * @param user object to check
369
 * @return TRUE if user has disabled private messaging, FALSE otherwise
370
 */
371
function privatemsg_is_disabled($account) {
372
  if (!$account || !isset($account->uid) || !$account->uid) {
373
    return FALSE;
374
  }
375

    
376
  if (!isset($account->privatemsg_disabled)) {
377
    // Make sure we have a fully loaded user object and try to load it if not.
378
    if ((!empty($account->roles) || $account = user_load($account->uid)) && user_access('allow disabling privatemsg', $account)) {
379
      $account->privatemsg_disabled = (bool)db_query('SELECT 1 FROM {pm_disable} WHERE uid = :uid ', array(':uid' => $account->uid))->fetchField();
380
    }
381
    else {
382
      $account->privatemsg_disabled = FALSE;
383
    }
384
  }
385

    
386
  return $account->privatemsg_disabled;
387
}
388

    
389
/**
390
 * Load a thread with all the messages and participants.
391
 *
392
 * This function is called by the menu system through the %privatemsg_thread
393
 * wildcard.
394
 *
395
 * @param $thread_id
396
 *   Thread id, pmi.thread_id or pm.mid of the first message in that thread.
397
 * @param $account
398
 *   User object for which the thread should be loaded, defaults to
399
 *   the current user.
400
 * @param $start
401
 *   Message offset from the start of the thread.
402
 * @param $useAccessDenied
403
 *   Set to TRUE if the function should forward to the access denied page
404
 *   instead of not found. This is used by the menu system because that does
405
 *   load arguments before access checks are made. Defaults to FALSE.
406
 *
407
 * @return
408
 *   $thread object, with keys messages, participants, title and user. messages
409
 *   contains an array of messages, participants an array of user, subject the
410
 *   subject of the thread and user the user viewing the thread.
411
 *
412
 *   If no messages are found, or the thread_id is invalid, the function returns
413
 *   FALSE.
414

    
415
 * @ingroup api
416
 */
417
function privatemsg_thread_load($thread_id, $account = NULL, $start = NULL, $useAccessDenied = FALSE) {
418
  $threads = &drupal_static(__FUNCTION__, array());
419
  $thread_id = (int)$thread_id;
420
  if ($thread_id > 0) {
421
    $thread = array('thread_id' => $thread_id);
422

    
423
    if (is_null($account)) {
424
      global $user;
425
      $account = clone $user;
426
    }
427

    
428
    if (!isset($threads[$account->uid])) {
429
      $threads[$account->uid] = array();
430
    }
431

    
432
    if (!array_key_exists($thread_id, $threads[$account->uid])) {
433
      // Load the list of participants.
434
      $thread['participants'] = _privatemsg_load_thread_participants($thread_id, $account, FALSE, 'view');
435
      $thread['read_all'] = FALSE;
436
      if (empty($thread['participants']) && privatemsg_user_access('read all private messages', $account)) {
437
        $thread['read_all'] = TRUE;
438
        // Load all participants.
439
        $thread['participants'] = _privatemsg_load_thread_participants($thread_id, FALSE, FALSE, 'view');
440
      }
441

    
442
      // Load messages returned by the messages query with privatemsg_message_load_multiple().
443
      $query = _privatemsg_assemble_query('messages', array($thread_id), $thread['read_all'] ? NULL : $account);
444
      // Use subquery to bypass group by since it is not possible to alter
445
      // existing GROUP BY statements.
446
      $countQuery = db_select($query);
447
      $countQuery->addExpression('COUNT(*)');
448
      $thread['message_count'] = $thread['to'] = $countQuery->execute()->fetchField();
449
      $thread['from'] = 1;
450
      // Check if we need to limit the messages.
451
      $max_amount = variable_get('privatemsg_view_max_amount', 20);
452

    
453
      // If there is no start value, select based on get params.
454
      if (is_null($start)) {
455
        if (isset($_GET['start']) && $_GET['start'] < $thread['message_count']) {
456
          $start = $_GET['start'];
457
        }
458
        elseif (!variable_get('privatemsg_view_use_max_as_default', FALSE) && $max_amount == PRIVATEMSG_UNLIMITED) {
459
          $start = PRIVATEMSG_UNLIMITED;
460
        }
461
        else {
462
          $start = $thread['message_count'] - (variable_get('privatemsg_view_use_max_as_default', FALSE) ? variable_get('privatemsg_view_default_amount', 10) : $max_amount);
463
        }
464
      }
465

    
466
      if ($start != PRIVATEMSG_UNLIMITED) {
467
        if ($max_amount == PRIVATEMSG_UNLIMITED) {
468
          $last_page = 0;
469
          $max_amount = $thread['message_count'];
470
        }
471
        else {
472
          // Calculate the number of messages on the "last" page to avoid
473
          // message overlap.
474
          // Note - the last page lists the earliest messages, not the latest.
475
          $paging_count = variable_get('privatemsg_view_use_max_as_default', FALSE) ? $thread['message_count'] - variable_get('privatemsg_view_default_amount', 10) : $thread['message_count'];
476
          $last_page = $paging_count % $max_amount;
477
        }
478

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

    
485
        //If there are newer messages on the page, show pager link allowing to go to the newer messages.
486
        if (($start + $max_amount + 1) < $thread['message_count']) {
487
          $thread['to'] = $start + $max_amount;
488
          $thread['newer_start'] = $start + $max_amount;
489
        }
490
        if ($start - $max_amount >= 0) {
491
          $thread['older_start'] = $start - $max_amount;
492
        }
493
        elseif ($start > 0) {
494
          $thread['older_start'] = 0;
495
        }
496

    
497
        // Do not show messages on the last page that would show on the page
498
        // before. This will only work when using the visual pager.
499
        if ($start < $last_page && $max_amount != PRIVATEMSG_UNLIMITED && $max_amount < $thread['message_count']) {
500
          unset($thread['older_start']);
501
          $thread['to'] = $thread['newer_start'] = $max_amount = $last_page;
502
          // Start from the first message - this is a specific hack to make sure
503
          // the message display has sane paging on the last page.
504
          $start = 0;
505
        }
506
        // Visual counts start from 1 instead of zero, so plus one.
507
        $thread['from'] = $start + 1;
508
        $query->range($start, $max_amount);
509
      }
510
      $conditions = array();
511
      if (!$thread['read_all']) {
512
        $conditions['account'] = $account;
513
      }
514
      $thread['messages'] = privatemsg_message_load_multiple($query->execute()->fetchCol(), $conditions);
515

    
516
      // If there are no messages, don't allow access to the thread.
517
      if (empty($thread['messages'])) {
518
        if ($useAccessDenied) {
519
          // Generate new query with read all to see if the thread does exist.
520
          $query = _privatemsg_assemble_query('messages', array($thread_id), NULL);
521
          $exists = $query->countQuery()->execute()->fetchField();
522
          if (!$exists) {
523
            // Thread does not exist, display 404.
524
            $thread = FALSE;
525
          }
526
        }
527
        else {
528
          $thread = FALSE;
529
        }
530
      }
531
      else {
532
        // General data, assume subject is the same for all messages of that thread.
533
        $thread['user'] = $account;
534
        $message = current($thread['messages']);
535
        $thread['subject'] = $thread['subject-original'] = $message->subject;
536
        if ($message->has_tokens) {
537
          $thread['subject'] = privatemsg_token_replace($thread['subject'], array('privatemsg_message' => $message), array('sanitize' => TRUE, 'privatemsg-show-span' => FALSE));
538
        }
539
      }
540
      $threads[$account->uid][$thread_id] = $thread;
541
    }
542
    return $threads[$account->uid][$thread_id];
543
  }
544
  return FALSE;
545
}
546

    
547
/**
548
 * Implements hook_privatemsg_view_template().
549
 *
550
 * Allows modules to define different message view template.
551
 *
552
 * This hook returns information about available themes for privatemsg viewing.
553
 *
554
 * array(
555
 *  'machine_template_name' => 'Human readable template name',
556
 *  'machine_template_name_2' => 'Human readable template name 2'
557
 * };
558
 */
559
function privatemsg_privatemsg_view_template() {
560
  return array(
561
    'privatemsg-view' => 'Default view',
562
  );
563
}
564

    
565
/**
566
 * Implements hook_cron().
567
 *
568
 * If the flush feature is enabled, a given amount of deleted messages that are
569
 * old enough are flushed.
570
 */
571
function privatemsg_cron() {
572
  if (variable_get('privatemsg_flush_enabled', FALSE)) {
573
    $query = _privatemsg_assemble_query('deleted', variable_get('privatemsg_flush_days', 30), variable_get('privatemsg_flush_max', 200));
574

    
575
    foreach ($query->execute()->fetchCol() as $mid) {
576
      $message = privatemsg_message_load($mid);
577
      module_invoke_all('privatemsg_message_flush', $message);
578

    
579
      // Delete recipients of the message.
580
      db_delete('pm_index')
581
        ->condition('mid', $mid)
582
        ->execute();
583
      // Delete message itself.
584
      db_delete('pm_message')
585
        ->condition('mid', $mid)
586
        ->execute();
587
    }
588
  }
589

    
590
  // Number of user ids to process for this cron run.
591
  $total_remaining = variable_get('privatemgs_cron_recipient_per_run', 1000);
592
  $current_process = variable_get('privatemsg_cron_recipient_process', array());
593

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

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

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

    
611
  $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));
612
  foreach ($result as $row) {
613
    // If this is equal to the row that is currently processed, add it first in
614
    // the array.
615
    if (!empty($current_process) && $current_process['mid'] == $row->mid && $current_process['type'] == $row->type && $current_process['recipient'] == $row->recipient) {
616
      array_unshift($rows, $row);
617
    }
618
    else {
619
      $rows[] = $row;
620
    }
621
  }
622

    
623
  foreach ($rows as $row) {
624
    $type = privatemsg_recipient_get_type($row->type);
625
    if (isset($type['load']) && is_callable($type['load'])) {
626
      $loaded = $type['load'](array($row->recipient));
627
      if (empty($loaded)) {
628
        continue;
629
      }
630
      $recipient = reset($loaded);
631
    }
632

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

    
639
    $load_function = $type['generate recipients'];
640
    $uids = $load_function($recipient, $total_remaining, $offset);
641
    if (!empty($uids)) {
642
      foreach ($uids as $uid) {
643
        privatemsg_message_change_recipient($row->mid, $uid, 'hidden');
644
      }
645
    }
646
    // If less than the total remaining uids were returned, we are finished.
647
    if (count($uids) < $total_remaining) {
648
      $total_remaining -= count($uids);
649
      db_update('pm_index')
650
        ->fields(array('is_new' => PRIVATEMSG_READ))
651
        ->condition('mid', $row->mid)
652
        ->condition('recipient', $row->recipient)
653
        ->condition('type', $row->type)
654
        ->execute();
655
      // Reset current process if necessary.
656
      if ($offset > 0) {
657
        variable_set('privatemsg_cron_recipient_process', array());
658
      }
659
    }
660
    else {
661
      // We are not yet finished, save current process and break.
662
      $existing_offset = isset($current_process['offset']) ? $current_process['offset'] : 0;
663
      $current_process = (array)$row;
664
      $current_process['offset'] = $existing_offset + count($uids);
665
      variable_set('privatemsg_cron_recipient_process', $current_process);
666
      break;
667
    }
668
  }
669
}
670

    
671
function privatemsg_theme() {
672
  $templates = array(
673
    'privatemsg_view'    => array(
674
      'variables'        => array('message' => NULL),
675
      'template'         => variable_get('private_message_view_template', 'privatemsg-view'), // 'privatemsg',
676
    ),
677
    'privatemsg_from'    => array(
678
      'variables'        => array('author' => NULL),
679
      'template'         => 'privatemsg-from',
680
    ),
681
    'privatemsg_recipients' => array(
682
      'variables'        => array('message' => NULL),
683
      'template'         => 'privatemsg-recipients',
684
    ),
685
    'privatemsg_between' => array(
686
      'variables'        => array('recipients' => NULL),
687
      'template'         => 'privatemsg-between',
688
    ),
689
    // Define pattern for header/field templates. The theme system will register all
690
    // theme functions that start with the defined pattern.
691
    'privatemsg_list_header'  => array(
692
      'file'                  => 'privatemsg.theme.inc',
693
      'path'                  => drupal_get_path('module', 'privatemsg'),
694
      'pattern'               => 'privatemsg_list_header__',
695
      'variables'             => array(),
696
    ),
697
    'privatemsg_list_field'   => array(
698
      'file'                  => 'privatemsg.theme.inc',
699
      'path'                  => drupal_get_path('module', 'privatemsg'),
700
      'pattern'               => 'privatemsg_list_field__',
701
      'variables'             => array('thread' => array()),
702
    ),
703
    'privatemsg_new_block'  => array(
704
      'file'                  => 'privatemsg.theme.inc',
705
      'path'                  => drupal_get_path('module', 'privatemsg'),
706
      'variables'             => array('count'),
707
    ),
708
    'privatemsg_username'  => array(
709
      'file'                  => 'privatemsg.theme.inc',
710
      'path'                  => drupal_get_path('module', 'privatemsg'),
711
      'variables'             => array('recipient' => NULL, 'options' => array()),
712
    ),
713
  );
714
  // Include the theme file to load the theme suggestions.
715
  module_load_include('inc', 'privatemsg', 'privatemsg.theme');
716
  $templates += drupal_find_theme_functions($templates, array('theme'));
717
  return $templates;
718
}
719

    
720
function template_preprocess_privatemsg_view(&$vars) {
721
  global $user;
722

    
723
  $message = $vars['message'];
724
  $vars['mid'] = isset($message->mid) ? $message->mid : NULL;
725
  $vars['message_classes'] = isset($message->classes) ? $message->classes : array();
726
  $vars['thread_id'] = isset($message->thread_id) ? $message->thread_id : NULL;
727
  $vars['author_picture'] = theme('user_picture', array('account' => $message->author));
728
  // Directly address the current user if he is the author.
729
  if ($user->uid == $message->author->uid) {
730
    $vars['author_name_link'] = t('You');
731
  }
732
  else {
733
    $vars['author_name_link'] = privatemsg_recipient_format($message->author);
734
  }
735
  $vars['message_timestamp'] = privatemsg_format_date($message->timestamp);
736

    
737
  $message->content = array(
738
    '#view_mode' => 'message',
739
    'body' => array(
740
      '#markup' => check_markup($message->body, $message->format),
741
      '#weight' => -4,
742
    ),
743
  );
744

    
745
  if ($message->has_tokens) {
746
    // Replace tokens including option to add a notice if the user is not a
747
    // recipient.
748
    $message->content['body']['#markup'] = privatemsg_token_replace($message->content['body']['#markup'], array('privatemsg_message' => $message), array('privatemsg-token-notice' => TRUE, 'sanitize' => TRUE));
749
  }
750

    
751
  // Build fields content.
752
  field_attach_prepare_view('privatemsg_message', array($vars['mid'] => $message), 'message');
753
  $message->content += field_attach_view('privatemsg_message', $message, 'message');
754

    
755
  // Render message body.
756
  $vars['message_body'] =  drupal_render($message->content);
757
  if (isset($vars['mid']) && isset($vars['thread_id']) && privatemsg_user_access('delete privatemsg')) {
758
    $vars['message_actions'][] = array('title' => t('Delete'), 'href' => 'messages/delete/' . $vars['thread_id'] . '/' . $vars['mid']);
759
  }
760
  $vars['message_anchors'][] = 'privatemsg-mid-' . $vars['mid'];
761
  if (!empty($message->is_new)) {
762
    $vars['message_anchors'][] = 'new';
763
    $vars['new'] = drupal_ucfirst(t('new'));
764
  }
765

    
766
  // call hook_privatemsg_message_view_alter
767
  drupal_alter('privatemsg_message_view', $vars);
768

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

    
771
  $vars['anchors'] = '';
772
  foreach ($vars['message_anchors'] as $anchor) {
773
    $vars['anchors'] .= '<a name="' . $anchor . '"></a>';
774
  }
775
}
776

    
777
function template_preprocess_privatemsg_recipients(&$vars) {
778
  $vars['participants'] = ''; // assign a default empty value
779
  if (isset($vars['thread']['participants'])) {
780
    $vars['participants'] = _privatemsg_format_participants($vars['thread']['participants']);
781
  }
782
}
783

    
784
/**
785
 * Changes the read/new status of a single message.
786
 *
787
 * @param $pmid
788
 *   Message id
789
 * @param $status
790
 *   Either PRIVATEMSG_READ or PRIVATEMSG_UNREAD
791
 * @param $account
792
 *   User object, defaults to the current user
793
 */
794
function privatemsg_message_change_status($pmid, $status, $account = NULL) {
795
  if (!$account) {
796
    global $user;
797
    $account = $user;
798
  }
799
  db_update('pm_index')
800
    ->fields(array('is_new' => $status))
801
    ->condition('mid', $pmid)
802
    ->condition('recipient', $account->uid)
803
    ->condition('type', array('hidden', 'user'))
804
    ->execute();
805

    
806
  // Allows modules to respond to the status change.
807
  module_invoke_all('privatemsg_message_status_changed', $pmid, $status, $account);
808
}
809

    
810
/**
811
 * Return number of unread messages for an account.
812
 *
813
 * @param $account
814
 *   Specify the user for which the unread count should be loaded.
815
 *
816
 * @ingroup api
817
 */
818
function privatemsg_unread_count($account = NULL) {
819
  $counts = &drupal_static(__FUNCTION__, array());
820
  if (!$account || $account->uid == 0) {
821
    global $user;
822
    $account = $user;
823
  }
824
  if (!isset($counts[$account->uid])) {
825
    $counts[$account->uid] = _privatemsg_assemble_query('unread_count', $account)
826
      ->execute()
827
      ->fetchField();
828
  }
829
  return $counts[$account->uid];
830
}
831

    
832
/**
833
 * Load all participants of a thread.
834
 *
835
 * @param $thread_id
836
 *   Thread ID for which the participants should be loaded.
837
 * @param $account
838
 *   For which account should the messages be loaded. *
839
 * @param $ignore_hidden
840
 *   Ignores hidden participants.
841
 * @param $access
842
 *   Which access permission should be checked (write or view).
843
 *
844
 * @return
845
 *   Array with all visible/writable participants for that thread.
846
 */
847
function _privatemsg_load_thread_participants($thread_id, $account, $ignore_hidden = TRUE, $access = 'write') {
848
  $query = _privatemsg_assemble_query('participants', $thread_id, $account);
849
  $participants = array();
850
  $to_load = array();
851
  foreach ($query->execute() as $participant) {
852
    if ($ignore_hidden && $participant->type == 'hidden') {
853
      continue;
854
    }
855
    elseif (privatemsg_recipient_access($participant->type, $access, $participant)) {
856
      $to_load[$participant->type][] = $participant->recipient;
857
    }
858
  }
859

    
860
  // Now, load all non-user recipients.
861
  foreach ($to_load as $type => $ids) {
862
    $type_info = privatemsg_recipient_get_type($type);
863
    if (isset($type_info['load']) && is_callable($type_info['load'])) {
864
      $loaded = $type_info['load']($ids);
865
      if (is_array($loaded)) {
866
        $participants += $loaded;
867
      }
868
    }
869
  }
870
  if ($access == 'write' && $account) {
871
    // Remove author if loading participants for writing and when he is not the
872
    // only recipient.
873
    if (isset($participants['user_' . $account->uid]) && count($participants) > 1) {
874
      unset($participants['user_' . $account->uid]);
875
    }
876
  }
877
  return $participants;
878
}
879

    
880
/**
881
 * Extract the valid usernames of a string and loads them.
882
 *
883
 * This function is used to parse a string supplied by a username autocomplete
884
 * field and load all user objects.
885
 *
886
 * @param $string
887
 *   A string in the form "usernameA, usernameB, ...".
888
 * @return $type
889
 *   Array of recipient types this should be limited to.
890
 *
891
 * @return
892
 *   Array, first element is an array of loaded user objects, second an array
893
 *   with invalid names.
894
 *
895
 */
896
function _privatemsg_parse_userstring($input, $types_limitations = array()) {
897
  if (is_string($input)) {
898
    $input = explode(',', $input);
899
  }
900

    
901
  // Start working through the input array.
902
  $invalid = array();
903
  $recipients = array();
904
  $duplicates = array();
905
  $denieds = array();
906
  foreach ($input as $string) {
907
    $string = trim($string);
908
    // Ignore spaces.
909
    if (!empty($string)) {
910

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

    
914
      // Remember if a possible match denies access.
915
      $access_denied = FALSE;
916

    
917
      // Collect matches from hook implementations.
918
      foreach (module_implements('privatemsg_name_lookup') as $module) {
919
        $function = $module . '_privatemsg_name_lookup';
920
        $return = $function($string);
921
        if (isset($return) && is_array($return)) {
922
          foreach ($return as $recipient) {
923
            // Save recipients under their key to merge recipients which were
924
            // loaded multiple times.
925
            if (empty($recipient->type)) {
926
              $recipient->type = 'user';
927
              $recipient->recipient = $recipient->uid;
928
            }
929
            $matches[privatemsg_recipient_key($recipient)] = $recipient;
930
          }
931
        }
932
      }
933

    
934
      foreach ($matches as $key => $recipient) {
935
        // Check permissions, remove any recipients the user doesn't have write
936
        // access for.
937
        if (!privatemsg_recipient_access($recipient->type, 'write', $recipient)) {
938
          unset($matches[$key]);
939
          $access_denied = TRUE;
940
        }
941
        // Apply limitations.
942
        if (!empty($types_limitations) && !in_array($recipient->type, $types_limitations)) {
943
          unset($matches[$key]);
944
        }
945
      }
946

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

    
950
      // Check if there are any matches.
951
      $number_of_matches = count($matches);
952
      switch ($number_of_matches) {
953
        case 1:
954
          // Only a single match found, add to recipients.
955
          $recipients += $matches;
956
          break;
957
        case 0:
958
          // No match found, check if access was denied.
959
          if ($access_denied) {
960
            // There were possible matches, but access was denied.
961
            $denieds[$string] = $string;
962
          }
963
          else {
964
            // The string does not contain any valid recipients.
965
            $invalid[$string] = $string;
966
          }
967
          break;
968

    
969
        default:
970
          // Multiple matches were found. The user has to specify which one he
971
          // meant.
972
          $duplicates[$string] = $matches;
973
          break;
974
      }
975
    }
976
  }
977
  // Todo: Provide better API.
978
  return array($recipients, $invalid, $duplicates, $denieds);
979
}
980

    
981
/**
982
 * Implements hook_privatemsg_name_lookup().
983
 */
984
function privatemsg_privatemsg_name_lookup($string) {
985
  // Remove optional user specifier.
986
  $string = trim(str_replace('[user]', '', $string));
987
  // Fall back to the default username lookup.
988
  if (!$error = module_invoke('user', 'validate_name', $string)) {
989
    // String is a valid username, look it up.
990
    if ($recipient = user_load_by_name($string)) {
991
      $recipient->recipient = $recipient->uid;
992
      $recipient->type = 'user';
993
      return array(privatemsg_recipient_key($recipient) => $recipient);
994
    }
995
  }
996
}
997

    
998
/**
999
 * @addtogroup sql
1000
 * @{
1001
 */
1002

    
1003
/**
1004
 * Query definition to load a list of threads.
1005
 *
1006
 * @param $account
1007
 *  User object for which the messages are being loaded.
1008
 * @param $argument
1009
 *  string argument which can be used in the query builder to modify the thread listing.
1010
 *
1011
 * @see hook_query_privatemsg_list_alter()
1012
 */
1013
function privatemsg_sql_list($account, $argument = 'list') {
1014
  $query = db_select('pm_message', 'pm')->extend('TableSort')->extend('PagerDefault');
1015
  $query->join('pm_index', 'pmi', 'pm.mid = pmi.mid');
1016

    
1017
  // Create count query;
1018
  $count_query = db_select('pm_message', 'pm');
1019
  $count_query->addExpression('COUNT(DISTINCT pmi.thread_id)', 'count');
1020
  $count_query->join('pm_index', 'pmi', 'pm.mid = pmi.mid');
1021
  $count_query
1022
    ->condition('pmi.recipient', $account->uid)
1023
    ->condition('pmi.type', array('hidden', 'user'))
1024
    ->condition('pmi.deleted', 0);
1025
  $query->setCountQuery($count_query);
1026

    
1027

    
1028
  // Required columns
1029
  $query->addField('pmi', 'thread_id');
1030
  $query->addExpression('MIN(pm.subject)', 'subject');
1031
  $query->addExpression('MAX(pm.timestamp)', 'last_updated');
1032
  $query->addExpression('MAX(pm.has_tokens)', 'has_tokens');
1033
  $query->addExpression('SUM(pmi.is_new)', 'is_new');
1034

    
1035
  // Load enabled columns
1036
  $fields = array_filter(variable_get('privatemsg_display_fields', array('participants')));
1037

    
1038
  if (in_array('count', $fields)) {
1039
    // We only want the distinct number of messages in this thread.
1040
    $query->addExpression('COUNT(distinct pmi.mid)', 'count');
1041
  }
1042
  if (in_array('participants', $fields)) {
1043
    // Query for a string with uids, for example "1,6,7". This needs a subquery on PostgreSQL.
1044
    if (db_driver() == 'pgsql') {
1045
      $query->addExpression("array_to_string(array(SELECT DISTINCT pmia.type || '_' || pmia.recipient
1046
                                                          FROM {pm_index} pmia
1047
                                                          WHERE pmia.type <> 'hidden' AND pmia.thread_id = pmi.thread_id AND pmia.recipient <> :current), ',')", 'participants', array(':current' => $account->uid));
1048
    }
1049
    else {
1050
      $query->addExpression("(SELECT GROUP_CONCAT(DISTINCT CONCAT(pmia.type, '_', pmia.recipient))
1051
                                     FROM {pm_index} pmia
1052
                                     WHERE pmia.type <> 'hidden' AND pmia.thread_id = pmi.thread_id AND pmia.recipient <> :current)", 'participants', array(':current' => $account->uid));
1053
    }
1054
  }
1055
  if (in_array('thread_started', $fields)) {
1056
    $query->addExpression('MIN(pm.timestamp)', 'thread_started');
1057
  }
1058
  return $query
1059
    ->condition('pmi.recipient', $account->uid)
1060
    ->condition('pmi.type', array('hidden', 'user'))
1061
    ->condition('pmi.deleted', 0)
1062
    ->groupBy('pmi.thread_id')
1063
    ->orderByHeader(_privatemsg_list_headers(array_merge(array('subject', 'last_updated'), $fields)))
1064
    ->limit(variable_get('privatemsg_per_page', 25));
1065
}
1066

    
1067
/**
1068
 * Query definition to load messages of one or multiple threads.
1069
 *
1070
 * @param $threads
1071
 *   Array with one or multiple thread id's.
1072
 * @param $account
1073
 *   User object for which the messages are being loaded.
1074
 * @param $load_all
1075
 *   Deleted messages are only loaded if this is set to TRUE.
1076
 *
1077
 * @see hook_query_privatemsg_messages_alter()
1078
 */
1079
function privatemsg_sql_messages($threads, $account = NULL, $load_all = FALSE) {
1080
  $query = db_select('pm_index', 'pmi');
1081
  $query->addField('pmi', 'mid');
1082
  $query->join('pm_message', 'pm', 'pm.mid = pmi.mid');
1083
  if (!$load_all) {
1084
    $query->condition('pmi.deleted', 0);
1085
  }
1086
  // If there are multiple inserts during the same second (tests, for example)
1087
  // sort by mid second to have them in the same order as they were saved.
1088
  $query
1089
    ->condition('pmi.thread_id', $threads)
1090
    ->groupBy('pm.timestamp')
1091
    ->groupBy('pmi.mid')
1092
    // Order by timestamp first.
1093
    ->orderBy('pm.timestamp', 'ASC')
1094
    // If there are multiple inserts during the same second (tests, for example)
1095
    // sort by mid second to have them in the same order as they were saved.
1096
    ->orderBy('pmi.mid', 'ASC');
1097
  if ($account) {
1098
    $query
1099
      ->condition('pmi.recipient', $account->uid)
1100
      ->condition('pmi.type', array('hidden', 'user'));
1101
  }
1102
  return $query;
1103
}
1104

    
1105
/**
1106
 * Load all participants of a thread.
1107
 *
1108
 * @param $thread_id
1109
 *   Thread id from which the participants should be loaded.
1110
 * @param $account
1111
 *   User account that should be considered when loading participants.
1112
 *
1113
 * @see hook_query_privatemsg_participants_alter()
1114
 */
1115
function privatemsg_sql_participants($thread_id, $account = NULL) {
1116
  $query = db_select('pm_index', 'pmi');
1117
  $query->leftJoin('users', 'u', "u.uid = pmi.recipient AND pmi.type IN ('user', 'hidden')");
1118
  $query
1119
    ->fields('pmi', array('recipient', 'type'))
1120
    ->fields('u', array('name'))
1121
    ->condition('pmi.thread_id', $thread_id);
1122

    
1123
  // If an account is provided, limit participants.
1124
  if ($account) {
1125
    $query->condition(db_or()
1126
      ->condition('pmi.type', 'hidden', '<>')
1127
      ->condition(db_and()
1128
        ->condition('pmi.type', 'hidden')
1129
        ->condition('pmi.recipient', $account->uid)
1130
    ));
1131

    
1132
    // Only load recipients of messages which are visible for that user.
1133
    $query->where('(SELECT 1 FROM {pm_index} pmiu WHERE pmi.mid = pmiu.mid AND pmiu.recipient = :recipient LIMIT 1) = 1', array(':recipient' => $account->uid));
1134
  }
1135
  else {
1136
    // If not, only limit participants to visible ones.
1137
    $query->condition('pmi.type', 'hidden', '<>');
1138
  }
1139

    
1140
  return $query
1141
    ->groupBy('pmi.recipient')
1142
    ->groupBy('u.name')
1143
    ->groupBy('pmi.type');
1144
}
1145

    
1146
/**
1147
 * Count threads with unread messages.
1148
 *
1149
 * @param $account
1150
 *   User account for which should be checked.
1151
 *
1152
 * @see hook_query_privatemsg_unread_count_alter()
1153
 */
1154
function privatemsg_sql_unread_count($account) {
1155
  $query = db_select('pm_index', 'pmi');
1156
  $query->addExpression('COUNT(DISTINCT thread_id)', 'unread_count');
1157
  return $query
1158
    ->condition('pmi.deleted', 0)
1159
    ->condition('pmi.is_new', 1)
1160
    ->condition('pmi.recipient', $account->uid)
1161
    ->condition('pmi.type', array('hidden', 'user'));
1162
}
1163

    
1164
/**
1165
 * Looks up autocomplete suggestions for users.
1166
 *
1167
 * @param $search
1168
 *   The string that is being searched for.
1169
 * @param $names
1170
 *   Array of names which are already selected and should be excluded.
1171
 *
1172
 * @see hook_query_privatemsg_autocomplete_alter()
1173
 */
1174
function privatemsg_sql_autocomplete($search, $names) {
1175
  $query = db_select('users', 'u')
1176
    ->fields('u', array('uid'))
1177
    ->condition('u.name', $search . '%', 'LIKE')
1178
    ->condition('u.status', 0, '<>')
1179
    ->where('NOT EXISTS (SELECT 1 FROM {pm_disable} pd WHERE pd.uid=u.uid)')
1180
    ->orderBy('u.name', 'ASC')
1181
    ->range(0, 10);
1182

    
1183
  if (!empty($names)) {
1184
    $query->condition('u.name', $names, 'NOT IN');
1185
  }
1186
  return $query;
1187
}
1188

    
1189
/**
1190
 * Query Builder function to load all messages that should be flushed.
1191
 *
1192
 * @param $days
1193
 *   Select messages older than x days.
1194
 * @param $max
1195
 *   Select no more than $max messages.
1196
 *
1197
 * @see hook_query_privatemsg_deleted_alter()
1198
 */
1199
function privatemsg_sql_deleted($days, $max) {
1200
  $query = db_select('pm_message', 'pm');
1201
  $query->addField('pm', 'mid');
1202

    
1203
  $query->join('pm_index', 'pmi', 'pmi.mid = pm.mid');
1204
  return $query
1205
    ->groupBy('pm.mid')
1206
    ->having('MIN(pmi.deleted) > 0 AND MAX(pmi.deleted) < :old', array(':old' => REQUEST_TIME - $days * 86400))
1207
    ->range(0, $max);
1208
}
1209

    
1210
/**
1211
 * @}
1212
 */
1213

    
1214
/**
1215
 * Implements hook_user_view().
1216
 */
1217
function privatemsg_user_view($account) {
1218
  if (($url = privatemsg_get_link(array($account))) && variable_get('privatemsg_display_profile_links', 1)) {
1219
    $account->content['privatemsg_send_new_message'] = array(
1220
      '#type'   => 'link',
1221
      '#title'  => t('Send this user a private message'),
1222
      '#href'  => $url,
1223
      '#weight' => 10,
1224
      '#options' => array(
1225
        'query' => drupal_get_destination(),
1226
        'title' => t('Send this user a private message'),
1227
        'attributes' => array('class' => 'privatemsg-send-link privatemsg-send-link-profile'),
1228
      ),
1229
    );
1230
  }
1231
}
1232

    
1233
/**
1234
 * Implements hook_user_login().
1235
 */
1236
function privatemsg_user_login(&$edit, $account) {
1237
  if (variable_get('privatemsg_display_loginmessage', TRUE) && privatemsg_user_access()) {
1238
    $count = privatemsg_unread_count();
1239
    if ($count) {
1240
      drupal_set_message(format_plural($count, 'You have <a href="@messages">1 unread message</a>.', 'You have <a href="@messages">@count unread messages</a>', array('@messages' => url('messages'))));
1241
    }
1242
  }
1243
}
1244

    
1245
/**
1246
 * Implements hook_user_cancel().
1247
 */
1248
function privatemsg_user_cancel($edit, $account, $method) {
1249
  switch ($method) {
1250
    case 'user_cancel_reassign':
1251
      db_update('pm_message')
1252
        ->condition('author', $account->uid)
1253
        ->fields(array('author' => 0))
1254
        ->execute();
1255
      break;
1256
    case 'user_cancel_block_unpublish':
1257
      _privatemsg_delete_data($account);
1258
      break;
1259
  }
1260
  // Always delete user settings.
1261
  db_delete('pm_disable')
1262
    ->condition('uid', $account->uid)
1263
    ->execute();
1264
}
1265

    
1266
/**
1267
 * Implements hook_user_delete().
1268
 */
1269
function privatemsg_user_delete($account) {
1270
  _privatemsg_delete_data($account);
1271
  // Always delete user settings.
1272
  db_delete('pm_disable')
1273
    ->condition('uid', $account->uid)
1274
    ->execute();
1275
}
1276

    
1277
/**
1278
 * Delete all message data from a user.
1279
 */
1280
function _privatemsg_delete_data($account) {
1281
  $mids = db_select('pm_message', 'pm')
1282
    ->fields('pm', array('mid'))
1283
    ->condition('author', $account->uid)
1284
    ->execute()
1285
    ->fetchCol();
1286

    
1287
  if (!empty($mids)) {
1288
    // Delete recipient entries in {pm_index} of the messages the user wrote.
1289
    db_delete('pm_index')
1290
      ->condition('mid', $mids)
1291
      ->execute();
1292
  }
1293

    
1294
  // Delete messages the user wrote.
1295
  db_delete('pm_message')
1296
    ->condition('author', $account->uid)
1297
    ->execute();
1298

    
1299
  // Delete recipient entries of that user.
1300
  db_delete('pm_index')
1301
    ->condition('recipient', $account->uid)
1302
    ->condition('type', array('user', 'hidden'))
1303
    ->execute();
1304

    
1305
  // DELETE any disable flag for user.
1306
  db_delete('pm_disable')
1307
    ->condition('uid', $account->uid)
1308
    ->execute();
1309
}
1310

    
1311
/**
1312
 * Implements hook_form_alter().
1313
 */
1314
function privatemsg_form_alter(&$form, &$form_state, $form_id) {
1315
  if (($form_id == 'user_register_form' || $form_id == 'user_profile_form') && $form['#user_category'] == 'account') {
1316

    
1317
    // Create array to be able to merge in fieldset and avoid overwriting
1318
    // already added options.
1319
    if (!isset($form['privatemsg'])) {
1320
      $form['privatemsg'] = array();
1321
    }
1322

    
1323
    // Always create the fieldset in case other modules want to add
1324
    // Privatemsg-related settings through hook_form_alter(). If it's still
1325
    // empty after the build process, the after build function will remove it.
1326
    $form['privatemsg'] += array(
1327
      '#type' => 'fieldset',
1328
      '#title' => t('Private messages'),
1329
      '#collapsible' => TRUE,
1330
      '#collapsed' => FALSE,
1331
      '#weight' => 10,
1332
      '#after_build' => array('privatemsg_account_fieldset_remove_if_empty'),
1333
    );
1334

    
1335
    // We have to use user_access() because privatemsg_user_access() would
1336
    // return FALSE when privatemsg is disabled.
1337
    if ((user_access('write privatemsg') || user_access('read privatemsg')) && user_access('allow disabling privatemsg')) {
1338
      $form['privatemsg']['pm_enable'] = array(
1339
        '#type' => 'checkbox',
1340
        '#title' => t('Enable private messages'),
1341
        '#default_value' => !privatemsg_is_disabled($form['#user']),
1342
        '#description' => t('Disabling private messages prevents you from sending or receiving messages from other users.'),
1343
        '#weight' => -10,
1344
      );
1345
    }
1346
  }
1347
}
1348

    
1349
/**
1350
 * Hides the settings fieldset if there are no options to be displayed.
1351
 */
1352
function privatemsg_account_fieldset_remove_if_empty($element) {
1353
  if (count(element_children($element)) == 0) {
1354
    $element['#access'] = FALSE;
1355
  }
1356
  return $element;
1357
}
1358

    
1359
/**
1360
 * Implements hook_user_insert().
1361
 */
1362
function privatemsg_user_update(&$edit, $account, $category) {
1363
  if (isset($edit['pm_enable']) && (user_access('write privatemsg') || user_access('read privatemsg')) && user_access('allow disabling privatemsg')) {
1364
    $current = privatemsg_is_disabled($account);
1365
    $disabled = (!$edit['pm_enable']);
1366
    $edit['pm_enable'] = NULL;
1367

    
1368
    $account->privatemsg_disabled = $disabled;
1369

    
1370
    // only perform the save if the value has changed
1371
    if ($current != $disabled) {
1372
      if ($disabled) {
1373
        db_insert('pm_disable')
1374
          ->fields(array('uid' => $account->uid))
1375
          ->execute();
1376
      }
1377
      else {
1378
        db_delete('pm_disable')
1379
          ->condition('uid', $account->uid)
1380
          ->execute();
1381
      }
1382
    }
1383
  }
1384
}
1385

    
1386
/**
1387
 * Implements hook_privatemsg_block_message().
1388
 */
1389
function privatemsg_privatemsg_block_message($author, $recipients, $context = array()) {
1390
  $blocked = array();
1391
  if (privatemsg_is_disabled($author)) {
1392
    $blocked[] = array(
1393
      'recipient' => 'user_' . $author->uid,
1394
      'message' => t('You have disabled private message sending and receiving.'),
1395
    );
1396
  }
1397
  foreach ($recipients as $recipient) {
1398
    if (privatemsg_is_disabled($recipient)) {
1399
      $blocked[] = array(
1400
        'recipient' => 'user_' . $recipient->uid,
1401
        'message' => t('%recipient has disabled private message receiving.', array('%recipient' => privatemsg_recipient_format($recipient, array('plain' => TRUE)))),
1402
      );
1403
    }
1404
    // Do not send private messages to blocked users.
1405
    else if (isset($recipient->status) && (!$recipient->status)) {
1406
      $blocked[] = array(
1407
        'recipient' => 'user_' . $recipient->uid,
1408
        'message' => t('%recipient has disabled his or her account.', array('%recipient' => privatemsg_recipient_format($recipient, array('plain' => TRUE)))),
1409
      );
1410
    }
1411
  }
1412

    
1413
  return $blocked;
1414
}
1415

    
1416
/**
1417
 * Implements hook_block_info().
1418
 */
1419
function privatemsg_block_info() {
1420
  $blocks = array();
1421
  $blocks['privatemsg-menu'] = array(
1422
      'info' => t('Privatemsg links'),
1423
      'cache' => DRUPAL_NO_CACHE,
1424
  );
1425
  $blocks['privatemsg-new'] = array(
1426
      'info' => t('New message indication'),
1427
      'cache' => DRUPAL_NO_CACHE,
1428
  );
1429

    
1430
  return $blocks;
1431
}
1432

    
1433
/**
1434
 * Implements hook_block_view().
1435
 */
1436
function privatemsg_block_view($delta) {
1437
  $block = array();
1438
  switch ($delta) {
1439
    case 'privatemsg-menu':
1440
      $block = _privatemsg_block_menu();
1441
      break;
1442
    case 'privatemsg-new':
1443
      $block = _privatemsg_block_new();
1444
      break;
1445
  }
1446
  return $block;
1447
}
1448

    
1449
/**
1450
 * Implements hook_block_configure().
1451
 */
1452
function privatemsg_block_configure($delta = '') {
1453
  if ($delta == 'privatemsg-new') {
1454
    $form['notification'] = array(
1455
      '#type' => 'checkbox',
1456
      '#title' => t('Display block when there are no new messages'),
1457
      '#default_value' => variable_get('privatemsg_no_messages_notification', 0),
1458
      '#description' => t('Enable this to have this block always displayed, even if there are no new messages'),
1459
    );
1460
    return $form;
1461
  }
1462
}
1463

    
1464
/**
1465
 * Implements hook_block_save().
1466
 */
1467
function privatemsg_block_save($delta = '', $edit = array()) {
1468
  if ($delta == 'privatemsg-new') {
1469
    variable_set('privatemsg_no_messages_notification', $edit['notification']);
1470
  }
1471
}
1472

    
1473
function privatemsg_title_callback($title = NULL) {
1474
  $count = privatemsg_unread_count();
1475

    
1476
  if ($count > 0) {
1477
    return format_plural($count, 'Messages (1 new)', 'Messages (@count new)');
1478
  }
1479
  return t('Messages');
1480
}
1481

    
1482

    
1483
function _privatemsg_block_new() {
1484
  $block = array();
1485

    
1486
  if (!privatemsg_user_access()) {
1487
    return $block;
1488
  }
1489

    
1490
  $count = privatemsg_unread_count();
1491
  if ($count || variable_get('privatemsg_no_messages_notification', 0)) {
1492
    $block = array(
1493
      'subject' => $count ? format_plural($count, 'New message', 'New messages') : t('No new messages'),
1494
      'content' => theme('privatemsg_new_block', array('count' => $count)),
1495
    );
1496
    return $block;
1497
  }
1498
  return array();
1499
}
1500

    
1501
function _privatemsg_block_menu() {
1502
  $block = array();
1503

    
1504
  $links = array();
1505
  if (privatemsg_user_access('write privatemsg')) {
1506
    $links[] = l(t('Write new message'), 'messages/new');
1507
  }
1508
  if (privatemsg_user_access('read privatemsg') || privatemsg_user_access('read all private messages') ) {
1509
    $links[] = l(privatemsg_title_callback(), 'messages');
1510
  }
1511
  if (count($links)) {
1512
    $block = array(
1513
      'subject' => t('Private messages'),
1514
      'content' => theme('item_list', array('items' => $links)),
1515
    );
1516
  }
1517
  return $block;
1518
}
1519

    
1520
/**
1521
 * Delete or restore a message.
1522
 *
1523
 * @param $pmid
1524
 *   Message id, pm.mid field.
1525
 * @param $delete
1526
 *   Either deletes or restores the thread (1 => delete, 0 => restore)
1527
 * @param $account
1528
 *   User account for which the delete action should be carried out.
1529
 *   Set to NULL to delete for all users.
1530
 *
1531
 * @ingroup api
1532
 */
1533
function privatemsg_message_change_delete($pmid, $delete, $account = NULL) {
1534
  $delete_value = 0;
1535
  if ($delete == TRUE) {
1536
    $delete_value = REQUEST_TIME;
1537
  }
1538

    
1539
  $update = db_update('pm_index')
1540
    ->fields(array('deleted' => $delete_value))
1541
    ->condition('mid', $pmid);
1542
  if ($account) {
1543
    $update
1544
      ->condition('recipient', $account->uid)
1545
      ->condition('type', array('user', 'hidden'));
1546
  }
1547
  $update->execute();
1548
}
1549

    
1550
/**
1551
 * Send a new message.
1552
 *
1553
 * This functions does send a message in a new thread.
1554
 * Example:
1555
 * @code
1556
 * privatemsg_new_thread(array(user_load(5)), 'The subject', 'The body text');
1557
 * @endcode
1558
 *
1559
 * @param $recipients
1560
 *   Array of recipients (user objects)
1561
 * @param $subject
1562
 *   The subject of the new message
1563
 * @param $body
1564
 *   The body text of the new message
1565
 * @param $options
1566
 *   Additional options, possible keys:
1567
 *     author => User object of the author
1568
 *     timestamp => Time when the message was sent
1569
 *
1570
 * @return
1571
 *   An array with a key success. If TRUE, it also contains a key 'message' with
1572
 *   the created $message array, the same that is passed to the insert hook.
1573
 *   If FALSE, it contains a key 'messages'. This key contains an array where
1574
 *   the key is the error type (error, warning, notice) and an array with
1575
 *   messages of that type.
1576
 *
1577
 *   It is theoretically possible for success to be TRUE and message to be FALSE.
1578
 *   For example if one of the privatemsg database tables become corrupted. When testing
1579
 *   for success of message being sent it is always best to see if ['message'] is not FALSE
1580
 *   as well as ['success'] is TRUE.
1581
 *
1582
 *   Example:
1583
 *   @code
1584
 *   array('error' => array('A error message'))
1585
 *   @endcode
1586
 *
1587
 * @ingroup api
1588
 */
1589
function privatemsg_new_thread($recipients, $subject, $body = NULL, $options = array()) {
1590
  global $user;
1591
  $author = clone $user;
1592

    
1593
  $message = (object)$options;
1594
  $message->subject = $subject;
1595
  $message->body = $body;
1596
  // Make sure that recipients are keyed correctly and are not added
1597
  // multiple times.
1598
  foreach ($recipients as $recipient) {
1599
    if (!isset($recipient->type)) {
1600
      $recipient->type = 'user';
1601
      $recipient->recipient = $recipient->uid;
1602
    }
1603
    $message->recipients[privatemsg_recipient_key($recipient)] = $recipient;
1604
  }
1605

    
1606
  // Apply defaults - this will not overwrite existing keys.
1607
  if (!isset($message->author)) {
1608
    $message->author = $author;
1609
  }
1610
  if (!isset($message->timestamp)) {
1611
    $message->timestamp = REQUEST_TIME;
1612
  }
1613
  if (!isset($message->format)) {
1614
    $message->format = filter_default_format($message->author);
1615
  }
1616

    
1617
  $validated = _privatemsg_validate_message($message);
1618
  if ($validated['success']) {
1619
    $validated['message'] = _privatemsg_send($message);
1620
    if ($validated['message'] !== FALSE) {
1621
      _privatemsg_handle_recipients($validated['message']->mid, $validated['message']->recipients, FALSE);
1622
    }
1623
  }
1624

    
1625
  return $validated;
1626
}
1627

    
1628
/**
1629
 * Send a reply message
1630
 *
1631
 * This functions replies on an existing thread.
1632
 *
1633
 * @param $thread_id
1634
 *   Thread id
1635
 * @param $body
1636
 *   The body text of the new message
1637
 * @param $options
1638
 *   Additional options, possible keys:
1639
 *     author => User object of the author
1640
 *     timestamp => Time when the message was sent
1641
 *
1642
 * @return
1643
 *   An array with a key success and messages. This key contains an array where
1644
 *   the key is the error type (error, warning, notice) and an array with
1645
 *   messages of that type.. If success is TRUE, it also contains a key $message
1646
 *   with the created $message array, the same that is passed to
1647
 *   hook_privatemsg_message_insert().
1648
 *
1649
 *   It is theoretically possible for success to be TRUE and message to be FALSE.
1650
 *   For example if one of the privatemsg database tables become corrupted. When testing
1651
 *   for success of message being sent it is always best to see if ['message'] is not FALSE
1652
 *   as well as ['success'] is TRUE.
1653
 *
1654
 *   Example messages values:
1655
 *   @code
1656
 *   array('error' => array('A error message'))
1657
 *   @endcode
1658
 *
1659
 * @ingroup api
1660
 */
1661
function privatemsg_reply($thread_id, $body, $options = array()) {
1662
  global $user;
1663
  $author = clone $user;
1664

    
1665
  $message = (object)$options;
1666
  $message->body = $body;
1667

    
1668
  // Apply defaults - this will not overwrite existing keys.
1669
  if (!isset($message->author)) {
1670
    $message->author = $author;
1671
  }
1672
  if (!isset($message->timestamp)) {
1673
    $message->timestamp = REQUEST_TIME;
1674
  }
1675
  if (!isset($message->format)) {
1676
    $message->format = filter_default_format($message->author);
1677
  }
1678

    
1679
  // We don't know the subject and the recipients, so we need to load them..
1680
  // thread_id == mid on the first message of the thread
1681
  $first_message = privatemsg_message_load($thread_id, $message->author);
1682
  if (!$first_message) {
1683
    return array(
1684
      'success'  => FALSE,
1685
      'messages'   => array('error' => array(t('Thread %thread_id not found, unable to answer', array('%thread_id' => $thread_id)))),
1686
    );
1687
  }
1688

    
1689
  $message->thread_id = $thread_id;
1690
  // Load participants
1691
  $message->recipients = _privatemsg_load_thread_participants($thread_id, $message->author);
1692
  $message->subject = $first_message->subject;
1693
  $validated = _privatemsg_validate_message($message);
1694
  if ($validated['success']) {
1695
    $validated['message'] = _privatemsg_send($message);
1696
    if ($validated['message'] !== FALSE) {
1697
      _privatemsg_handle_recipients($validated['message']->mid, $validated['message']->recipients, FALSE);
1698
    }
1699
  }
1700
  return $validated;
1701
}
1702

    
1703
function _privatemsg_validate_message(&$message, $form = FALSE) {
1704
  $messages = array('error' => array(), 'warning' => array());
1705
  if (!(privatemsg_user_access('write privatemsg', $message->author) || (privatemsg_user_access('reply only privatemsg', $message->author) && isset($message->thread_id)))) {
1706
    // no need to do further checks in this case...
1707
    if ($form) {
1708
      form_set_error('author', t('You are not allowed to write messages.'));
1709
      return array(
1710
        'success'  => FALSE,
1711
        'messages'   => $messages,
1712
      );
1713
    }
1714
    else {
1715
      $messages['error'][] = t('@user is not allowed to write messages.', array('@user' => privatemsg_recipient_format($message->author, array('plain' => TRUE))));
1716
      return array(
1717
         'success'  => FALSE,
1718
         'messages'   => $messages,
1719
      );
1720
    }
1721
  }
1722

    
1723
  // Prevent subjects which only consist of a space as these can not be clicked.
1724
  $message->subject = trim($message->subject);
1725
  if (empty($message->subject)) {
1726
    if ($form) {
1727
      form_set_error('subject', t('You must include a subject line with your message.'));
1728
    }
1729
    else {
1730
      $messages['error'][] = t('A subject must be included with the message.');
1731
    }
1732
  }
1733

    
1734
  // Don't allow replies without a body.
1735
  if (!empty($message->thread_id) && ($message->body === NULL || $message->body === '') ) {
1736
    if ($form) {
1737
      form_set_error('body', t('You must include a message in your reply.'));
1738
    }
1739
    else {
1740
      $messages['error'][] = t('A message must be included in your reply.');
1741
    }
1742
  }
1743

    
1744
  // Check if an allowed format is used.
1745
  if (!filter_access(filter_format_load($message->format), $message->author)) {
1746
    if ($form) {
1747
      form_set_error('format', t('You are not allowed to use the specified format.'));
1748
    }
1749
    else {
1750
      $messages['error'][] = t('@user is not allowed to use the specified input format.', array('@user' => privatemsg_recipient_format($message->author, array('plain' => TRUE))));
1751
    }
1752
  }
1753

    
1754
  if (empty($message->recipients) || !is_array($message->recipients)) {
1755
    if ($form) {
1756
      form_set_error('recipient', t('You must include at least one valid recipient.'));
1757
    }
1758
    else {
1759
      $messages['error'][] = t('At least one valid recipient must be included with the message.');
1760
    }
1761
  }
1762

    
1763
  if (!empty($message->recipients) && is_array($message->recipients)) {
1764
    foreach (module_invoke_all('privatemsg_block_message', $message->author, $message->recipients, (array)$message) as $blocked) {
1765
      unset($message->recipients[$blocked['recipient']]);
1766
      if ($form) {
1767
        drupal_set_message($blocked['message'], 'warning');
1768
      }
1769
      else {
1770
        $messages['warning'][] = $blocked['message'];
1771
      }
1772
    }
1773
  }
1774

    
1775
  // Check again, give another error message if all recipients are blocked
1776
  if (empty($message->recipients)) {
1777
    if ($form) {
1778
      form_set_error('recipient', t('You are not allowed to send this message because all recipients are blocked.'));
1779
    }
1780
    else {
1781
      $messages['error'][] = t('The message cannot be sent because all recipients are blocked.');
1782
    }
1783
  }
1784

    
1785
  // Verify if message has tokens and user is allowed to use them.
1786
  $message->has_tokens = privatemsg_user_access('use tokens in privatemsg', $message->author) && count(token_scan($message->subject . $message->body));
1787

    
1788
  $messages = array_merge_recursive(module_invoke_all('privatemsg_message_validate', $message, $form), $messages);
1789

    
1790
  // Check if there are errors in $messages or if $form is TRUE, there are form errors.
1791
  $success = empty($messages['error']) || ($form && count((array)form_get_errors()) > 0);
1792
  return array(
1793
    'success'  => $success,
1794
    'messages'   => $messages,
1795
  );
1796
}
1797

    
1798
/**
1799
 * Internal function to save a message.
1800
 *
1801
 * @param $message
1802
 *   A $message array with the data that should be saved. If a thread_id exists
1803
 *   it will be created as a reply to an existing thread. If not, a new thread
1804
 *   will be created.
1805
 *
1806
 * @return
1807
 *   The updated $message array.
1808
 */
1809
function _privatemsg_send($message) {
1810
  $transaction = db_transaction();
1811
  try {
1812
    drupal_alter('privatemsg_message_presave', $message);
1813
    field_attach_presave('privatemsg_message', $message);
1814

    
1815
    $query = db_insert('pm_index')->fields(array('mid', 'thread_id', 'recipient', 'type', 'is_new', 'deleted'));
1816
    if (isset($message->read_all) && $message->read_all && isset($message->thread_id)) {
1817
      // The message was sent in read all mode, add the author as recipient to all
1818
      // existing messages.
1819
      $query_messages = _privatemsg_assemble_query('messages', array($message->thread_id), NULL);
1820
      foreach ($query_messages->execute()->fetchCol() as $mid) {
1821
        $query->values(array(
1822
          'mid' => $mid,
1823
          'thread_id' => $message->thread_id,
1824
          'recipient' => $message->author->uid,
1825
          'type' => 'user',
1826
          'is_new' => 0,
1827
          'deleted' => 0,
1828
        ));
1829
      }
1830
    }
1831

    
1832
    // 1) Save the message body first.
1833
    $args = array();
1834
    $args['subject'] = $message->subject;
1835
    $args['author'] = $message->author->uid;
1836
    $args['body'] = $message->body;
1837
    $args['format'] = $message->format;
1838
    $args['timestamp'] = $message->timestamp;
1839
    $args['has_tokens'] = (int)$message->has_tokens;
1840
    $mid = db_insert('pm_message')
1841
      ->fields($args)
1842
      ->execute();
1843
    $message->mid = $mid;
1844

    
1845
    // Thread ID is the same as the mid if it's the first message in the thread.
1846
    if (!isset($message->thread_id)) {
1847
      $message->thread_id = $mid;
1848
    }
1849

    
1850
    // 2) Save message to recipients.
1851
    // Each recipient gets a record in the pm_index table.
1852
    foreach ($message->recipients as $recipient) {
1853
      $query->values(array(
1854
        'mid' => $mid,
1855
        'thread_id' => $message->thread_id,
1856
        'recipient' => $recipient->recipient,
1857
        'type' => $recipient->type,
1858
        'is_new' => 1,
1859
        'deleted' => 0,
1860
      ));
1861
    }
1862

    
1863
    // We only want to add the author to the pm_index table, if the message has
1864
    // not been sent directly to him.
1865
    if (!isset($message->recipients['user_' . $message->author->uid])) {
1866
      $query->values(array(
1867
        'mid' => $mid,
1868
        'thread_id' => $message->thread_id,
1869
        'recipient' => $message->author->uid,
1870
        'type' => 'user',
1871
        'is_new' => 0,
1872
        'deleted' => 0,
1873
      ));
1874
    }
1875
    $query->execute();
1876

    
1877
    module_invoke_all('privatemsg_message_insert', $message);
1878
    field_attach_insert('privatemsg_message', $message);
1879

    
1880
  } catch (Exception $exception) {
1881
    $transaction->rollback();
1882
    watchdog_exception('privatemsg', $exception);
1883
    throw $exception;
1884
  }
1885

    
1886
  // If we reached here that means we were successful at writing all messages to db.
1887
  return $message;
1888
}
1889

    
1890
/**
1891
 * Returns a link to send message form for a specific users.
1892
 *
1893
 * Contains permission checks of author/recipient, blocking and
1894
 * if a anonymous user is involved.
1895
 *
1896
 * @param $recipient
1897
 *   Recipient of the message
1898
 * @param $account
1899
 *   Sender of the message, defaults to the current user
1900
 *
1901
 * @return
1902
 *   Either FALSE or a URL string
1903
 *
1904
 * @ingroup api
1905
 */
1906
function privatemsg_get_link($recipients, $account = array(), $subject = NULL) {
1907
  if ($account == NULL) {
1908
    global $user;
1909
    $account = $user;
1910
  }
1911

    
1912
  if (!is_array($recipients)) {
1913
    $recipients = array($recipients);
1914
  }
1915

    
1916
  if (!privatemsg_user_access('write privatemsg', $account) || $account->uid == 0) {
1917
    return FALSE;
1918
  }
1919

    
1920
  $validated = array();
1921
  foreach ($recipients as $recipient) {
1922
    if (!privatemsg_user_access('read privatemsg', $recipient)) {
1923
      continue;
1924
    }
1925
    if (variable_get('privatemsg_display_link_self', TRUE) == FALSE && $account->uid == $recipient->uid) {
1926
      continue;
1927
    }
1928
    if (count(module_invoke_all('privatemsg_block_message', $account, array(privatemsg_recipient_key($recipient) => $recipient))) > 0) {
1929
      continue;
1930
    }
1931
    $validated[] = $recipient->uid;
1932
  }
1933
  if (empty($validated)) {
1934
    return FALSE;
1935
  }
1936
  $url = 'messages/new/' . implode(',', $validated);
1937
  if (!is_null($subject)) {
1938
    // Explicitly encode the / so that it will be encoded twice to work around
1939
    // the the menu_system.
1940
    $url .= '/' . str_replace('/', '%2F', $subject);
1941
  }
1942
  return $url;
1943
}
1944

    
1945
/**
1946
 * Load a single message.
1947
 *
1948
 * @param $pmid
1949
 *   Message id, pm.mid field
1950
 * @param $account
1951
 *   For which account the message should be loaded.
1952
 *   Defaults to the current user.
1953
 *
1954
 * @ingroup api
1955
 */
1956
function privatemsg_message_load($pmid, $account = NULL) {
1957
  // If $pmid is object or array - do nothing
1958
  // (fixing conflict with message_load() function in message module).
1959
  if(is_array($pmid) || is_object($pmid)) {
1960
    return NULL;
1961
  }
1962
  $conditions = array();
1963
  if ($account) {
1964
    $conditions['account'] = $account;
1965
  }
1966
  $messages = privatemsg_message_load_multiple(array($pmid), $conditions);
1967
  return current($messages);
1968
}
1969

    
1970
/**
1971
 * Load multiple messages.
1972
 *
1973
 * @param $pmids
1974
 *   Array of Message ids, pm.mid field
1975
 * @param $account
1976
 *   For which account the message should be loaded.
1977
 *   Defaults to the current user.
1978
 *
1979
 * @ingroup api
1980
 */
1981
function privatemsg_message_load_multiple(array $pmids, array $conditions = array(), $reset = FALSE) {
1982
  $result = entity_load('privatemsg_message', $pmids, $conditions, $reset);
1983
  return $result;
1984
}
1985

    
1986
/**
1987
 * Generates a query based on a query id.
1988
 *
1989
 * @param $query
1990
 *   Either be a string ('some_id') or an array('group_name', 'query_id'),
1991
 *   if a string is supplied, group_name defaults to 'privatemsg'.
1992
 *
1993
 * @return SelectQuery
1994
 *    Array with the keys query and count. count can be used to count the
1995
 *    elements which would be returned by query. count can be used together
1996
 *    with pager_query().
1997
 *
1998
 * @ingroup sql
1999
 */
2000
function _privatemsg_assemble_query($query) {
2001

    
2002
  // Modules will be allowed to choose the prefix for the query builder,
2003
  // but if there is not one supplied, 'privatemsg' will be taken by default.
2004
  if (is_array($query)) {
2005
    $query_id = $query[0];
2006
    $query_group = $query[1];
2007
  }
2008
  else {
2009
    $query_id = $query;
2010
    $query_group = 'privatemsg';
2011
  }
2012

    
2013
  /**
2014
   * Begin: dynamic arguments
2015
   */
2016
  $args = func_get_args();
2017
  unset($args[0]);
2018
  // We do the merge because we call call_user_func_array and not drupal_alter.
2019
  // This is necessary because otherwise we would not be able to use $args
2020
  // correctly (otherwise it doesn't unfold).
2021
  $query_function = $query_group . '_sql_' . $query_id;
2022
  if (!function_exists($query_function)) {
2023
    drupal_set_message(t('Query function %function does not exist', array('%function' => $query_function)), 'error');
2024
    return FALSE;
2025
  }
2026
  $query = call_user_func_array($query_function, $args);
2027
  // Add a tag to make it alterable.
2028
  $query->addTag($query_group . '_' . $query_id);
2029

    
2030
  // Add arguments as metadata.
2031
  foreach ($args as $id => $arg) {
2032
    $query->addMetaData('arg_' . $id, $arg);
2033
  }
2034

    
2035
  return $query;
2036
}
2037

    
2038
/**
2039
 * Marks one or multiple threads as (un)read.
2040
 *
2041
 * @param $threads
2042
 *   Array with thread id's or a single thread id.
2043
 * @param $status
2044
 *   Either PRIVATEMSG_READ or PRIVATEMSG_UNREAD, sets the new status.
2045
 * @param $account
2046
 *   User object for which the threads should be deleted, defaults to the current user.
2047
 */
2048
function privatemsg_thread_change_status($threads, $status, $account = NULL) {
2049
  if (!is_array($threads)) {
2050
    $threads = array($threads);
2051
  }
2052
  if (empty($account)) {
2053
    global $user;
2054
    $account = clone $user;
2055
  }
2056
  // Merge status and uid with the existing thread list.
2057
  $params = array(
2058
    ':status' => $status,
2059
    ':recipient' => $account->uid,
2060
    ':threads' => $threads,
2061
  );
2062

    
2063
  // Record which messages will change status.
2064
  $result = db_query("SELECT mid FROM {pm_index} WHERE is_new <> :status AND recipient = :recipient and type IN ('user', 'hidden') AND thread_id IN (:threads)", $params);
2065
  $changed = $result->fetchCol();
2066

    
2067
  // Update the status of the threads.
2068
  db_update('pm_index')
2069
    ->fields(array('is_new' => $status))
2070
    ->condition('thread_id', $threads)
2071
    ->condition('recipient', $account->uid)
2072
    ->condition('type', array('user', 'hidden'))
2073
    ->execute();
2074

    
2075
  // Allow modules to respond to the status changes.
2076
  foreach ($changed as $mid) {
2077
    module_invoke_all('privatemsg_message_status_changed', $mid, $status, $account);
2078
  }
2079

    
2080
  if ($status == PRIVATEMSG_UNREAD) {
2081
    drupal_set_message(format_plural(count($threads), 'Marked 1 thread as unread.', 'Marked @count threads as unread.'));
2082
  }
2083
  else {
2084
    drupal_set_message(format_plural(count($threads), 'Marked 1 thread as read.', 'Marked @count threads as read.'));
2085
  }
2086
}
2087
/**
2088
 * Returns a table header definition based on the submitted keys.
2089
 *
2090
 * Uses @link theming theme patterns @endlink to theme single headers.
2091
 *
2092
 * @param $has_posts
2093
 *   TRUE when there is at least one row. Decides if the select all checkbox should be displayed.
2094
 * @param $keys
2095
 *   Array with the keys which are present in the query/should be displayed.
2096
 * @return
2097
 *   Array with header definitions for tablesort_sql and theme('table').
2098
 */
2099
function _privatemsg_list_headers($keys) {
2100

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

    
2104
  $header = array();
2105
  foreach ($keys as $key) {
2106
    // First, try to load a specific theme for that header, if not present, use the default.
2107
    if ($return = theme('privatemsg_list_header__' . $key)) {
2108
      // The default theme returns nothing, only store the value if we have something.
2109
      $header[$key] = $return;
2110
    }
2111
  }
2112
  uasort($header, 'element_sort');
2113
  // Remove weight column or it will show up in the markup.
2114
  foreach ($header as $key => $element) {
2115
    if (isset($header[$key]['#weight'])) {
2116
      unset($header[$key]['#weight']);
2117
    }
2118
  }
2119
  return $header;
2120
}
2121

    
2122
/**
2123
 * Formats all rows (#options) in the privatemsg tableselect thread list.
2124
 *
2125
 * Uses @link theming theme patterns @endlink to theme single fields.
2126
 *
2127
 * @param $thread
2128
 *   Array with the row data returned by the database.
2129
 * @return
2130
 *   Row definition for use with theme('table')
2131
 */
2132
function _privatemsg_list_thread($tableselect) {
2133
  foreach ($tableselect['#options'] as $id => $thread) {
2134
    $row = array();
2135
    if (!empty($thread['is_new'])) {
2136
      // Set the css class in the tr tag.
2137
      $row['#attributes']['class'][] = 'privatemsg-unread';
2138
    }
2139
    foreach ($thread as $key => $data) {
2140
      // First, try to load a specific theme for that field, if not present, use the default.
2141
      if ($return = theme('privatemsg_list_field__' . $key, array('thread' => $thread))) {
2142
        // The default theme returns nothing, only store the value if we have something.
2143
        $row[$key] = $return;
2144
      }
2145
    }
2146
    $tableselect['#options'][$id] = $row;
2147
  }
2148
  return $tableselect;
2149
}
2150

    
2151
/**
2152
 * Execute an operation on a number of threads.
2153
 *
2154
 * @param $operation
2155
 *   The operation that should be executed.
2156
 *   @see hook_privatemsg_thread_operations()
2157
 * @param $threads
2158
 *   An array of thread ids. The array is filtered before used, a checkboxes
2159
 *   array can be directly passed to it.
2160
 */
2161
function privatemsg_operation_execute($operation, $threads, $account = NULL) {
2162
  // Filter out unchecked threads, this gives us an array of "checked" threads.
2163
  $threads = array_filter($threads);
2164

    
2165
  if (empty($threads)) {
2166
    // Do not execute anything if there are no checked threads.
2167
    drupal_set_message(t('You must first select one (or more) messages before you can take that action.'), 'warning');
2168
    return FALSE;
2169
  }
2170
  // Add in callback arguments if present.
2171
  if (isset($operation['callback arguments'])) {
2172
    $args = array_merge(array($threads), $operation['callback arguments']);
2173
  }
2174
  else {
2175
    $args = array($threads);
2176
  }
2177

    
2178
  // Add the user object to the arguments.
2179
  if ($account) {
2180
    $args[] = $account;
2181
  }
2182

    
2183
  // Execute the chosen action and pass the defined arguments.
2184
  call_user_func_array($operation['callback'], $args);
2185

    
2186
  if (!empty($operation['success message'])) {
2187
    drupal_set_message($operation['success message']);
2188
  }
2189

    
2190
  // Check if that operation has defined an undo callback.
2191
  if (isset($operation['undo callback']) && $undo_function = $operation['undo callback']) {
2192
    // Add in callback arguments if present.
2193
    if (isset($operation['undo callback arguments'])) {
2194
      $undo_args = array_merge(array($threads), $operation['undo callback arguments']);
2195
    }
2196
    else {
2197
      $undo_args = array($threads);
2198
    }
2199

    
2200
    // Avoid saving the complete user object in the session.
2201
    if ($account) {
2202
      $undo_args['account'] = $account->uid;
2203
    }
2204
    // Store the undo callback in the session and display a "Undo" link.
2205
    // @todo: Provide a more flexible solution for such an undo action, operation defined string for example.
2206
    $_SESSION['privatemsg']['undo callback'] = array('function' => $undo_function, 'args' => $undo_args);
2207
    $undo = url('messages/undo/action', array('query' => drupal_get_destination()));
2208

    
2209
    drupal_set_message(t('The previous action can be <a href="!undo">undone</a>.', array('!undo' => $undo)));
2210
  }
2211

    
2212
  // Allow modules to respond to the operation.
2213
  module_invoke_all('privatemsg_operation_executed', $operation, $threads, $account);
2214

    
2215
  return TRUE;
2216
}
2217

    
2218
/**
2219
 * Delete or restore one or multiple threads.
2220
 *
2221
 * @param $threads
2222
 *   Array with thread id's or a single thread id.
2223
 * @param $delete
2224
 *   Indicates if the threads should be deleted or restored. 1 => delete, 0 => restore.
2225
 * @param $account
2226
 *   User account for which the delete action should be carried out - Set to NULL to delete for all users.
2227
 */
2228
function privatemsg_thread_change_delete($threads, $delete, $account = NULL) {
2229
  if (!is_array($threads)) {
2230
    $threads = array($threads);
2231
  }
2232
  if (empty($account)) {
2233
    global $user;
2234
    $account = clone $user;
2235
  }
2236

    
2237
  // Load all messages of those threads including the deleted.
2238
  $query = _privatemsg_assemble_query('messages', $threads, $account, TRUE);
2239

    
2240
  // Delete each message. We need to do that to trigger the delete hook.
2241
  foreach ($query->execute() as $row) {
2242
    privatemsg_message_change_delete($row->mid, $delete, $account);
2243
  }
2244

    
2245
  if ($delete) {
2246
    drupal_set_message(format_plural(count($threads), 'Deleted 1 thread.', 'Deleted @count threads.'));
2247
  }
2248
  else {
2249
    drupal_set_message(format_plural(count($threads), 'Restored 1 thread.', 'Restored @count threads.'));
2250
  }
2251
}
2252

    
2253
/**
2254
 * Implements hook_privatemsg_thread_operations().
2255
 */
2256
function privatemsg_privatemsg_thread_operations() {
2257
  $operations = array(
2258
    'mark as read' => array(
2259
      'label' => t('Mark as read'),
2260
      'callback' => 'privatemsg_thread_change_status',
2261
      'callback arguments' => array('status' => PRIVATEMSG_READ),
2262
      'undo callback' => 'privatemsg_thread_change_status',
2263
      'undo callback arguments' => array('status' => PRIVATEMSG_UNREAD),
2264
    ),
2265
    'mark as unread' => array(
2266
      'label' => t('Mark as unread'),
2267
      'callback' => 'privatemsg_thread_change_status',
2268
      'callback arguments' => array('status' => PRIVATEMSG_UNREAD),
2269
      'undo callback' => 'privatemsg_thread_change_status',
2270
      'undo callback arguments' => array('status' => PRIVATEMSG_READ),
2271
    ),
2272
  );
2273
  if (privatemsg_user_access('delete privatemsg')) {
2274
    $operations['delete'] = array(
2275
      'label' => t('Delete'),
2276
      'callback' => 'privatemsg_thread_change_delete',
2277
      'callback arguments' => array('delete' => 1),
2278
      'undo callback' => 'privatemsg_thread_change_delete',
2279
      'undo callback arguments' => array('delete' => 0),
2280
      'button' => TRUE,
2281
    );
2282
  }
2283
  return $operations;
2284
}
2285

    
2286
/**
2287
 * Implements hook_entity_info().
2288
 */
2289
function privatemsg_entity_info() {
2290
  return array(
2291
    'privatemsg_message' => array(
2292
      'label' => t('Privatemsg'),
2293
      'base table' => 'pm_message',
2294
      'fieldable' => TRUE,
2295
      'controller class' => 'PrivatemsgMessageController',
2296
      'uri callback' => 'privatemsg_message_uri_callback',
2297
      'entity keys' => array(
2298
        'id' => 'mid',
2299
      ),
2300
      'bundles' => array(
2301
        'privatemsg_message' => array(
2302
          'label' => t('Private message'),
2303
          'admin' => array(
2304
            'path' => 'admin/config/messaging/privatemsg',
2305
            'access arguments' => array('administer privatemsg settings'),
2306
          ),
2307
        ),
2308
      ),
2309
    ),
2310
  );
2311
}
2312

    
2313
/**
2314
 * Returns the URI for a private message.
2315
 *
2316
 * @param $message
2317
 *   Private message object.
2318
 *
2319
 * @return
2320
 *   URI array as defined by hook_entity_info().
2321
 */
2322
function privatemsg_message_uri_callback($message) {
2323
  $uri = array();
2324
  if (isset($message->mid) && isset($message->thread_id)) {
2325
    $uri = array(
2326
      'path' => 'messages/view/' . $message->thread_id,
2327
      'options' => array(),
2328
    );
2329
    // Add message fragment, if necessary.
2330
    if ($message->mid != $message->thread_id) {
2331
      $uri['options']['fragment'] = 'privatemsg-mid-' . $message->mid;
2332
    }
2333
  }
2334
  return $uri;
2335
}
2336

    
2337
/**
2338
 * Implements hook_build_modes().
2339
 */
2340
function privatemsg_build_modes($obj_type) {
2341
  $modes = array();
2342
  if ($obj_type == 'privatemsg_message') {
2343
    $modes['message'] = t('Message');
2344
  }
2345
  return $modes;
2346
}
2347

    
2348
/**
2349
 * Implements hook_node_view().
2350
 */
2351
function privatemsg_node_view($node, $view_mode) {
2352
  $types = array_filter(variable_get('privatemsg_link_node_types', array()));
2353
  if (in_array($node->type, $types) && ($view_mode == 'full' || (variable_get('privatemsg_display_on_teaser', 1) && $view_mode == 'teaser'))) {
2354
    $url = privatemsg_get_link(user_load($node->uid));
2355
    if (!empty($url)){
2356
      $node->content['links']['#links']['privatemsg_link'] = array(
2357
        'title' => t('Send author a message'),
2358
        'href' => $url . '/' . t('Message regarding @node', array('@node' => $node->title)),
2359
        'query' => drupal_get_destination(),
2360
        'attributes' => array('class' => 'privatemsg-send-link privatemsg-send-link-node'),
2361
      );
2362
    }
2363
  }
2364
}
2365

    
2366
/**
2367
 * Implements hook_comment_view().
2368
 */
2369
function privatemsg_comment_view($comment) {
2370
  $types = array_filter(variable_get('privatemsg_link_node_types', array()));
2371
  if (in_array(node_load($comment->nid)->type, $types) && variable_get('privatemsg_display_on_comments', 0)) {
2372
    $url = privatemsg_get_link(user_load($comment->uid));
2373
    if (!empty($url)){
2374
      $links['privatemsg_link'] = array(
2375
        'title' => t('Send author a message'),
2376
        'href' => $url . '/' . t('Message regarding @comment', array('@comment' => trim($comment->subject))),
2377
        'query' => drupal_get_destination(),
2378
      );
2379
      $comment->content['links']['privatemsg'] = array(
2380
        '#theme' => 'links',
2381
        '#links' => $links,
2382
        '#attributes' => array('class' => array('privatemsg-send-link', 'privatemsg-send-link-node', 'links', 'inline')),
2383
      );
2384
    }
2385
  }
2386
}
2387
/**
2388
 * Implements hook_views_api().
2389
 */
2390
function privatemsg_views_api() {
2391
  return array(
2392
    'api' => 2,
2393
    'path' => drupal_get_path('module', 'privatemsg') . '/views',
2394
  );
2395
}
2396

    
2397
/**
2398
 * Privatemsg wrapper function for user_load_multiple().
2399
 *
2400
 * The function adds the privatemsg specific recipient id (uid)
2401
 * and recipient type to the user object.
2402
 *
2403
 * @param $uid
2404
 *   Which uid to load. Can either be a single id or an array of uids.
2405
 * @return
2406
 *   If existing for the passed in uid, the user object with the recipient
2407
 *   and type properties.
2408
 */
2409
function privatemsg_user_load_multiple($uids) {
2410
  $accounts = array();
2411
  foreach (user_load_multiple($uids) as $account) {
2412
    $account->recipient = $account->uid;
2413
    $account->type = 'user';
2414
    $accounts[privatemsg_recipient_key($account)] = $account;
2415
  }
2416
  return $accounts;
2417
}
2418

    
2419
/**
2420
 * Return key for a recipient object used for arrays.
2421
 * @param $recipient
2422
 *   Recipient object, must have type and recipient properties.
2423
 * @return
2424
 *   A string that looks like type_id.
2425
 *
2426
 * @ingroup types
2427
 */
2428
function privatemsg_recipient_key($recipient) {
2429
  if (empty($recipient->type)) {
2430
    return 'user_' . $recipient->uid;
2431
  }
2432
  return $recipient->type . '_' . $recipient->recipient;
2433
}
2434

    
2435
/**
2436
 * Returns an array of defined recipient types.
2437
 *
2438
 * @return
2439
 *   Array of recipient types
2440
 * @see hook_privatemsg_recipient_type_info()
2441
 *
2442
 * @ingroup types
2443
 */
2444
function privatemsg_recipient_get_types() {
2445
  $types = &drupal_static(__FUNCTION__, NULL);
2446

    
2447
  if ($types === NULL) {
2448
    $types = module_invoke_all('privatemsg_recipient_type_info');
2449
    if (!is_array($types)) {
2450
      $types = array();
2451
    }
2452
    drupal_alter('privatemsg_recipient_type_info', $types);
2453
    uasort($types, 'element_sort');
2454
  }
2455
  return $types;
2456
}
2457
/**
2458
 * Return a single recipient type information.
2459
 * @param $type
2460
 *   Name of the recipient type.
2461
 * @return
2462
 *   Array with the recipient type definition. NULL if the type doesn't exist.
2463
 *
2464
 * @ingroup types
2465
 */
2466
function privatemsg_recipient_get_type($type) {
2467
  $types = privatemsg_recipient_get_types();
2468
  if (!is_string($type)) {
2469
    exit;
2470
  }
2471
  if (isset($types[$type])) {
2472
    return $types[$type];
2473
  }
2474
}
2475

    
2476
/**
2477
 * Add or remove a recipient to an existing message.
2478
 *
2479
 * @param $mid
2480
 *   Message id for which the recipient should be added.
2481
 * @param $recipient
2482
 *   Recipient id that should be added, for example uid.
2483
 * @param $type
2484
 *   Type of the recipient, defaults to hidden.
2485
 * @param $add
2486
 *   If TRUE, adds the recipient, if FALSE, removes it.
2487
 */
2488
function privatemsg_message_change_recipient($mid, $uid, $type = 'user', $add = TRUE) {
2489
  // The message is statically cached, so only a single load is necessary.
2490
  $message = privatemsg_message_load($mid);
2491
  $thread_id = $message->thread_id;
2492
  if ($add) {
2493
    // Only add the recipient if he does not block the author.
2494
    $recipient = user_load($uid);
2495
    $context = ($thread_id == $mid) ? array() : array('thread_id' => $thread_id);
2496
    $user_blocked = module_invoke_all('privatemsg_block_message', $message->author, array(privatemsg_recipient_key($recipient) => $recipient), $context);
2497
    if (count($user_blocked) <> 0) {
2498
      return;
2499
    }
2500

    
2501
    // Make sure to only add a recipient once. The types user and hidden are
2502
    // considered equal here.
2503
    $query = db_select('pm_index', 'pmi');
2504
    $query->addExpression('1');
2505
    $exists = $query
2506
      ->condition('mid', $mid)
2507
      ->condition('recipient', $uid)
2508
      ->condition('type', ($type == 'user' || $type == 'hidden') ? array('user', 'hidden') : $type)
2509
      ->execute()
2510
      ->fetchField();
2511
    if (!$exists) {
2512
      db_insert('pm_index')
2513
        ->fields(array(
2514
          'mid' => $mid,
2515
          'thread_id' => $thread_id,
2516
          'recipient' => $uid,
2517
          'type' => $type,
2518
          'is_new' => 1,
2519
          'deleted' => 0,
2520
        ))
2521
        ->execute();
2522
    }
2523
  }
2524
  else {
2525
    db_delete('pm_index')
2526
      >condition('mid', $mid)
2527
      ->condition('recipient', $uid)
2528
      ->condition('type', $type)
2529
      ->execute();
2530
  }
2531
  module_invoke_all('privatemsg_message_recipient_changed', $mid, $thread_id, $uid, $type, $add);
2532
}
2533

    
2534
/**
2535
 * Handle the non-user recipients of a new message.
2536
 *
2537
 * Either process them directly if they have less than a certain amount of users
2538
 * or, if enabled, add them to a batch.
2539
 *
2540
 * @param $mid
2541
 *   Message id for which the recipients are processed.
2542
 * @param $recipients
2543
 *   Array of recipients.
2544
 * @param $use_batch
2545
 *   Use batch API to process recipients.
2546
 */
2547
function _privatemsg_handle_recipients($mid, $recipients, $use_batch = TRUE) {
2548
  $batch = array(
2549
    'title' => t('Processing recipients'),
2550
    'operations' => array(),
2551
    'file' => drupal_get_path('module', 'privatemsg') . '/privatemsg.pages.inc',
2552
    'progress_message' =>  t('Processing recipients'),
2553
  );
2554
  $small_threshold = variable_get('privatemsg_recipient_small_threshold', 100);
2555
  foreach ($recipients as $recipient) {
2556
    // Add a batch operation to press non-user recipient types.
2557
    if ($recipient->type != 'user' && $recipient->type != 'hidden') {
2558
      $type = privatemsg_recipient_get_type($recipient->type);
2559
      // Count the recipients, if there are less than small_threshold, process
2560
      // them right now.
2561
      $count_function = $type['count'];
2562
      if (!is_callable($count_function)) {
2563
        db_update('pm_index')
2564
          ->fields(array('is_new' => PRIVATEMSG_READ))
2565
          ->condition('mid', $mid)
2566
          ->condition('recipient', $recipient->recipient)
2567
          ->condition('type', $recipient->type)
2568
          ->execute();
2569
        drupal_set_message(t('Recipient type %type is not correctly implemented', array('%type' => $recipient->type)), 'error');
2570
        continue;
2571
      }
2572
      $count = $count_function($recipient);
2573
      if ($count < $small_threshold) {
2574
        $load_function = $type['generate recipients'];
2575
        if (!is_callable($load_function)) {
2576
          db_update('pm_index')
2577
            ->fields(array('is_new' => PRIVATEMSG_READ))
2578
            ->condition('mid', $mid)
2579
            ->condition('recipient', $recipient->recipient)
2580
            ->condition('type', $recipient->type)
2581
            ->execute();
2582
          drupal_set_message(t('Recipient type %type is not correctly implemented', array('%type' => $recipient->type)), 'error');
2583
          continue;
2584
        }
2585
        $uids = $load_function($recipient, $small_threshold, 0);
2586
        if (!empty($uids)) {
2587
          foreach ($uids as $uid) {
2588
            privatemsg_message_change_recipient($mid, $uid, 'hidden');
2589
          }
2590
        }
2591
        db_update('pm_index')
2592
          ->fields(array('is_new' => PRIVATEMSG_READ))
2593
          ->condition('mid', $mid)
2594
          ->condition('recipient', $recipient->recipient)
2595
          ->condition('type', $recipient->type)
2596
          ->execute();
2597
        continue;
2598
      }
2599
      if ($use_batch) {
2600
        $batch['operations'][] = array('privatemsg_load_recipients', array($mid, $recipient));
2601
      }
2602
    }
2603
  }
2604

    
2605
  // Set batch if there are outstanding operations.
2606
  if ($use_batch && !empty($batch['operations'])) {
2607
    batch_set($batch);
2608
  }
2609
}
2610

    
2611
/**
2612
 * This function is used to test if the current user has write/view access
2613
 * for a specific recipient type.
2614
 *
2615
 * @param $type_name
2616
 *   The name of the recipient type.
2617
 * @param $permission
2618
 *   Which permission should be checked: 'write' or 'view'.
2619
 * @param $recipient
2620
 *   Optionally pass in a recipient for which the permission should be checked.
2621
 *   This only has effect if a the recipient type defines a callback function
2622
 *   and is simply passed through in that case.
2623
 *
2624
 * @return
2625
 *   TRUE if the user has that permission (or not permission is defined) and
2626
 *   FALSE if not.
2627
 *
2628
 * @ingroup types
2629
 */
2630
function privatemsg_recipient_access($type_name, $permission, $recipient = NULL) {
2631
  if (($type = privatemsg_recipient_get_type($type_name))) {
2632
    // First check if a callback function is defined.
2633
    if (!empty($type[$permission . ' callback']) && is_callable($type[$permission . ' callback'])) {
2634
      $callback = $type[$permission . ' callback'];
2635
      return $callback($recipient);
2636
    }
2637

    
2638
    if (isset($type[$permission . ' access'])) {
2639
      if (is_bool($type[$permission . ' access'])) {
2640
        return $type[$permission . ' access'];
2641
      }
2642
      return user_access($type[$permission . ' access']);
2643
    }
2644
  }
2645
  // If no access permission is defined, access is allowed.
2646
  return TRUE;
2647
}
2648

    
2649
/**
2650
 * Format a single participant.
2651
 *
2652
 * @param $participant
2653
 *   The participant object to format.
2654
 *
2655
 * @ingroup types.
2656
 */
2657
function privatemsg_recipient_format($recipient, $options = array()) {
2658
  if (!isset($recipient->type)) {
2659
    $recipient->type = 'user';
2660
    $recipient->recipient = $recipient->uid;
2661
  }
2662
  $type = privatemsg_recipient_get_type($recipient->type);
2663
  if (isset($type['format'])) {
2664
    return theme($type['format'], array('recipient' => $recipient, 'options' => $options));
2665
  }
2666
  return NULL;
2667
}
2668

    
2669
/**
2670
 * Implements hook_privatemsg_recipient_type_info().
2671
 */
2672
function privatemsg_privatemsg_recipient_type_info() {
2673
  return array(
2674
    'user' => array(
2675
      'name' => t('User'),
2676
      'description' => t('Enter a user name to write a message to a user.'),
2677
      'load' => 'privatemsg_user_load_multiple',
2678
      'format' => 'privatemsg_username',
2679
      'autocomplete' => 'privatemsg_user_autocomplete',
2680
      // Make sure this comes always last.
2681
      '#weight' => 50,
2682
    ),
2683
  );
2684
}
2685

    
2686
/**
2687
 * Implements callback_recipient_autocomplete().
2688
 */
2689
function privatemsg_user_autocomplete($fragment, $names, $limit) {
2690
  // First, load all possible uids.
2691
  $uids = _privatemsg_assemble_query('autocomplete', $fragment, $names)
2692
    ->range(0, $limit)
2693
    ->execute()
2694
    ->fetchCol();
2695
  $query = _privatemsg_assemble_query('autocomplete', $fragment, $names);
2696
  $query->preExecute();
2697
  $query->getArguments();
2698
  // Load the corresponding users, make sure to not load any duplicates.
2699
  $accounts = user_load_multiple(array_unique($uids));
2700

    
2701
  // Return them in an array with the correct recipient key.
2702
  $suggestions = array();
2703
  foreach ($accounts as $account) {
2704
    $account->type = 'user';
2705
    $account->recipient = $account->uid;
2706
    $suggestions[privatemsg_recipient_key($account)] = $account;
2707
  }
2708
  return $suggestions;
2709

    
2710
}
2711

    
2712
/**
2713
 * Implements hook_field_extra_fields().
2714
 */
2715
function privatemsg_field_extra_fields() {
2716
  $extra['user']['user'] = array(
2717
    'form' => array(
2718
      'privatemsg' => array(
2719
        'label' => 'Private msg',
2720
        'description' => t('Private messages'),
2721
        'weight' => 5,
2722
      ),
2723
    ),
2724
    'display' => array(
2725
      'privatemsg_send_new_message' => array(
2726
        'label' => 'Private msg',
2727
        'description' => t('Private messages'),
2728
        'weight' => 5,
2729
      ),
2730
    ),
2731
  );
2732
  $extra['privatemsg_message']['privatemsg_message'] = array(
2733
    'form' => array(
2734
      'recipient' => array(
2735
        'label' => t('To'),
2736
        'description' => t('Recipient field'),
2737
        'weight' => -10,
2738
      ),
2739
      'subject' => array(
2740
        'label' => t('Message subject'),
2741
        'description' => t('Message subject'),
2742
        'weight' => -5,
2743
      ),
2744
      'body' => array(
2745
        'label' => t('Message body'),
2746
        'description' => t('Message body'),
2747
        'weight' => -3,
2748
      ),
2749
      'token' => array(
2750
        'label' => t('Token browser'),
2751
        'description' => t('Displays usable tokens in a table for those who are allowed to use tokens in private messages.'),
2752
        'weight' => -1,
2753
      ),
2754
    ),
2755
    'display' => array(
2756
      'body' => array(
2757
        'label' => t('Message body'),
2758
        'description' => t('Message body'),
2759
        'weight' => -4,
2760
      ),
2761
    )
2762
  );
2763
  return $extra;
2764
}
2765

    
2766
/**
2767
 * Implements hook_file_download_access().
2768
 */
2769
function privatemsg_file_download_access($field, $entity_type, $entity) {
2770
  global $user;
2771

    
2772
  if ($entity_type == 'privatemsg_message') {
2773
    // Users with read all private messages permission can view all files too.
2774
    if (user_access('read all private messages')) {
2775
      return TRUE;
2776
    }
2777

    
2778
    // Check if user is a recipient of this message.
2779
    return (bool)db_query_range("SELECT 1 FROM {pm_index} WHERE recipient = :uid AND type IN ('user', 'hidden') AND mid = :mid", 0, 1, array(':uid' => $user->uid, ':mid' => $entity->mid))->fetchField();
2780
  }
2781
}
2782

    
2783
/**
2784
 * Implements hook_token_info().
2785
 */
2786
function privatemsg_token_info() {
2787
  $type = array(
2788
    'name' => t('Private message'),
2789
    'description' => t('Tokens related to private messages.'),
2790
    'needs-data' => 'privatemsg_message',
2791
  );
2792

    
2793
  // Tokens for private messages.
2794
  $message['mid'] = array(
2795
    'name' => t("Message ID"),
2796
    'description' => t("The unique ID of the message."),
2797
  );
2798
  $message['thread-id'] = array(
2799
    'name' => t("Thread ID"),
2800
    'description' => t("The unique ID of the thread."),
2801
  );
2802
  $message['url'] = array(
2803
    'name' => t("URL"),
2804
    'description' => t("URL that points to the message."),
2805
  );
2806
  $message['subject'] = array(
2807
    'name' => t("Subject"),
2808
    'description' => t("The subject of the message."),
2809
  );
2810
  $message['body'] = array(
2811
    'name' => t("Body"),
2812
    'description' => t("The body of the message."),
2813
  );
2814

    
2815
  // Chained tokens for nodes.
2816
  $message['timestamp'] = array(
2817
    'name' => t("Date created"),
2818
    'description' => t("The date the message was sent."),
2819
    'type' => 'date',
2820
  );
2821
  $message['author'] = array(
2822
    'name' => t("Author"),
2823
    'description' => t("The author of the message."),
2824
    'type' => 'user',
2825
  );
2826
  $message['recipient'] = array(
2827
    'name' => t("Recipient"),
2828
    'description' => t("The recipient of the message."),
2829
    'type' => 'user',
2830
  );
2831

    
2832
  return array(
2833
    'types' => array('privatemsg_message' => $type),
2834
    'tokens' => array('privatemsg_message' => $message),
2835
  );
2836
}
2837

    
2838
/**
2839
 * Implements hook_tokens().
2840
 */
2841
function privatemsg_tokens($type, $tokens, array $data = array(), array $options = array()) {
2842
  global $user;
2843
  $url_options = array('absolute' => TRUE);
2844
  if (isset($options['language'])) {
2845
    $url_options['language'] = $options['language'];
2846
    $language_code = $options['language']->language;
2847
  }
2848
  else {
2849
    $language_code = NULL;
2850
  }
2851

    
2852
  $recipient = $user;
2853
  if (isset($data['privatemsg_recipient'])) {
2854
    $recipient = $data['privatemsg_recipient'];
2855
  }
2856

    
2857
  $sanitize = !empty($options['sanitize']);
2858
  $replacements = array();
2859
  if ($type == 'privatemsg_message' && !empty($data['privatemsg_message'])) {
2860
    $message = $data['privatemsg_message'];
2861

    
2862
    foreach ($tokens as $name => $original) {
2863
      switch ($name) {
2864
        case 'mid':
2865
          $replacements[$original] = $message->mid;
2866
          break;
2867

    
2868
        case 'thread-id':
2869
          $replacements[$original] = $message->thread_id;
2870
          break;
2871

    
2872
        case 'subject':
2873
          // Avoid recursion.
2874
          if (empty($options['privatemsg_recursion'])) {
2875
            $subject = privatemsg_token_replace($message->subject, $data, $options + array('privatemsg_recursion' => 1));
2876
          }
2877
          else {
2878
            $subject = $message->subject;
2879
          }
2880
          $replacements[$original] = $sanitize ? check_plain($subject) : $subject;
2881
          break;
2882

    
2883
        case 'body':
2884
          // Avoid recursion.
2885
          if (empty($options['privatemsg_recursion'])) {
2886
            $replacements[$original] = privatemsg_token_replace($sanitize ? check_markup($message->body, $message->format) : $message->body, $data, $options + array('privatemsg_recursion' => 1));
2887
          }
2888
          else {
2889
            $replacements[$original] = $sanitize ? check_markup($message->body, $message->format) : $message->body;
2890
          }
2891
          break;
2892

    
2893
        case 'url':
2894
          $uri = entity_uri('privatemsg_message', $message);
2895
          $replacements[$original] = url($uri['path'], $url_options + $uri['options']);
2896
          break;
2897

    
2898
        // Default values for the chained tokens handled below.
2899
        case 'author':
2900
          $replacements[$original] = $sanitize ? filter_xss(privatemsg_recipient_format($message->author, array('plain' => TRUE))) : privatemsg_recipient_format($message->author, array('plain' => TRUE));
2901
          break;
2902

    
2903
        case 'recipient':
2904
            $replacements[$original] = $sanitize ? filter_xss(privatemsg_recipient_format($recipient, array('plain' => TRUE))) : privatemsg_recipient_format($recipient, array('plain' => TRUE));
2905
          break;
2906

    
2907
        case 'timestamp':
2908
          $replacements[$original] = format_date($message->timestamp, 'medium', '', NULL, $language_code);
2909
          break;
2910
      }
2911
    }
2912

    
2913
    if ($author_tokens = token_find_with_prefix($tokens, 'author')) {
2914
      $replacements += token_generate('user', $author_tokens, array('user' => $message->author), $options);
2915
    }
2916

    
2917
    if ($recipient_tokens = token_find_with_prefix($tokens, 'recipient')) {
2918
      $replacements += token_generate('user', $recipient_tokens, array('user' => $recipient), $options);
2919
    }
2920

    
2921
    if ($sent_tokens = token_find_with_prefix($tokens, 'timestamp')) {
2922
      $replacements += token_generate('date', $sent_tokens, array('date' => $message->timestamp), $options);
2923
    }
2924
  }
2925

    
2926
  return $replacements;
2927
}
2928

    
2929
/**
2930
 * Wrapper function for token_replace() that does not replace the tokens if the
2931
 * user viewing the message is not a recipient.
2932
 */
2933
function privatemsg_token_replace($text, $data, array $options = array()) {
2934
  global $user;
2935
  if (empty($data['privatemsg_recipient'])) {
2936
    $recipient = $user;
2937
  }
2938
  else {
2939
    $recipient = $data['privatemsg_recipient'];
2940
  }
2941

    
2942
  if (isset($options['language'])) {
2943
    $url_options['language'] = $options['language'];
2944
    $language_code = $options['language']->language;
2945
  }
2946
  else {
2947
    $language_code = NULL;
2948
  }
2949

    
2950
  $message = $data['privatemsg_message'];
2951
  $show_span = !isset($options['privatemsg-show-span']) || $options['privatemsg-show-span'];
2952

    
2953
  // We do not replace tokens if the user viewing the message is the author or
2954
  // not a real recipient to avoid confusion.
2955
  $sql = "SELECT 1 FROM {pm_index} WHERE recipient = :uid AND type IN ('hidden', 'user') AND mid = :mid";
2956
  $args = array(':uid' => $recipient->uid, ':mid' => $message->mid);
2957
  if ($message->author->uid == $recipient->uid || !db_query($sql, $args)->fetchField()) {
2958
    // Get all tokens of the message.
2959
    $tokens = token_scan($text);
2960
    $invalid_tokens = array();
2961
    if (function_exists('token_get_invalid_tokens_by_context')) {
2962
      $invalid_tokens = token_get_invalid_tokens_by_context($text, array('privatemsg_message'));
2963
    }
2964

    
2965
    if (!empty($tokens)) {
2966
      $replacements = array();
2967
      // Loop over the found tokens.
2968
      foreach ($tokens as $tokens_type) {
2969
        // token_replace() returns tokens separated by type.
2970
        foreach ($tokens_type as $original) {
2971
          // Displaying invalid tokens only works with token.module.
2972
          if (in_array($original, $invalid_tokens)) {
2973
            $token = t('INVALID TOKEN @token', array('@token' => $original), array('langcode' => $language_code));
2974
            if (!$show_span) {
2975
              $replacements[$original] = '< ' . $token . ' >';
2976
            }
2977
            else {
2978
              $replacements[$original] = '<span class="privatemsg-token-invalid">&lt; ' . $token . ' &gt;</span>';
2979
            }
2980
          }
2981
          else {
2982
            $token = t('Token @token', array('@token' => $original), array('langcode' => $language_code));
2983
            if (!$show_span) {
2984
              $replacements[$original] = '< ' . $token . ' >';
2985
            }
2986
            else {
2987
              $replacements[$original] = '<span class="privatemsg-token-valid">&lt; ' . $token . ' &gt;</span>';
2988
            }
2989
          }
2990
        }
2991
      }
2992
      $text = str_replace(array_keys($replacements), $replacements, $text);
2993

    
2994
      // If there are any tokens, add a notice that the tokens will be replaced
2995
      // for the recipient.
2996
      if (!empty($options['privatemsg-token-notice'])) {
2997
        $text .= '<p class="privatemsg-token-notice">' . t('Note: Valid tokens will be replaced when a recipient is reading this message.') . '</p>';
2998
      }
2999
    }
3000
    return $text;
3001
  }
3002

    
3003
  // If the user is a recipient, use default token_replace() function.
3004
  return token_replace($text, $data, $options);
3005
}
3006

    
3007
/**
3008
 * Implements hook_entity_property_info().
3009
 */
3010
function privatemsg_entity_property_info() {
3011
  $info = array();
3012
  // Add meta-data about the basic node properties.
3013
  $properties = &$info['privatemsg_message']['properties'];
3014
  $properties = array(
3015
    'mid' => array(
3016
      'type'  => 'integer',
3017
      'label' => t('Private message ID'),
3018
      'description' => t('Private message ID'),
3019
    ),
3020
    'thread_id' => array(
3021
      'type'  => 'integer',
3022
      'label' => t('Private message thread ID'),
3023
      'description' => t('Private message thread ID'),
3024
      'getter callback' => 'entity_property_verbatim_get',
3025
    ),
3026
    'author' => array(
3027
      'type'  => 'user',
3028
      'label' => t('Private message author'),
3029
      'description' => t('Private message author'),
3030
      'setter callback' => 'entity_property_verbatim_set',
3031
    ),
3032
    'subject' => array(
3033
      'type'  => 'text',
3034
      'label' => t('Private message subject'),
3035
      'description' => t('Private message subject'),
3036
      'setter callback' => 'entity_property_verbatim_set',
3037
    ),
3038
    'body' => array(
3039
      'type'  => 'text',
3040
      'label' => t('Private message body'),
3041
      'description' => t('Private message body'),
3042
      'setter callback' => 'entity_property_verbatim_set',
3043
    ),
3044
    'timestamp' => array(
3045
      'type' => 'date',
3046
      'label' => t('Private message sent date'),
3047
      'description' => t('Private message sent date'),
3048
    ),
3049
  );
3050
  return $info;
3051
}
3052

    
3053
/**
3054
 * Private message controller, loads private messages.
3055
 */
3056
class PrivatemsgMessageController extends DrupalDefaultEntityController {
3057

    
3058
  protected $account = NULL;
3059

    
3060
  protected function attachLoad(&$messages, $revision_id = FALSE) {
3061
    global $user;
3062
    foreach ($messages as $message) {
3063
      $message->user = $this->account ? $this->account : $user;
3064
      // Load author of message.
3065
      if (!($message->author = user_load($message->author))) {
3066
        // If user does not exist, load anonymous user.
3067
        $message->author = user_load(0);
3068
      }
3069
    }
3070
    parent::attachLoad($messages, $revision_id);
3071
  }
3072

    
3073
  protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {
3074
    // Remove account from conditions.
3075
    if (isset($conditions['account'])) {
3076
      $this->account = $conditions['account'];
3077
      unset($conditions['account']);
3078
    }
3079

    
3080
    $query = parent::buildQuery($ids, $conditions, $revision_id);
3081
    $query->fields('pmi', array('is_new', 'thread_id'));
3082
    if ($this->account) {
3083
      $query
3084
        ->condition('pmi.recipient', $this->account->uid)
3085
        ->condition('pmi.type', array('hidden', 'user'));
3086
    }
3087
    else {
3088
      // If no account is given, at least limit the result to a single row per
3089
      // message.
3090
      $query->distinct();
3091
    }
3092
    $query->join('pm_index', 'pmi', "base.mid = pmi.mid");
3093
    return $query;
3094
  }
3095
}
3096

    
3097
/**
3098
 * Implements hook_date_formats().
3099
 */
3100
function privatemsg_date_formats() {
3101
  $formats = array('g:i a', 'H:i', 'M j', 'j M', 'm/d/y', 'd/m/y', 'j/n/y', 'n/j/y');
3102
  $types = array_keys(privatemsg_date_format_types());
3103
  $date_formats = array();
3104
  foreach ($types as $type) {
3105
    foreach ($formats as $format) {
3106
      $date_formats[] = array(
3107
        'type' => $type,
3108
        'format' => $format,
3109
        'locales' => array(),
3110
      );
3111
    }
3112
  }
3113
  return $date_formats;
3114
}
3115

    
3116
/**
3117
 * Implements hook_date_format_types().
3118
 */
3119
function privatemsg_date_format_types() {
3120
  return array(
3121
    'privatemsg_current_day' => t('Privatemsg: Current day'),
3122
    'privatemsg_current_year' => t('Privatemsg: Current year'),
3123
    'privatemsg_years' => t('Privatemsg: Other years'),
3124
  );
3125
}
3126

    
3127
/**
3128
 * Formats a timestamp according to the defines rules.
3129
 *
3130
 * Examples/Rules:
3131
 *
3132
 * Current hour: 25 min ago
3133
 * Current day (but not within the hour): 10:30 am
3134
 * Current year (but not on the same day): Nov 25
3135
 * Prior years (not the current year): 11/25/08
3136
 *
3137
 * @param $timestamp
3138
 *   UNIX Timestamp.
3139
 *
3140
 * @return
3141
 *   The formatted date.
3142
 */
3143
function privatemsg_format_date($timestamp) {
3144
  if ($timestamp > ((int)(REQUEST_TIME / 3600)) * 3600) {
3145
    return t('@interval ago', array('@interval' => format_interval(abs(REQUEST_TIME - $timestamp), 1)));
3146
  }
3147
  if ($timestamp > ((int)(REQUEST_TIME / 86400)) * 86400) {
3148
    return format_date($timestamp, 'privatemsg_current_day');
3149
  }
3150
  if ($timestamp > mktime(0, 0, 0, 1, 0, date('Y'))) {
3151
    return format_date($timestamp, 'privatemsg_current_year');
3152
  }
3153
  return format_date($timestamp, 'privatemsg_years');
3154
}