#1019360 by te-brian: Fixed can't submit 'Actions' on list form.
[project/privatemsg.git] / privatemsg.module
CommitLineData
129e8ef6
OT
1<?php
2// $Id$
3
4/**
5 * @file
6 * Allows users to send private messages to other users.
7 */
544d3bb5 8
4ba95ee6
OT
9/**
10 * Status constant for read messages.
11 */
12define('PRIVATEMSG_READ', 0);
13/**
14 * Status constant for unread messages.
15 */
16define('PRIVATEMSG_UNREAD', 1);
14392929
SG
17/**
18 * Show unlimited messages in a thread.
19 */
20define('PRIVATEMSG_UNLIMITED', 'unlimited');
cd266b9d 21
56306fe2 22/**
8086d49e 23 * Implements hook_perm().
258788ab 24 */
3be9375b
OT
25function privatemsg_perm() {
26 return array(
27 'read privatemsg',
28 'read all private messages',
129e8ef6 29 'administer privatemsg settings',
668e9c20
OT
30 'write privatemsg',
31 'delete privatemsg',
c1ab6ef9 32 );
c1ab6ef9
KN
33}
34
5605b147
OT
35/**
36 * Generate aray of user objects based on a string.
37 *
38 *
39 * @param $userstring
40 * A string with user id, for example 1,2,4. Returned by the list query
41 *
42 * @return
43 * Array with user objects.
44 */
68847a38 45function _privatemsg_generate_user_array($userstring, $slice = NULL) {
fbbe3e72
OT
46 static $user_cache = array();
47
68847a38
OT
48 // Convert user uid list (uid1,uid2,uid3) into an array. If $slice is not NULL
49 // pass that as argument to array_slice(). For example, -4 will only load the
50 // last four users.
57f755c6
SG
51 // This is done to avoid loading user objects that are not displayed, for
52 // obvious performance reasons.
68847a38
OT
53 $users = explode(',', $userstring);
54 if (!is_null($slice)) {
55 $users = array_slice($users, $slice);
56 }
fbbe3e72 57 $participants = array();
68847a38
OT
58 foreach ($users as $uid) {
59 if (!array_key_exists($uid, $user_cache)) {
60 $user_cache[$uid] = user_load($uid);
fbbe3e72 61 }
68847a38
OT
62 if (is_object($user_cache[$uid])) {
63 $participants[$uid] = $user_cache[$uid];
fbbe3e72
OT
64 }
65 }
66 return $participants;
67}
68
5605b147
OT
69/**
70 * Format an array of user objects.
71 *
72 * @param $part_array
73 * Array with user objects, for example the one returnd by
74 * _privatemsg_generate_user_array.
75 *
76 * @param $limit
77 * Limit the number of user objects which should be displayed.
78 * @param $no_text
79 * When TRUE, don't display the Participants/From text.
80 * @return
81 * String with formated user objects, like user1, user2.
82 */
a4f3f9c4 83function _privatemsg_format_participants($part_array, $limit = NULL, $no_text = FALSE) {
4b3cbfb4 84 if (count($part_array) > 0) {
fbbe3e72 85 $to = array();
4b3cbfb4
OT
86 $limited = FALSE;
87 foreach ($part_array as $account) {
a4f3f9c4 88 if (is_int($limit) && count($to) >= $limit) {
4b3cbfb4 89 $limited = TRUE;
fbbe3e72
OT
90 break;
91 }
4b3cbfb4 92 $to[] = theme('username', $account);
fbbe3e72
OT
93 }
94
95 $limit_string = '';
96 if ($limited) {
97 $limit_string = t(' and others');
98 }
99
100
4b3cbfb4 101 if ($no_text) {
fbbe3e72
OT
102 return implode(', ', $to) . $limit_string;
103 }
104
105 $last = array_pop($to);
106 if (count($to) == 0) { // Only one participant
107 return t("From !last", array('!last' => $last));
108 }
109 else { // Multipe participants..
110 $participants = implode(', ', $to);
4b3cbfb4 111 return t('Participants: !participants and !last', array('!participants' => $participants, '!last' => $last));
fbbe3e72
OT
112 }
113 }
114 return '';
115}
116
c1ab6ef9 117/**
8086d49e 118 * Implements hook_menu().
c73fcb85 119 */
3be9375b 120function privatemsg_menu() {
129e8ef6
OT
121 $items['messages'] = array(
122 'title' => 'Messages',
123 'title callback' => 'privatemsg_title_callback',
4ba95ee6 124 'page callback' => 'drupal_get_form',
c277e471 125 'page arguments' => array('privatemsg_list', 'list'),
bc4689a7 126 'access callback' => 'privatemsg_user_access',
129e8ef6 127 'type' => MENU_NORMAL_ITEM,
3be9375b 128 );
c277e471
OT
129 $items['messages/list'] = array(
130 'title' => 'Messages',
4ba95ee6 131 'page callback' => 'drupal_get_form',
c277e471 132 'page arguments' => array('privatemsg_list', 'list'),
bc4689a7 133 'access callback' => 'privatemsg_user_access',
129e8ef6
OT
134 'type' => MENU_DEFAULT_LOCAL_TASK,
135 'weight' => -10,
3be9375b 136 );
fb405f31
OT
137 $items['messages/view/%privatemsg_thread'] = array(
138 'title' => 'Read message',
129e8ef6
OT
139 'page callback' => 'privatemsg_view',
140 'page arguments' => array(2),
bc4689a7 141 'access callback' => 'privatemsg_view_access',
fb405f31 142 'type' => MENU_LOCAL_TASK,
db359c51 143 'weight' => -5,
3be9375b 144 );
ca5ce8f9 145 $items['messages/delete/%privatemsg_thread/%privatemsg_message'] = array(
129e8ef6
OT
146 'title' => 'Delete message',
147 'page callback' => 'drupal_get_form',
ca5ce8f9 148 'page arguments' => array('privatemsg_delete', 2, 3),
bc4689a7 149 'access callback' => 'privatemsg_user_access',
668e9c20 150 'access arguments' => array('delete privatemsg'),
129e8ef6 151 'type' => MENU_CALLBACK,
129e8ef6
OT
152 );
153 $items['messages/new'] = array(
154 'title' => 'Write new message',
155 'page callback' => 'drupal_get_form',
68847a38 156 'page arguments' => array('privatemsg_new', 2, 3, NULL),
bc4689a7 157 'access callback' => 'privatemsg_user_access',
129e8ef6
OT
158 'access arguments' => array('write privatemsg'),
159 'type' => MENU_LOCAL_TASK,
db359c51 160 'weight' => -3,
129e8ef6 161 );
129e8ef6
OT
162 // Auto-completes available user names & removes duplicates.
163 $items['messages/user-name-autocomplete'] = array(
164 'page callback' => 'privatemsg_user_name_autocomplete',
bc4689a7 165 'access callback' => 'privatemsg_user_access',
3be9375b 166 'access arguments' => array('write privatemsg'),
129e8ef6
OT
167 'type' => MENU_CALLBACK,
168 'weight' => -10,
169 );
170 $items['admin/settings/messages'] = array(
171 'title' => 'Private messages',
172 'description' => 'Configure private messaging settings.',
173 'page callback' => 'drupal_get_form',
174 'page arguments' => array('private_message_settings'),
175 'access arguments' => array('administer privatemsg settings'),
176 'type' => MENU_NORMAL_ITEM,
c1ab6ef9 177 );
e5fe4a8d
OT
178 $items['admin/settings/messages/default'] = array(
179 'title' => 'Private messages',
180 'description' => 'Configure private messaging settings.',
181 'page callback' => 'drupal_get_form',
182 'page arguments' => array('private_message_settings'),
183 'access arguments' => array('administer privatemsg settings'),
184 'type' => MENU_DEFAULT_LOCAL_TASK,
185 'weight' => -10,
186 );
4ba95ee6
OT
187 $items['messages/undo/action'] = array(
188 'title' => 'Private messages',
189 'description' => 'Undo last thread action',
190 'page callback' => 'privatemsg_undo_action',
191 'access arguments' => array('read privatemsg'),
192 'type' => MENU_CALLBACK,
193 );
bf1e88fc
OT
194 $items['user/%/messages'] = array(
195 'title' => 'Messages',
196 'page callback' => 'drupal_get_form',
197 'page arguments' => array('privatemsg_list', 'list', 1),
ad4afd09
SG
198 'access callback' => 'privatemsg_user_access',
199 'access arguments' => array('read all private messages'),
bf1e88fc
OT
200 'type' => MENU_LOCAL_TASK,
201 );
202
3be9375b 203 return $items;
c1ab6ef9 204}
bc4689a7
OT
205
206/**
5605b147
OT
207 * Privatemsg wrapper for user_access.
208 *
209 * Never allows anonymous user access as that doesn't makes sense.
bc4689a7 210 *
5605b147
OT
211 * @param $permission
212 * Permission string, defaults to read privatemsg
bc4689a7 213 *
5605b147
OT
214 * @return
215 * TRUE if user has access, FALSE if not
bc4689a7 216 *
5605b147 217 * @ingroup api
bc4689a7
OT
218 */
219function privatemsg_user_access($permission = 'read privatemsg', $account = NULL) {
220 if ( $account === NULL ) {
221 global $user;
222 $account = $user;
223 }
224 if (!$account->uid) { // Disallow anonymous access, regardless of permissions
225 return FALSE;
226 }
227 if (!user_access($permission, $account)) {
228 return FALSE;
229 }
230 return TRUE;
231}
232
bf1e88fc 233/**
5605b147
OT
234 * Check access to the view messages page.
235 *
236 * Function to restrict the access of the view messages page to just the
237 * messages/view/% pages and not to leave tabs artifact on other lower
238 * level pages such as the messages/new/%.
239 *
240 * @ingroup api
bc4689a7
OT
241 */
242function privatemsg_view_access() {
243 if (privatemsg_user_access('read privatemsg') && arg(1) == 'view') {
244 return TRUE;
245 }
246 return FALSE;
247}
248
fb405f31 249/**
5605b147
OT
250 * Load a thread with all the messages and participants.
251 *
252 * This function is called by the menu system through the %privatemsg_thread
253 * wildcard.
fb405f31 254 *
5605b147
OT
255 * @param $thread_id
256 * Thread id, pmi.thread_id or pm.mid of the first message in that thread.
053a2f78
OT
257 * @param $account
258 * User object for which the thread should be loaded, defaults to
259 * the current user.
2b9b5029
SG
260 * @param $start
261 * Message offset from the start of the thread.
5605b147 262 *
053a2f78
OT
263 * @return
264 * $thread object, with keys messages, participants, title and user. messages
265 * contains an array of messages, participants an array of user, subject the
266 * subject of the thread and user the user viewing the thread.
267 *
268 * If no messages are found, or the thread_id is invalid, the function returns
269 * FALSE.
270
5605b147 271 * @ingroup api
fb405f31 272 */
2b9b5029 273function privatemsg_thread_load($thread_id, $account = NULL, $start = NULL) {
c120fb99 274 static $threads = array();
fb405f31 275 if ((int)$thread_id > 0) {
68847a38 276 $thread = array('thread_id' => $thread_id);
053a2f78
OT
277
278 if (is_null($account)) {
279 global $user;
280 $account = drupal_clone($user);
281 }
bf1e88fc 282
c120fb99
OT
283 if (!isset($threads[$account->uid])) {
284 $threads[$account->uid] = array();
bf1e88fc
OT
285 }
286
c120fb99
OT
287 if (!array_key_exists($thread_id, $threads[$account->uid])) {
288 // Load the list of participants.
289 $query = _privatemsg_assemble_query('participants', $thread_id);
290 $participants = db_query($query['query']);
291 $thread['participants'] = array();
292 while ($participant = db_fetch_object($participants)) {
293 $thread['participants'][$participant->uid] = $participant;
294 }
295 $thread['read_all'] = FALSE;
296 if (!array_key_exists($account->uid, $thread['participants']) && privatemsg_user_access('read all private messages', $account)) {
297 $thread['read_all'] = TRUE;
053a2f78 298 }
053a2f78 299
14392929 300 // Load messages returned by the messages query with privatemsg_message_load_multiple().
c120fb99 301 $query = _privatemsg_assemble_query('messages', array($thread_id), $thread['read_all'] ? NULL : $account);
14392929
SG
302 $thread['message_count'] = $thread['to'] = db_result(db_query($query['count']));
303 $thread['from'] = 1;
077e5644 304 // Check if we need to limit the messages.
14392929 305 $max_amount = variable_get('privatemsg_view_max_amount', 20);
2b9b5029
SG
306
307 // If there is no start value, select based on get params.
308 if (is_null($start)) {
309 if (isset($_GET['start']) && $_GET['start'] < $thread['message_count']) {
310 $start = $_GET['start'];
311 }
3871ac37
SG
312 elseif (!variable_get('privatemsg_view_use_max_as_default', FALSE) && $max_amount == PRIVATEMSG_UNLIMITED) {
313 $start = PRIVATEMSG_UNLIMITED;
314 }
2b9b5029 315 else {
3871ac37 316 $start = $thread['message_count'] - (variable_get('privatemsg_view_use_max_as_default', FALSE) ? variable_get('privatemsg_view_default_amount', 10) : $max_amount);
2b9b5029
SG
317 }
318 }
319
320 if ($start != PRIVATEMSG_UNLIMITED) {
14392929 321 if ($max_amount == PRIVATEMSG_UNLIMITED) {
2b9b5029
SG
322 $last_page = 0;
323 $max_amount = $thread['message_count'];
324 }
325 else {
326 // Calculate the number of messages on the "last" page to avoid
327 // message overlap.
328 // Note - the last page lists the earliest messages, not the latest.
329 $paging_count = variable_get('privatemsg_view_use_max_as_default', FALSE) ? $thread['message_count'] - variable_get('privatemsg_view_default_amount', 10) : $thread['message_count'];
330 $last_page = $paging_count % $max_amount;
331 }
332
333 // Sanity check - we cannot start from a negative number.
334 if ($start < 0) {
335 $start = 0;
14392929 336 }
2b9b5029
SG
337 $thread['start'] = $start;
338
339 //If there are newer messages on the page, show pager link allowing to go to the newer messages.
340 if (($start + $max_amount + 1) < $thread['message_count']) {
341 $thread['to'] = $start + $max_amount;
342 $thread['newer_start'] = $start + $max_amount;
14392929 343 }
2b9b5029
SG
344 if ($start - $max_amount >= 0) {
345 $thread['older_start'] = $start - $max_amount;
14392929 346 }
2b9b5029
SG
347 elseif ($start > 0) {
348 $thread['older_start'] = 0;
14392929 349 }
2b9b5029
SG
350
351 // Do not show messages on the last page that would show on the page
352 // before. This will only work when using the visual pager.
99c5bb7f 353 if ($start < $last_page && $max_amount != PRIVATEMSG_UNLIMITED && $max_amount < $thread['message_count']) {
2b9b5029
SG
354 unset($thread['older_start']);
355 $thread['to'] = $thread['newer_start'] = $max_amount = $last_page;
356 // Start from the first message - this is a specific hack to make sure
357 // the message display has sane paging on the last page.
358 $start = 0;
359 }
360 // Visual counts start from 1 instead of zero, so plus one.
361 $thread['from'] = $start + 1;
362 $conversation = db_query_range($query['query'], $start, $max_amount);
363 }
14392929
SG
364 else {
365 $conversation = db_query($query['query']);
077e5644 366 }
22ae69fd 367 $mids = array();
c120fb99 368 while ($result = db_fetch_array($conversation)) {
22ae69fd 369 $mids[] = $result['mid'];
c120fb99 370 }
22ae69fd
SG
371 // Load messages returned by the messages query.
372 $thread['messages'] = privatemsg_message_load_multiple($mids, $thread['read_all'] ? NULL : $account);
373
077e5644 374 // If there are no messages, don't allow access to the thread.
c120fb99
OT
375 if (empty($thread['messages'])) {
376 $thread = FALSE;
8086d49e
SG
377 }
378 else {
077e5644 379 // General data, assume subject is the same for all messages of that thread.
c120fb99
OT
380 $thread['user'] = $account;
381 $message = current($thread['messages']);
382 $thread['subject'] = $message['subject'];
383 }
384 $threads[$account->uid][$thread_id] = $thread;
385 }
053a2f78 386
c120fb99 387 return $threads[$account->uid][$thread_id];
fb405f31
OT
388 }
389 return FALSE;
390}
129e8ef6 391
3be9375b
OT
392function private_message_view_options() {
393 $options = module_invoke_all('privatemsg_view_template');
394 return $options;
fe5f5474 395}
129e8ef6 396
c1ab6ef9 397/**
8086d49e 398 * Implements hook_privatemsg_view_template().
129e8ef6
OT
399 *
400 * Allows modules to define different message view template.
401 *
402 * This hook returns information about available themes for privatemsg viewing.
3be9375b 403 *
3be9375b
OT
404 * array(
405 * 'machine_template_name' => 'Human readable template name',
406 * 'machine_template_name_2' => 'Human readable template name 2'
407 * };
408 */
409function privatemsg_privatemsg_view_template() {
410 return array(
129e8ef6 411 'privatemsg-view' => 'Default view',
c1ab6ef9 412 );
3be9375b 413}
129e8ef6 414
3be9375b
OT
415function private_message_settings() {
416 $form = array();
129e8ef6 417
3be9375b 418 $form['theming_settings'] = array(
129e8ef6
OT
419 '#type' => 'fieldset',
420 '#collapsible' => TRUE,
421 '#collapsed' => TRUE,
422 '#title' => t('Theming settings'),
3be9375b
OT
423 );
424 $form['theming_settings']['private_message_view_template'] = array(
129e8ef6
OT
425 '#type' => 'radios',
426 '#title' => t('Private message display template'),
3be9375b 427 '#default_value' => variable_get('private_message_view_template', 'privatemsg-view'),
129e8ef6
OT
428 '#options' => private_message_view_options(),
429 );
20b4f5d2
OT
430 $form['privatemsg_display_loginmessage'] = array(
431 '#type' => 'checkbox',
432 '#title' => t('Inform the user about new messages on login'),
433 '#default_value' => variable_get('privatemsg_display_loginmessage', TRUE),
434 '#description' => t('This option can safely be disabled if the "New message indication" block is used instead.'),
435 );
7bfff1e7
SG
436
437 $form['flush_deleted'] = array(
438 '#type' => 'fieldset',
439 '#collapsible' => TRUE,
440 '#collapsed' => TRUE,
441 '#title' => t('Flush deleted messages'),
88a6dac4 442 '#description' => t('By default, deleted messages are only hidden from the user but still stored in the database. These settings control if and when messages should be removed.'),
7bfff1e7
SG
443 );
444
445 $form['flush_deleted']['privatemsg_flush_enabled'] = array(
446 '#type' => 'checkbox',
447 '#title' => t('Flush deleted messages'),
448 '#default_value' => variable_get('privatemsg_flush_enabled', FALSE),
449 '#description' => t('Enable the flushing of deleted messages. Requires that cron is enabled'),
450 );
451
452 $form['flush_deleted']['privatemsg_flush_days'] = array(
453 '#type' => 'select',
454 '#title' => t('Flush messages after they have been deleted for more days than'),
455 '#default_value' => variable_get('privatemsg_flush_days', 30),
456 '#options' => drupal_map_assoc(array(0, 1, 2, 5, 10, 30, 100)),
457 );
458
459 $form['flush_deleted']['privatemsg_flush_max'] = array(
460 '#type' => 'select',
461 '#title' => t('Maximum number of messages to flush per cron run'),
462 '#default_value' => variable_get('privatemsg_flush_max', 200),
463 '#options' => drupal_map_assoc(array(50, 100, 200, 500, 1000)),
464 );
465
4ba95ee6
OT
466 $form['privatemsg_listing'] = array(
467 '#type' => 'fieldset',
468 '#title' => t('Configure listings'),
469 '#collapsible' => TRUE,
db359c51 470 '#collapsed' => FALSE,
4ba95ee6
OT
471 );
472
14392929
SG
473 $form['privatemsg_listing']['privatemsg_per_page'] = array(
474 '#type' => 'select',
475 '#title' => t('Threads per page'),
476 '#default_value' => variable_get('privatemsg_per_page', 25),
477 '#options' => drupal_map_assoc(array(10, 25, 50, 75, 100)),
478 '#description' => t('Choose the number of conversations that should be listed per page.'),
479 );
480
4ba95ee6
OT
481 $form['privatemsg_listing']['privatemsg_display_fields'] = array(
482 '#type' => 'checkboxes',
483 '#title' => t('Configure fields'),
484 '#description' => t('Select which columns/fields should be displayed in the message listings. Subject and Last updated cannot be disabled.'),
485 '#options' => array(
c277e471 486 'participants' => t('Participants'),
4ba95ee6 487 'thread_started' => t('Started'),
6aad28b8 488 'count' => t('Messages'),
4ba95ee6 489 ),
c277e471 490 '#default_value' => variable_get('privatemsg_display_fields', array('participants')),
4ba95ee6
OT
491 );
492
14392929
SG
493 $amounts = drupal_map_assoc(array(5, 10, 20, 30, 50, 70, 90, 150, 200, 250, 300));
494 $form['privatemsg_listing']['privatemsg_view_max_amount'] = array(
495 '#type' => 'select',
496 '#title' => t('Number of messages on thread pages'),
497 '#options' => $amounts + array(PRIVATEMSG_UNLIMITED => t('Unlimited')),
498 '#default_value' => variable_get('privatemsg_view_max_amount', 20),
499 '#description' => t('Threads will not show more than this number of messages on a single page.'),
077e5644
SG
500 '#weight' => 10,
501 );
502
14392929
SG
503 $form['privatemsg_listing']['privatemsg_view_use_max_as_default'] = array(
504 '#type' => 'checkbox',
505 '#title' => t('Display different amount of messages on first thread page'),
506 '#default_value' => variable_get('privatemsg_view_use_max_as_default', FALSE),
507 '#description' => t('By default, the first thread page shows the maximally allowed amount of messages. Enable this checkbox to set a different value.'),
077e5644
SG
508 '#weight' => 15,
509 );
510
14392929
SG
511 $form['privatemsg_listing']['privatemsg_view_default_amount'] = array(
512 '#prefix' => '<div id="privatemsg-view-default-button">',
513 '#suffix' => '</div>',
514 '#type' => 'select',
515 '#title' => t('Number of messages on first thread page'),
516 '#default_value' => variable_get('privatemsg_view_default_amount', 10),
517 '#description' => t('The number of messages to be displayed on first thread page. Displays the newest messages.'),
518 '#options' => $amounts,
519 '#weight' => 20,
520 );
521 drupal_add_js(drupal_get_path('module', 'privatemsg') .'/privatemsg-admin.js');
522
3be9375b
OT
523 $form['#submit'][] = 'private_message_settings_submit';
524 return system_settings_form($form);
525}
129e8ef6 526
3be9375b
OT
527function private_message_settings_submit() {
528 drupal_rebuild_theme_registry();
529}
c1ab6ef9 530
7bfff1e7 531/**
8086d49e 532 * Implements hook_cron().
7bfff1e7
SG
533 *
534 * If the flush feature is enabled, a given amount of deleted messages that are
535 * old enough are flushed.
536 */
537function privatemsg_cron() {
538 if (variable_get('privatemsg_flush_enabled', FALSE)) {
539 $query = _privatemsg_assemble_query('deleted', variable_get('privatemsg_flush_days', 30));
540 $result = db_query($query['query']);
541
542 $flushed = 0;
543 while (($row = db_fetch_array($result)) && ($flushed < variable_get('privatemsg_flush_max', 200))) {
22ae69fd 544 $message = privatemsg_message_load($row['mid']);
7bfff1e7
SG
545 module_invoke_all('privatemsg_message_flush', $message);
546
547 // Delete recipients of the message.
548 db_query('DELETE FROM {pm_index} WHERE mid = %d', $row['mid']);
549 // Delete message itself.
550 db_query('DELETE FROM {pm_message} WHERE mid = %d', $row['mid']);
551 $flushed++;
552 }
553 }
554}
555
3be9375b
OT
556function privatemsg_theme() {
557 return array(
558 'privatemsg_view' => array(
129e8ef6 559 'arguments' => array('message' => NULL),
5605b147 560 'template' => variable_get('private_message_view_template', 'privatemsg-view'), // 'privatemsg',
3be9375b
OT
561 ),
562 'privatemsg_from' => array(
129e8ef6
OT
563 'arguments' => array('author' => NULL),
564 'template' => 'privatemsg-from',
3be9375b 565 ),
303ed90c 566 'privatemsg_recipients' => array(
129e8ef6
OT
567 'arguments' => array('message' => NULL),
568 'template' => 'privatemsg-recipients',
3be9375b 569 ),
129e8ef6
OT
570 'privatemsg_between' => array(
571 'arguments' => array('recipients' => NULL),
572 'template' => 'privatemsg-between',
573 ),
4ba95ee6
OT
574 'privatemsg_list' => array(
575 'file' => 'privatemsg.theme.inc',
576 'path' => drupal_get_path('module', 'privatemsg'),
577 'arguments' => array('form'),
578 ),
579 // Define pattern for header/field templates. The theme system will register all
580 // theme functions that start with the defined pattern.
581 'privatemsg_list_header' => array(
582 'file' => 'privatemsg.theme.inc',
583 'path' => drupal_get_path('module', 'privatemsg'),
584 'pattern' => 'privatemsg_list_header__',
585 'arguments' => array(),
586 ),
587 'privatemsg_list_field' => array(
588 'file' => 'privatemsg.theme.inc',
589 'path' => drupal_get_path('module', 'privatemsg'),
590 'pattern' => 'privatemsg_list_field__',
591 'arguments' => array('thread'),
3be9375b 592 ),
20b4f5d2 593 'privatemsg_new_block' => array(
4ba95ee6
OT
594 'file' => 'privatemsg.theme.inc',
595 'path' => drupal_get_path('module', 'privatemsg'),
20b4f5d2
OT
596 'arguments' => array('count'),
597 ),
c1ab6ef9 598 );
c1ab6ef9 599}
129e8ef6 600
3b333877 601function template_preprocess_privatemsg_view(&$vars) {
129e8ef6
OT
602// drupal_set_message('<pre>'. print_r($vars,1 ) . '</pre>');
603
3be9375b 604 $message = $vars['message'];
ca5ce8f9
SG
605 $vars['mid'] = isset($message['mid']) ? $message['mid'] : NULL;
606 $vars['thread_id'] = isset($message['thread_id']) ? $message['thread_id'] : NULL;
3be9375b
OT
607 $vars['author_picture'] = theme('user_picture', $message['author']);
608 $vars['author_name_link'] = theme('username', $message['author']);
129e8ef6
OT
609 /**
610 * @todo perhaps make this timestamp configurable via admin UI?
611 */
612 $vars['message_timestamp'] = format_date($message['timestamp'], 'small');
ed18b9f3 613 $vars['message_body'] = check_markup($message['body'], $message['format'], FALSE);
ca5ce8f9
SG
614 if (isset($vars['mid']) && isset($vars['thread_id']) && privatemsg_user_access('delete privatemsg')) {
615 $vars['message_actions'][] = array('title' => t('Delete message'), 'href' => 'messages/delete/' . $vars['thread_id'] . '/' . $vars['mid']);
6aad28b8
OT
616 }
617 $vars['message_anchors'][] = 'privatemsg-mid-' . $vars['mid'];
618 if (!empty($message['is_new'])) {
619 $vars['message_anchors'][] = 'new';
5c329162 620 $vars['new'] = drupal_ucfirst(t('new'));
129e8ef6 621 }
93a1235e
OT
622
623 // call hook_privatemsg_message_view_alter
624 drupal_alter('privatemsg_message_view', $vars);
328f2c62 625
efb7face 626 $vars['message_actions'] = !empty($vars['message_actions']) ? theme('links', $vars['message_actions'], array('class' => 'message-actions')) : '';
811e9fd5 627
6aad28b8
OT
628 $vars['anchors'] = '';
629 foreach ($vars['message_anchors'] as $anchor) {
8086d49e 630 $vars['anchors'] .= '<a name="' . $anchor . '"></a>';
6aad28b8 631 }
e50464be 632}
129e8ef6 633
3b333877 634function template_preprocess_privatemsg_recipients(&$vars) {
5605b147 635 $vars['participants'] = ''; // assign a default empty value
fbbe3e72
OT
636 if (isset($vars['message']['participants'])) {
637 $vars['participants'] = _privatemsg_format_participants($vars['message']['participants']);
e50464be
MM
638 }
639}
fb8f4982
OT
640
641/**
129e8ef6 642 * List messages.
fb8f4982 643 *
c277e471
OT
644 * @param $form_state
645 * Form state array
646 * @param $argument
647 * An argument to pass through to the query builder.
5605b147 648 * @param $uid
c277e471
OT
649 * User id messages of another user should be displayed
650 *
347e66c9 651 * @return
c277e471 652 * Form array
fb8f4982 653 */
c277e471 654function privatemsg_list(&$form_state, $argument = 'list', $uid = NULL) {
fb8f4982 655 global $user;
129e8ef6
OT
656
657 // Setting default behavior...
fb8f4982 658 $account = $user;
4ba95ee6
OT
659 // Because uid is submitted by the menu system, it's a string not a integer.
660 if ((int)$uid > 0 && $uid != $user->uid) {
129e8ef6 661 // Trying to view someone else's messages...
bc4689a7 662 if (!privatemsg_user_access('read all private messages')) {
129e8ef6 663 drupal_set_message(t("You do not have sufficient rights to view someone else's messages"), 'warning');
fb8f4982 664 }
129e8ef6
OT
665 elseif ($account_check = user_load(array('uid' => $uid))) {
666 // Has rights and user_load return an array so user does exist
667 $account = $account_check;
fb8f4982 668 }
129e8ef6
OT
669 }
670 // By this point we have figured out for which user we are listing messages and now it is safe to use $account->uid in the listing query.
c277e471
OT
671
672 $query = _privatemsg_assemble_query('list', $account, $argument);
129e8ef6 673 $result = pager_query($query['query'], variable_get('privatemsg_per_page', 25), 0, $query['count']);
fb8f4982 674
4ba95ee6
OT
675 $threads = array();
676 $form['#data'] = array();
677 while ($row = db_fetch_array($result)) {
678 // Store the raw row data.
679 $form['#data'][$row['thread_id']] = $row;
4ba95ee6
OT
680 // store thread id for the checkboxes array
681 $threads[$row['thread_id']] = '';
129e8ef6 682 }
4756d26b 683 if (!empty($form['#data'])) {
4ba95ee6 684 $form['actions'] = _privatemsg_action_form();
fb8f4982 685 }
fb8f4982 686
348e9d62
SG
687 // Save the currently active account, used for actions.
688 $form['account'] = array('#type' => 'value', '#value' => $account);
689
4ba95ee6
OT
690 // Define checkboxes, pager and theme
691 $form['threads'] = array('#type' => 'checkboxes', '#options' => $threads);
692 $form['pager'] = array('#value' => theme('pager'), '#weight' => 20);
693 $form['#theme'] = 'privatemsg_list';
20b4f5d2 694
4ba95ee6
OT
695 // Store the account for which the threads are displayed.
696 $form['#account'] = $account;
697 return $form;
20b4f5d2
OT
698}
699
3be9375b 700/**
4ba95ee6 701 * Changes the read/new status of a single message.
3be9375b 702 *
5605b147
OT
703 * @param $pmid
704 * Message id
705 * @param $status
706 * Either PRIVATEMSG_READ or PRIVATEMSG_UNREAD
707 * @param $account
708 * User object, defaults to the current user
3be9375b 709 */
4ba95ee6
OT
710function privatemsg_message_change_status($pmid, $status, $account = NULL) {
711 if (!$account) {
712 global $user;
713 $account = $user;
129e8ef6 714 }
4ba95ee6
OT
715 $query = "UPDATE {pm_index} SET is_new = %d WHERE mid = %d AND uid = %d";
716 db_query($query, $status, $pmid, $account->uid);
129e8ef6
OT
717}
718
719/**
129e8ef6 720 * Return number of unread messages for an account.
5605b147
OT
721 *
722 * @param $account
723 * Specifiy the user for which the unread count should be loaded.
724 *
725 * @ingroup api
129e8ef6
OT
726 */
727function privatemsg_unread_count($account = NULL) {
728 static $counts = array();
129e8ef6
OT
729 if (!$account || $account->uid == 0) {
730 global $user;
3be9375b 731 $account = $user;
129e8ef6
OT
732 }
733 if ( !isset($counts[$account->uid])) {
f2161b8c 734 $query = _privatemsg_assemble_query('unread_count', $account);
129e8ef6
OT
735 $counts[$account->uid] = db_result(db_query($query['query']));
736 }
737 return $counts[$account->uid];
738}
739
053a2f78
OT
740/**
741 * Menu callback for viewing a thread.
742 *
743 * @param $thread
744 * A array containing all information about a specific thread, generated by
745 * privatemsg_thread_load().
746 * @return
747 * The page content.
8086d49e 748 * @see privatemsg_thread_load()
053a2f78
OT
749 */
750function privatemsg_view($thread) {
751 drupal_set_title(check_plain($thread['subject']));
35a8a46e 752
077e5644 753 // Generate paging links.
14392929
SG
754 $older = '';
755 if (isset($thread['older_start'])) {
756 $options = array(
757 'query' => array('start' => $thread['older_start']),
758 'title' => t('Display older messages'),
759 );
760 $older = l(t('<<'), 'messages/view/' . $thread['thread_id'], $options);
761 }
762 $newer = '';
763 if (isset($thread['newer_start'])) {
764 $options = array(
765 'query' => array('start' => $thread['newer_start']),
766 'title' => t('Display newer messages'),
767 );
768 $newer = l(t('>>'), 'messages/view/' . $thread['thread_id'], $options);
077e5644 769 }
14392929
SG
770 $substitutions = array('@from' => $thread['from'], '@to' => $thread['to'], '@total' => $thread['message_count'], '!previous_link' => $older, '!newer_link' => $newer);
771 $title = t('!previous_link Displaying messages @from - @to of @total !newer_link', $substitutions);
2b9b5029 772 $content['pager_top'] = array(
14392929
SG
773 '#value' => trim($title),
774 '#prefix' => '<div class="privatemsg-view-pager">',
775 '#suffix' => '</div>',
776 '#weight' => -10,
777 );
778
779 // Display a copy at the end.
2b9b5029
SG
780 $content['pager_bottom'] = $content['pager_top'];
781 $content['pager_bottom']['#weight'] = 3;
077e5644 782
053a2f78 783 // Render the participants.
303ed90c 784 $content['participants']['#value'] = theme('privatemsg_recipients', $thread);
129e8ef6
OT
785 $content['participants']['#weight'] = -5;
786
053a2f78
OT
787 // Render the messages.
788 $output = '';
789 foreach ($thread['messages'] as $pmid => $message) {
790 // Set message as read and theme it.
82b71e87
OT
791 if (!empty($message['is_new'])) {
792 privatemsg_message_change_status($pmid, PRIVATEMSG_READ, $thread['user']);
793 }
129e8ef6 794 $output .= theme('privatemsg_view', $message);
129e8ef6 795 }
053a2f78 796 $content['messages']['#value'] = $output;
129e8ef6
OT
797 $content['messages']['#weight'] = 0;
798
053a2f78
OT
799 // Display the reply form if user is allowed to use it.
800 if (privatemsg_user_access('write privatemsg')) {
bf1e88fc 801 $content['reply']['#value'] = drupal_get_form('privatemsg_new', $thread['participants'], $thread['subject'], $thread['thread_id'], $thread['read_all']);
129e8ef6
OT
802 $content['reply']['#weight'] = 5;
803 }
804
de0db33f
SG
805 // Check after calling the privatemsg_new form so that this message is only
806 // displayed when we are not sending a message.
807 if ($thread['read_all']) {
808 // User has permission to read all messages AND is not a participant of the current thread.
809 drupal_set_message(t('This conversation is being viewed with escalated priviledges and may not be the same as shown to normal users.'), 'warning');
810 }
811
8086d49e 812 // Allow other modules to hook into the $content array and alter it.
053a2f78
OT
813 drupal_alter('privatemsg_view_messages', $content, $thread);
814 return drupal_render($content);
e50464be
MM
815}
816
129e8ef6 817
bf1e88fc 818function privatemsg_new(&$form_state, $recipients = array(), $subject = '', $thread_id = NULL, $read_all = FALSE) {
e50464be 819 global $user;
129e8ef6 820
68847a38 821 $recipients_string = '';
3be9375b 822 $body = '';
129e8ef6 823
68847a38
OT
824 // convert recipients to array of user objects
825 if (!empty($recipients) && is_string($recipients) || is_int($recipients)) {
826 $recipients = _privatemsg_generate_user_array($recipients);
827 }
828 elseif (is_object($recipients)) {
829 $recipients = array($recipients);
830 }
831 elseif (empty($recipients) && is_string($recipients)) {
832 $recipients = array();
833 }
834
835 $usercount = 0;
836 $to = array();
668e9c20 837 $to_themed = array();
4234ba60 838 $blocked = FALSE;
68847a38
OT
839 foreach ($recipients as $recipient) {
840 if (in_array($recipient->name, $to)) {
841 // We already added the recipient to the list, skip him.
842 continue;
843 }
844 // Check if another module is blocking the sending of messages to the recipient by current user.
4234ba60 845 $user_blocked = module_invoke_all('privatemsg_block_message', $user, array($recipient->uid => $recipient));
68847a38
OT
846 if (!count($user_blocked) <> 0 && $recipient->uid) {
847 if ($recipient->uid == $user->uid) {
848 $usercount++;
849 // Skip putting author in the recipients list for now.
850 continue;
851 }
852 $to[] = $recipient->name;
668e9c20 853 $to_themed[$recipient->uid] = theme('username', $recipient);
68847a38 854 }
4234ba60
OT
855 else {
856 // Recipient list contains blocked users.
857 $blocked = TRUE;
858 }
68847a38
OT
859 }
860
4234ba60 861 if (empty($to) && $usercount >= 1 && !$blocked) {
68847a38
OT
862 // Assume the user sent message to own account as if the usercount is one or less, then the user sent a message but not to self.
863 $to[] = $user->name;
668e9c20 864 $to_themed[$user->uid] = theme('username', $user);
68847a38
OT
865 }
866
68847a38
OT
867 if (!empty($to)) {
868 $recipients_string = implode(', ', $to);
129e8ef6 869 }
3be9375b 870 if (isset($form_state['values'])) {
4234ba60
OT
871 if (isset($form_state['values']['recipient'])) {
872 $recipients_string = $form_state['values']['recipient'];
8086d49e 873
4234ba60 874 }
129e8ef6
OT
875 $subject = $form_state['values']['subject'];
876 $body = $form_state['values']['body'];
496acb83 877 }
68847a38
OT
878 if (!$thread_id && !empty($recipients_string)) {
879 drupal_set_title(t('Write new message to %recipient', array('%recipient' => $recipients_string)));
8086d49e
SG
880 }
881 elseif (!$thread_id) {
68847a38
OT
882 drupal_set_title(t('Write new message'));
883 }
e50464be 884
3be9375b
OT
885 $form = array();
886 if (isset($form_state['privatemsg_preview'])) {
887 $form['message_header'] = array(
888 '#type' => 'fieldset',
129e8ef6 889 '#attributes' => array('class' => 'preview'),
3be9375b
OT
890 );
891 $form['message_header']['message_preview'] = array(
892 '#value' => $form_state['privatemsg_preview'],
893 );
29001b19 894 }
129e8ef6
OT
895 $form['privatemsg'] = array(
896 '#type' => 'fieldset',
bc4689a7 897 '#access' => privatemsg_user_access('write privatemsg'),
129e8ef6
OT
898 );
899 $form['privatemsg']['author'] = array(
900 '#type' => 'value',
901 '#value' => $user,
3be9375b 902 );
668e9c20 903 if (is_null($thread_id)) {
811e9fd5 904 $form['privatemsg']['recipient'] = array(
668e9c20
OT
905 '#type' => 'textfield',
906 '#title' => t('To'),
907 '#description' => t('Separate multiple names with commas.'),
908 '#default_value' => $recipients_string,
909 '#required' => TRUE,
910 '#weight' => -10,
911 '#size' => 50,
912 '#autocomplete_path' => 'messages/user-name-autocomplete',
913 // Do not hardcode #maxlength, make it configurable by number of recipients, not their name length.
914 );
915 }
811e9fd5 916 $form['privatemsg']['subject'] = array(
3be9375b
OT
917 '#type' => 'textfield',
918 '#title' => t('Subject'),
919 '#size' => 50,
920 '#maxlength' => 255,
921 '#default_value' => $subject,
93a1235e 922 '#weight' => -5,
3be9375b 923 );
811e9fd5 924 $form['privatemsg']['body'] = array(
3be9375b
OT
925 '#type' => 'textarea',
926 '#title' => t('Message'),
3be9375b 927 '#rows' => 6,
93a1235e 928 '#weight' => 0,
3be9375b 929 '#default_value' => $body,
811e9fd5 930 '#resizable' => TRUE,
3be9375b 931 );
811e9fd5
SG
932 $format = FILTER_FORMAT_DEFAULT;
933 // The input filter widget looses the format during preview, specify it
934 // explicitly.
935 if (isset($form_state['values']) && array_key_exists('format', $form_state['values'])) {
936 $format = $form_state['values']['format'];
937 }
938 $form['privatemsg']['format'] = filter_form($format);
129e8ef6 939 $form['privatemsg']['preview'] = array(
3be9375b 940 '#type' => 'submit',
129e8ef6 941 '#value' => t('Preview message'),
3be9375b 942 '#submit' => array('pm_preview'),
174600c8 943 '#validate' => array('pm_send_validate'),
93a1235e 944 '#weight' => 10,
3be9375b 945 );
129e8ef6 946 $form['privatemsg']['submit'] = array(
3be9375b 947 '#type' => 'submit',
129e8ef6 948 '#value' => t('Send message'),
3be9375b 949 '#submit' => array('pm_send'),
174600c8 950 '#validate' => array('pm_send_validate'),
93a1235e 951 '#weight' => 15,
3be9375b 952 );
12a9da26 953 $url = 'messages';
edae41b8 954 $title = t('Cancel');
12a9da26
OT
955 if (isset($_REQUEST['destination'])) {
956 $url = $_REQUEST['destination'];
957 }
68847a38
OT
958 elseif (!is_null($thread_id)) {
959 $url = $_GET['q'];
edae41b8 960 $title = t('Clear');
68847a38 961 }
12a9da26 962
129e8ef6 963 $form['privatemsg']['cancel'] = array(
edae41b8 964 '#value' => l($title, $url, array('attributes' => array('id' => 'edit-cancel'))),
93a1235e 965 '#weight' => 20,
3be9375b 966 );
68847a38
OT
967
968 if (!is_null($thread_id)) {
969 $form['privatemsg']['thread_id'] = array(
970 '#type' => 'value',
971 '#value' => $thread_id,
972 );
973 $form['privatemsg']['subject'] = array(
974 '#type' => 'value',
975 '#default_value' => $subject,
976 );
668e9c20 977 $recipients_string_themed = implode(', ', $to_themed);
68847a38 978 $form['privatemsg']['recipient_display'] = array(
668e9c20 979 '#value' => '<p>'. t('<strong>Reply to thread</strong>:<br /> Recipients: !to', array('!to' => $recipients_string_themed)) .'</p>',
68847a38
OT
980 '#weight' => -10,
981 );
68847a38
OT
982 if (empty($recipients_string)) {
983 // If there are no valid recipients, unset the message reply form.
984 $form['privatemsg']['#access'] = FALSE;
985 }
986 }
4234ba60
OT
987 $form['privatemsg']['read_all'] = array(
988 '#type' => 'value',
989 '#value' => $read_all,
990 );
3be9375b 991 return $form;
29001b19 992}
129e8ef6 993
3be9375b 994function pm_send_validate($form, &$form_state) {
129e8ef6 995 // The actual message that is being sent, we create this during validation and pass to submit to send out.
f5755ddb 996 $message = $form_state['values'];
3be9375b 997 $message['timestamp'] = time();
3be9375b 998
58f02933
OT
999 $trimed_body = trim(truncate_utf8(strip_tags($message['body']), 50, TRUE, TRUE));
1000 if (empty($message['subject']) && !empty($trimed_body)) {
1001 $message['subject'] = $trimed_body;
1002 }
668e9c20 1003 // Only parse the user string for a new thread.
f5755ddb
SG
1004 if (!isset($message['thread_id'])) {
1005 list($message['recipients'], $invalid) = _privatemsg_parse_userstring($message['recipient']);
668e9c20
OT
1006 }
1007 else {
1008 // Load participants.
f5755ddb 1009 $message['recipients'] = _privatemsg_load_thread_participants($message['thread_id']);
668e9c20 1010 // Remove author.
f5755ddb
SG
1011 if (isset($message['recipients'][$message['author']->uid]) && count($message['recipients']) > 1) {
1012 unset($message['recipients'][$message['author']->uid]);
668e9c20
OT
1013 }
1014 }
1b56d511
OT
1015
1016 $validated = _privatemsg_validate_message($message, TRUE);
1017 foreach ($validated['messages'] as $type => $text) {
1018 drupal_set_message($text, $type);
1019 }
1020 $form_state['validate_built_message'] = $message;
1021 if (!empty($invalid)) {
c24db16d 1022 drupal_set_message(t('The following users will not receive this private message: @invalid', array('@invalid' => implode(", ", $invalid))), 'error');
1b56d511
OT
1023 }
1024}
1025
1026/**
668e9c20
OT
1027 * Load all participants of a thread, optionally without author.
1028 *
1029 * @param $thread_id
1030 * Thread ID for wich the participants should be loaded.
1031 */
1032function _privatemsg_load_thread_participants($thread_id) {
1033 $query = _privatemsg_assemble_query('participants', $thread_id);
1034 $result = db_query($query['query']);
1035 $participants = array();
1036 while ($uid = db_fetch_object($result)) {
1037 if (($recipient = user_load($uid->uid))) {
1038 $participants[$recipient->uid] = $recipient;
1039 }
1040 }
1041 return $participants;
1042}
1043
1044/**
1b56d511
OT
1045 * Extract the valid usernames of a string and loads them.
1046 *
1047 * This function is used to parse a string supplied by a username autocomplete
1048 * field and load all user objects.
1049 *
1050 * @param $string
1051 * A string in the form "usernameA, usernameB, ...".
1052 * @return
1053 * Array, first element is an array of loaded user objects, second an array
1054 * with invalid names.
1b56d511 1055 */
668e9c20
OT
1056function _privatemsg_parse_userstring($input) {
1057 if (is_string($input)) {
1058 $input = explode(',', $input);
1059 }
1060
1061 // Start working through the input array.
3be9375b 1062 $invalid = array();
668e9c20
OT
1063 $recipients = array();
1064 foreach ($input as $string) {
1065 $string = trim($string);
1066 if (!empty($string)) { // We don't care about white space names.
1067
1068 // First, check if another module is able to resolve the string into an
1069 // user object.
1070 foreach (module_implements('privatemsg_name_lookup') as $module) {
1071 $function = $module . '_privatemsg_name_lookup';
1072 if (($recipient = $function($string)) && is_object($recipient)) {
1073 // If there is a match, continue with the next input string.
1074 $recipients[$recipient->uid] = $recipient;
1075 continue 2;
1076 }
c9696a92 1077 }
668e9c20
OT
1078 // Fall back to the default username lookup.
1079 if (!$error = module_invoke('user', 'validate_name', $string)) {
1080 // String is a valid username, look it up.
1081 if ($recipient = user_load(array('name' => $string))) {
1082 $recipients[$recipient->uid] = $recipient;
1083 continue;
1084 }
c9696a92 1085 }
668e9c20 1086 $invalid[$string] = $string;
c9696a92 1087 }
c1ab6ef9 1088 }
129e8ef6 1089
668e9c20 1090 return array($recipients, $invalid);
3be9375b 1091}
129e8ef6 1092
e1d62afc
OT
1093/**
1094 * Submit callback for the privatemsg_new form.
1095 */
3be9375b 1096function pm_send($form, &$form_state) {
e1d62afc
OT
1097 $status = _privatemsg_send($form_state['validate_built_message']);
1098 // Load usernames to which the message was sent to.
1099 $recipient_names = array();
1100 foreach ($form_state['validate_built_message']['recipients'] as $recipient) {
1101 $recipient_names[] = theme('username', $recipient);
1102 }
1103 if ($status !== FALSE ) {
3f51bb2b 1104 drupal_set_message(t('A message has been sent to !recipients.', array('!recipients' => implode(', ', $recipient_names))));
6c04b224 1105 }
e1d62afc 1106 else {
d8dd83ca 1107 drupal_set_message(t('An attempt to send a message <em>may have failed</em> when sending to !recipients.', array('!recipients' => implode(', ', $recipient_names))), 'error');
e1d62afc 1108 }
c1ab6ef9 1109}
fb8f4982 1110
3be9375b 1111function pm_preview($form, &$form_state) {
3e0c9a83 1112
3be9375b
OT
1113 drupal_validate_form($form['form_id']['#value'], $form, $form_state);
1114 if (!form_get_errors()) {
129e8ef6 1115 $form_state['privatemsg_preview'] = theme('privatemsg_view', $form_state['validate_built_message']);
3be9375b 1116 }
129e8ef6 1117
5605b147 1118 $form_state['rebuild'] = TRUE; // this forces our form to be rebuilt instead of being submitted.
06203012 1119}
129e8ef6 1120
811e9fd5
SG
1121/**
1122 * @addtogroup sql
1123 * @{
1124 */
1125
1126/**
1127 * Query definition to load a list of threads.
1128 *
1129 * @param $fragments
8086d49e 1130 * Query fragments array.
811e9fd5 1131 * @param $account
8086d49e 1132 * User object for which the messages are being loaded.
811e9fd5 1133 * @param $argument
8086d49e
SG
1134 * String argument which can be used in the query builder to modify the
1135 * thread listing.
811e9fd5
SG
1136 */
1137
c277e471 1138function privatemsg_sql_list(&$fragments, $account, $argument = 'list') {
3be9375b 1139 $fragments['primary_table'] = '{pm_message} pm';
129e8ef6 1140
5b3def32 1141 // Load enabled columns.
c277e471 1142 $fields = array_filter(variable_get('privatemsg_display_fields', array('participants')));
4ba95ee6 1143
5b3def32 1144 // Required columns.
129e8ef6 1145 $fragments['select'][] = 'pmi.thread_id';
5b3def32
SG
1146 // We have to use MIN as the subject might not be the same in some threads.
1147 // MIN() does not have a useful meaning except that it helps to correctly
1148 // aggregate the thread on PostgreSQL.
129e8ef6 1149 $fragments['select'][] = 'MIN(pm.subject) as subject';
fbbe3e72 1150 $fragments['select'][] = 'MAX(pm.timestamp) as last_updated';
5b3def32 1151 // We use SUM so that we can count the number of unread messages.
6aad28b8 1152 $fragments['select'][] = 'SUM(pmi.is_new) as is_new';
b9fca179 1153
5b3def32
SG
1154 // Select number of messages in the thread if the count is
1155 // set to be displayed.
4ba95ee6 1156 if (in_array('count', $fields)) {
5b3def32 1157 $fragments['select'][] = 'COUNT(distinct pmi.mid) as count';
4ba95ee6 1158 }
c277e471 1159 if (in_array('participants', $fields)) {
5b3def32
SG
1160 // Query for a string with uid's, for example "1,6,7".
1161 // @todo: Replace this with a single query similiar to the tag list.
4ba95ee6 1162 if ($GLOBALS['db_type'] == 'pgsql') {
5b3def32 1163 // PostgreSQL does not know GROUP_CONCAT, so a subquery is required.
8086d49e 1164 $fragments['select'][] = "array_to_string(array(SELECT DISTINCT textin(int4out(pmia.uid))
c277e471
OT
1165 FROM {pm_index} pmia
1166 WHERE pmia.thread_id = pmi.thread_id), ',') AS participants";
4ba95ee6
OT
1167 }
1168 else {
8086d49e 1169 $fragments['select'][] = '(SELECT GROUP_CONCAT(DISTINCT pmia.uid SEPARATOR ",")
c277e471
OT
1170 FROM {pm_index} pmia
1171 WHERE pmia.thread_id = pmi.thread_id) AS participants';
4ba95ee6
OT
1172 }
1173 }
1174 if (in_array('thread_started', $fields)) {
1175 $fragments['select'][] = 'MIN(pm.timestamp) as thread_started';
1176 }
5b3def32 1177
f2161b8c 1178 $fragments['inner_join'][] = 'INNER JOIN {pm_index} pmi ON pm.mid = pmi.mid';
5b3def32
SG
1179
1180 // Only load undeleted messages of the current user and group by thread.
129e8ef6 1181 $fragments['where'][] = 'pmi.uid = %d';
19ee9333 1182 $fragments['query_args']['where'][] = $account->uid;
129e8ef6
OT
1183 $fragments['where'][] = 'pmi.deleted = 0';
1184 $fragments['group_by'][] = 'pmi.thread_id';
129e8ef6 1185
a00d885c
SG
1186 $order_by_first = 'MAX(pmi.is_new) DESC, ';
1187 // MySQL 4.1 does not allow to order by aggregate functions. MAX() is used
1188 // to avoid a ordering bug with multiple new messages.
1189 if ($GLOBALS['db_type'] != 'pgsql' && version_compare(db_version(), '5.0.0') < 0) {
1190 $order_by_first = 'is_new DESC, ';
1191 }
1192
8a7e7d6c
SG
1193 // tablesort_sql() generates a ORDER BY string. However, the "ORDER BY " part
1194 // is not needed and added by the query builder. Discard the first 9
1195 // characters of the string.
a00d885c 1196 $order_by = drupal_substr(tablesort_sql(_privatemsg_list_headers( FALSE, array_merge(array('subject', 'last_updated'), $fields)), $order_by_first), 9);
4ba95ee6 1197 $fragments['order_by'][] = $order_by;
c1ab6ef9
KN
1198}
1199
5605b147 1200/**
22ae69fd 1201 * Query function for loading a single or multiple messages.
bf1e88fc
OT
1202 *
1203 * @param $fragments
1204 * Query fragments array.
22ae69fd
SG
1205 * @param $pmids
1206 * Array of pmids.
1207 * @param $account
1208 * Account for which the messages should be loaded.
5605b147 1209 */
22ae69fd 1210function privatemsg_sql_load(&$fragments, $pmids, $account = NULL) {
5b3def32 1211 $fragments['primary_table'] = '{pm_message} pm';
129e8ef6
OT
1212
1213 $fragments['select'][] = "pm.mid";
1214 $fragments['select'][] = "pm.author";
1215 $fragments['select'][] = "pm.subject";
1216 $fragments['select'][] = "pm.body";
1217 $fragments['select'][] = "pm.timestamp";
811e9fd5 1218 $fragments['select'][] = "pm.format";
129e8ef6 1219 $fragments['select'][] = "pmi.is_new";
ca5ce8f9 1220 $fragments['select'][] = "pmi.thread_id";
129e8ef6
OT
1221
1222 $fragments['inner_join'][] = 'INNER JOIN {pm_index} pmi ON pm.mid = pmi.mid';
5b3def32 1223 // Use IN() to load multiple messages at the same time.
22ae69fd
SG
1224 $fragments['where'][] = 'pmi.mid IN (' . db_placeholders($pmids) . ')';
1225 $fragments['query_args']['where'] += $pmids;
bf1e88fc
OT
1226 if ($account) {
1227 $fragments['where'][] = 'pmi.uid = %d';
1228 $fragments['query_args']['where'][] = $account->uid;
1229 }
174600c8 1230 $fragments['order_by'][] = 'pm.timestamp ASC';
4dde25cf 1231 $fragments['order_by'][] = 'pm.mid ASC';
129e8ef6 1232}
bf1e88fc 1233
4ba95ee6
OT
1234/**
1235 * Query definition to load messages of one or multiple threads.
1236 *
1237 * @param $fragments
bf1e88fc 1238 * Query fragments array.
4ba95ee6 1239 * @param $threads
bf1e88fc 1240 * Array with one or multiple thread id's.
4ba95ee6 1241 * @param $account
bf1e88fc 1242 * User object for which the messages are being loaded.
4ba95ee6 1243 * @param $load_all
bf1e88fc 1244 * Deleted messages are only loaded if this is set to TRUE.
4ba95ee6 1245 */
bf1e88fc 1246function privatemsg_sql_messages(&$fragments, $threads, $account = NULL, $load_all = FALSE) {
129e8ef6
OT
1247 $fragments['primary_table'] = '{pm_index} pmi';
1248
077e5644 1249 $fragments['select'][] = 'pmi.mid';
4ba95ee6 1250 $fragments['where'][] = 'pmi.thread_id IN ('. db_placeholders($threads) .')';
19ee9333 1251 $fragments['query_args']['where'] += $threads;
174600c8 1252 $fragments['inner_join'][] = 'INNER JOIN {pm_message} pm ON (pm.mid = pmi.mid)';
bf1e88fc
OT
1253 if ($account) {
1254 // Only load the user's messages.
1255 $fragments['where'][] = 'pmi.uid = %d';
1256 $fragments['query_args']['where'][] = $account->uid;
1257 }
4ba95ee6 1258 if (!$load_all) {
5b3def32 1259 // Also load deleted messages when requested.
077e5644 1260 $fragments['where'][] = 'pmi.deleted = 0';
4ba95ee6 1261 }
077e5644
SG
1262 // Only load each mid once.
1263 $fragments['group_by'][] = 'pmi.mid';
3bb166c1
SG
1264 $fragments['group_by'][] = 'pm.timestamp';
1265
077e5644 1266 // Order by timestamp first.
174600c8 1267 $fragments['order_by'][] = 'pm.timestamp ASC';
077e5644
SG
1268 // If there are multiple inserts during the same second (tests, for example)
1269 // sort by mid second to have them in the same order as they were saved.
3bb166c1 1270 $fragments['order_by'][] = 'pmi.mid ASC';
077e5644 1271
c1ab6ef9 1272}
04b84abf 1273
cfbf1a54
OT
1274/**
1275 * Load all participants of a thread.
1276 *
1277 * @param $fragments
1278 * Query fragments array.
1279 * @param $thread_id
1280 * Thread id from which the participants should be loaded.
1281 */
f2161b8c 1282function privatemsg_sql_participants(&$fragments, $thread_id) {
3be9375b 1283 $fragments['primary_table'] = '{pm_index} pmi';
129e8ef6 1284
5b3def32
SG
1285 // Only load each participant once since they are listed as recipient for
1286 // every message of that thread.
cfbf1a54
OT
1287 $fragments['select'][] = 'DISTINCT(pmi.uid) AS uid';
1288 $fragments['select'][] = 'u.name AS name';
1289
1290 $fragments['inner_join'][] = 'INNER JOIN {users} u ON (u.uid = pmi.uid)';
129e8ef6 1291 $fragments['where'][] = 'pmi.thread_id = %d';
19ee9333 1292 $fragments['query_args']['where'][] = $thread_id;
fb8f4982
OT
1293}
1294
5b3def32
SG
1295/**
1296 * Query definition to count unread messages.
1297 *
1298 * @param $fragments
1299 * Query fragments array.
1300 * @param $account
1301 * User object for which the messages are being counted.
1302 */
f2161b8c 1303function privatemsg_sql_unread_count(&$fragments, $account) {
129e8ef6
OT
1304 $fragments['primary_table'] = '{pm_index} pmi';
1305
1306 $fragments['select'][] = 'COUNT(DISTINCT thread_id) as unread_count';
5b3def32
SG
1307
1308 // Only count new messages that have not been deleted.
129e8ef6
OT
1309 $fragments['where'][] = 'pmi.deleted = 0';
1310 $fragments['where'][] = 'pmi.is_new = 1';
1311 $fragments['where'][] = 'pmi.uid = %d';
19ee9333 1312 $fragments['query_args']['where'][] = $account->uid;
129e8ef6
OT
1313}
1314
5b3def32
SG
1315/**
1316 * Query definition to search for username autocomplete suggestions.
1317 *
1318 * @param $fragments
1319 * Query fragments array.
1320 * @param $search
1321 * Which search string is currently searched for.
1322 * @param $names
1323 * Array of names not to be used as suggestions.
1324 */
afbd3810
OT
1325function privatemsg_sql_autocomplete(&$fragments, $search, $names) {
1326 $fragments['primary_table'] = '{users} u';
1327 $fragments['select'][] = 'u.name';
5b3def32 1328 // Escape the % to get it through the placeholder replacement.
afbd3810 1329 $fragments['where'][] = "u.name LIKE '%s'";
19ee9333 1330 $fragments['query_args']['where'][] = $search .'%%';
afbd3810 1331 if (!empty($names)) {
5b3def32 1332 // If there are already names selected, exclude them from the suggestions.
afbd3810 1333 $fragments['where'][] = "u.name NOT IN (". db_placeholders($names, 'text') .")";
19ee9333 1334 $fragments['query_args']['where'] += $names;
afbd3810 1335 }
5b3def32 1336 // Only load active users and sort them by name.
afbd3810
OT
1337 $fragments['where'][] = 'u.status <> 0';
1338 $fragments['order_by'][] = 'u.name ASC';
1339}
1340
fb8f4982 1341/**
7bfff1e7
SG
1342 * Query Builder function to load all messages that should be flushed.
1343 *
1344 * @param $fragments
1345 * Query fragments array.
1346 * @param $days
1347 * Select messages older than x days.
1348 */
1349function privatemsg_sql_deleted(&$fragments, $days) {
1350 $fragments['primary_table'] = '{pm_message} pm';
1351
1352 $fragments['select'][] = 'pm.mid';
5b3def32 1353 // The lowest value is higher than 0 if all recipients have deleted a message.
7bfff1e7 1354 $fragments['select'][] = 'MIN(pmi.deleted) as is_deleted';
5b3def32 1355 // The time the most recent deletion happened.
7bfff1e7
SG
1356 $fragments['select'][] = 'MAX(pmi.deleted) as last_deleted';
1357
1358 $fragments['inner_join'][] = 'INNER JOIN {pm_index} pmi ON (pmi.mid = pm.mid)';
1359
1360 $fragments['group_by'][] = 'pm.mid';
1361
5b3def32 1362 // Ignore messages that have not been deleted by all users.
7bfff1e7 1363 $fragments['having'][] = 'MIN(pmi.deleted) > 0';
5b3def32
SG
1364
1365 // Only select messages that have been deleted more than n days ago.
7bfff1e7
SG
1366 $fragments['having'][] = 'MAX(pmi.deleted) < %d';
1367 $fragments['query_args']['having'][] = time() - $days * 86400;
1368}
1369
1370/**
5605b147
OT
1371 * @}
1372 */
1373
1374/**
fb8f4982 1375 * Return autocomplete results for usernames.
5605b147 1376 *
129e8ef6 1377 * Prevents usernames from being used and/or suggested twice.
fb8f4982
OT
1378 */
1379function privatemsg_user_name_autocomplete($string) {
1380 $names = array();
129e8ef6 1381 // 1: Parse $string and build list of valid user names.
fb8f4982
OT
1382 $fragments = explode(',', $string);
1383 foreach ($fragments as $index => $name) {
668e9c20 1384 if ($name = trim($name)) {
fb8f4982
OT
1385 $names[$name] = $name;
1386 }
1387 }
129e8ef6
OT
1388 // By using user_validate_user we can ensure that names included in $names are at least logisticaly possible.
1389 // 2: Find the next user name suggestion.
fb8f4982 1390 $fragment = array_pop($names);
56c677a0 1391 $matches = array();
fb8f4982 1392 if (!empty($fragment)) {
afbd3810
OT
1393 $query = _privatemsg_assemble_query('autocomplete', $fragment, $names);
1394 $result = db_query_range($query['query'], $fragment, 0, 10);
129e8ef6
OT
1395 $prefix = count($names) ? implode(", ", $names) .", " : '';
1396 // 3: Build proper suggestions and print.
fb8f4982 1397 while ($user = db_fetch_object($result)) {
129e8ef6 1398 $matches[$prefix . $user->name .", "] = $user->name;
fb8f4982 1399 }
93a1235e 1400 }
56c677a0
OT
1401 // convert to object to prevent drupal bug, see http://drupal.org/node/175361
1402 drupal_json((object)$matches);
fb8f4982
OT
1403}
1404
129e8ef6
OT
1405function privatemsg_user($op, &$edit, &$account, $category = NULL) {
1406 global $user;
1407
129e8ef6
OT
1408 switch ($op) {
1409 case 'view':
68847a38 1410 if ($url = privatemsg_get_link(array($account))) {
129e8ef6
OT
1411 $account->content['privatemsg_send_new_message'] = array(
1412 '#type' => 'markup',
93a1235e 1413 '#value' => l(t('Send this user a message'), $url, array('query' => drupal_get_destination())),
129e8ef6
OT
1414 '#weight' => 10,
1415 );
1416 }
1417 break;
1418 case 'login':
bc4689a7 1419 if (variable_get('privatemsg_display_loginmessage', TRUE) && privatemsg_user_access()) {
20b4f5d2
OT
1420 $count = privatemsg_unread_count();
1421 if ($count) {
acc45eb6 1422 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'))));
20b4f5d2 1423 }
129e8ef6
OT
1424 }
1425 break;
85ae2855
SG
1426 case 'delete':
1427
1428 // Load all mids of the messages the user wrote.
1429 $result = db_query("SELECT mid FROM {pm_message} WHERE author = %d", $account->uid);
1430 $mids = array();
1431 while ($row = db_fetch_array($result)) {
1432 $mids[] = $row['mid'];
1433 }
1434
1435 // Delete messages the user wrote.
1436 db_query('DELETE FROM {pm_message} WHERE author = %d', $account->uid);
1437
46a56689
SG
1438 if (!empty($mids)) {
1439 // Delete recipient entries in {pm_index} of the messages the user wrote.
1440 db_query('DELETE FROM {pm_index} WHERE mid IN (' . db_placeholders($mids) . ')', $mids);
1441 }
85ae2855
SG
1442
1443 // Delete recipient entries of that user.
1444 db_query('DELETE FROM {pm_index} WHERE uid = %d', $account->uid);
1445 break;
129e8ef6
OT
1446 }
1447}
1448
1449function privatemsg_block($op = 'list', $delta = 0, $edit = array()) {
1450 if ('list' == $op) {
1451 $blocks = array();
1452 $blocks['privatemsg-menu'] = array(
1453 'info' => t('Privatemsg links'),
7deb0c62 1454 'cache' => BLOCK_NO_CACHE,
129e8ef6 1455 );
20b4f5d2
OT
1456 $blocks['privatemsg-new'] = array(
1457 'info' => t('New message indication'),
7deb0c62 1458 'cache' => BLOCK_NO_CACHE,
20b4f5d2 1459 );
129e8ef6
OT
1460
1461 return $blocks;
1462 }
5605b147 1463 elseif ('view' == $op) {
129e8ef6
OT
1464 $block = array();
1465 switch ($delta) {
1466 case 'privatemsg-menu':
1467 $block = _privatemsg_block_menu();
1468 break;
20b4f5d2
OT
1469 case 'privatemsg-new':
1470 $block = _privatemsg_block_new();
1471 break;
129e8ef6
OT
1472 }
1473 return $block;
1474 }
1475}
1476
8960261c 1477function privatemsg_title_callback($title = NULL) {
129e8ef6 1478 $count = privatemsg_unread_count();
129e8ef6 1479
b1d8b30e 1480 if ($count > 0) {
924b247b 1481 return format_plural($count, 'Messages (1 new)', 'Messages (@count new)');
b1d8b30e
OT
1482 }
1483 return t('Messages');
129e8ef6
OT
1484}
1485
1486
20b4f5d2
OT
1487function _privatemsg_block_new() {
1488 $block = array();
1489
bc4689a7 1490 if (!privatemsg_user_access()) {
20b4f5d2
OT
1491 return $block;
1492 }
1493
1494 $count = privatemsg_unread_count();
1495 if ($count) {
1496 $block = array(
1497 'subject' => format_plural($count, 'New message', 'New messages'),
1498 'content' => theme('privatemsg_new_block', $count),
1499 );
1500 return $block;
1501 }
1502 return array();
1503}
1504
129e8ef6
OT
1505function _privatemsg_block_menu() {
1506 $block = array();
1507
129e8ef6 1508 $links = array();
bc4689a7 1509 if (privatemsg_user_access('write privatemsg')) {
129e8ef6
OT
1510 $links[] = l(t('Write new message'), 'messages/new');
1511 }
bc4689a7 1512 if (privatemsg_user_access('read privatemsg') || privatemsg_user_access('read all private messages') ) {
8960261c 1513 $links[] = l(privatemsg_title_callback(), 'messages');
129e8ef6
OT
1514 }
1515 if ( count( $links ) ) {
1516 $block = array(
8c6d3b74 1517 'subject' => t('Private messages'),
129e8ef6
OT
1518 'content' => theme('item_list', $links),
1519 );
1520 }
1521 return $block;
1522}
1523
ca5ce8f9 1524function privatemsg_delete($form_state, $thread, $message) {
129e8ef6
OT
1525 $form['pmid'] = array(
1526 '#type' => 'value',
ca5ce8f9
SG
1527 '#value' => $message['mid'],
1528 );
1529 $form['delete_destination'] = array(
1530 '#type' => 'value',
1531 '#value' => count($thread['messages']) > 1 ? 'messages/view/' . $message['thread_id'] : 'messages',
129e8ef6 1532 );
ca5ce8f9 1533
bf1e88fc
OT
1534 if (privatemsg_user_access('read all private messages')) {
1535 $form['delete_options'] = array(
1536 '#type' => 'checkbox',
12d416d0
SG
1537 '#title' => t('Delete this message for all users?'),
1538 '#description' => t('Tick the box to delete the message for all users.'),
bf1e88fc
OT
1539 '#default_value' => FALSE,
1540 );
1541 }
129e8ef6 1542 return confirm_form($form,
bf1e88fc 1543 t('Are you sure you want to delete this message?'),
ca5ce8f9 1544 isset($_GET['destination']) ? $_GET['destination'] : 'messages/view/'. $message['thread_id'],
129e8ef6
OT
1545 t('This action cannot be undone.'),
1546 t('Delete'),
1547 t('Cancel')
1548 );
1549}
1550
bf1e88fc
OT
1551function privatemsg_delete_submit($form, &$form_state) {
1552 global $user;
1553 $account = drupal_clone($user);
8086d49e 1554
bf1e88fc 1555 if ($form_state['values']['confirm']) {
ca5ce8f9 1556 if (isset($form_state['values']['delete_options']) && $form_state['values']['delete_options']) {
bf1e88fc 1557 privatemsg_message_change_delete($form_state['values']['pmid'], 1);
ca5ce8f9 1558 drupal_set_message(t('Message has been deleted for all users.'));
bf1e88fc
OT
1559 }
1560 else {
1561 privatemsg_message_change_delete($form_state['values']['pmid'], 1, $account);
ca5ce8f9 1562 drupal_set_message(t('Message has been deleted.'));
bf1e88fc
OT
1563 }
1564 }
ca5ce8f9 1565 $form_state['redirect'] = $form_state['values']['delete_destination'];
bf1e88fc
OT
1566}
1567
5605b147
OT
1568/**
1569 * Delete or restore a message.
1570 *
1571 * @param $pmid
1572 * Message id, pm.mid field.
1573 * @param $delete
1574 * Either deletes or restores the thread (1 => delete, 0 => restore)
1575 * @param $account
8086d49e
SG
1576 * User acccount for which the delete action should be carried out - Set to
1577 * NULL to delete for all users.
5605b147
OT
1578 *
1579 * @ingroup api
1580 */
4ba95ee6 1581function privatemsg_message_change_delete($pmid, $delete, $account = NULL) {
7bfff1e7
SG
1582 $delete_value = 0;
1583 if ($delete == TRUE) {
1584 $delete_value = time();
1585 }
1586
8086d49e 1587 if ($account) {
7bfff1e7 1588 db_query('UPDATE {pm_index} SET deleted = %d WHERE mid = %d AND uid = %d', $delete_value, $pmid, $account->uid);
bf1e88fc
OT
1589 }
1590 else {
1591 // Mark deleted for all users.
7bfff1e7 1592 db_query('UPDATE {pm_index} SET deleted = %d WHERE mid = %d', $delete_value, $pmid);
93a1235e 1593 }
129e8ef6 1594}
e1d62afc 1595
93a1235e 1596/**
5605b147 1597 * Send a new message.
93a1235e
OT
1598 *
1599 * This functions does send a message in a new thread.
5605b147
OT
1600 * Example:
1601 * @code
cb86482d 1602 * privatemsg_new_thread(array(user_load(5)), 'The subject', 'The body text');
5605b147
OT
1603 * @endcode
1604 *
4fa130d7
OT
1605 * @param $recipients
1606 * Array of recipients (user objects)
5605b147
OT
1607 * @param $subject
1608 * The subject of the new message
1609 * @param $body
1610 * The body text of the new message
4fa130d7
OT
1611 * @param $options
1612 * Additional options, possible keys:
1613 * author => User object of the author
1614 * timestamp => Time when the message was sent
93a1235e 1615 *
5605b147 1616 * @return
e1d62afc
OT
1617 * An array with a key success. If TRUE, it also contains a key 'message' with
1618 * the created $message array, the same that is passed to the insert hook.
1619 * If FALSE, it contains a key 'messages'. This key contains an array where
1620 * the key is the error type (error, warning, notice) and an array with
1621 * messages of that type.
1622 *
8086d49e
SG
1623 * It is theoretically possible for success to be TRUE and message to be
1624 * FALSE. For example if one of the privatemsg database tables become
1625 * corrupted. When testing for success of message being sent it is always
1626 * best to see if ['message'] is not FALSE as well as ['success'] is TRUE.
e1d62afc
OT
1627 *
1628 * Example:
1629 * @code
1630 * array('error' => array('A error message'))
1631 * @endcode
5605b147
OT
1632 *
1633 * @ingroup api
93a1235e 1634 */
4fa130d7
OT
1635function privatemsg_new_thread($recipients, $subject, $body = NULL, $options = array()) {
1636 global $user;
1637 $author = drupal_clone($user);
129e8ef6 1638
93a1235e
OT
1639 $message = array();
1640 $message['subject'] = $subject;
93a1235e 1641 $message['body'] = $body;
3852ecf8
SG
1642 // Make sure that recipients are keyed by user id and are not added
1643 // multiple times.
1644 foreach ($recipients as $recipient) {
1645 $message['recipients'][$recipient->uid] = $recipient;
1646 }
93a1235e 1647
cb86482d 1648 // Set custom options, if any.
4fa130d7
OT
1649 if (!empty($options)) {
1650 $message += $options;
93a1235e 1651 }
cb86482d 1652 // Apply defaults - this will not overwrite existing keys.
4fa130d7
OT
1653 $message += array(
1654 'author' => $author,
1655 'timestamp' => time(),
ed18b9f3 1656 'format' => filter_resolve_format(FILTER_FORMAT_DEFAULT),
4fa130d7
OT
1657 );
1658
1659 $validated = _privatemsg_validate_message($message);
1660 if ($validated['success']) {
e1d62afc 1661 $validated['message'] = _privatemsg_send($message);
93a1235e 1662 }
4fa130d7
OT
1663
1664 return $validated;
93a1235e 1665}
811e9fd5 1666
93a1235e
OT
1667/**
1668 * Send a reply message
1669 *
1670 * This functions replies on an existing thread.
1671 *
5605b147
OT
1672 * @param $thread_id
1673 * Thread id
1674 * @param $body
1675 * The body text of the new message
4fa130d7
OT
1676 * @param $options
1677 * Additional options, possible keys:
1678 * author => User object of the author
1679 * timestamp => Time when the message was sent
5605b147
OT
1680 *
1681 * @return
e1d62afc
OT
1682 * An array with a key success and messages. This key contains an array where
1683 * the key is the error type (error, warning, notice) and an array with
1684 * messages of that type.. If success is TRUE, it also contains a key $message
1685 * with the created $message array, the same that is passed to
1686 * hook_privatemsg_message_insert().
1687 *
8086d49e
SG
1688 * It is theoretically possible for success to be TRUE and message to be
1689 * FALSE. For example if one of the privatemsg database tables become
1690 * corrupted. When testing for success of message being sent it is always
1691 * best to see if ['message'] is not FALSE as well as ['success'] is TRUE.
e1d62afc
OT
1692 *
1693 * Example messages values:
1694 * @code
1695 * array('error' => array('A error message'))
1696 * @endcode
93a1235e 1697 *
5605b147 1698 * @ingroup api
93a1235e 1699 */
86ee5a80 1700function privatemsg_reply($thread_id, $body, $options = array()) {
4fa130d7
OT
1701 global $user;
1702 $author = drupal_clone($user);
93a1235e
OT
1703
1704 $message = array();
93a1235e 1705 $message['body'] = $body;
4fa130d7
OT
1706
1707 // set custom options, if any
1708 if (!empty($options)) {
1709 $message += $options;
1710 }
1711 // apply defaults
1712 $message += array(
1713 'author' => $author,
1714 'timestamp' => time(),
ed18b9f3 1715 'format' => filter_resolve_format(FILTER_FORMAT_DEFAULT),
4fa130d7 1716 );
93a1235e
OT
1717
1718 // We don't know the subject and the recipients, so we need to load them..
1719 // thread_id == mid on the first message of the thread
22ae69fd 1720 $first_message = privatemsg_message_load($thread_id, $message['author']);
93a1235e
OT
1721 if (!$first_message) {
1722 return array(t('Thread %thread_id not found, unable to answer', array('%thread_id' => $thread_id)));
1723 }
1724
89f2c1af 1725 $message['thread_id'] = $thread_id;
47b30a8b 1726 // Load participants.
668e9c20 1727 $message['recipients'] = _privatemsg_load_thread_participants($thread_id);
47b30a8b
SG
1728 // Remove author.
1729 if (isset($message['recipients'][$message['author']->uid]) && count($message['recipients']) > 1) {
1730 unset($message['recipients'][$message['author']->uid]);
1731 }
93a1235e
OT
1732 $message['subject'] = $first_message['subject'];
1733
4fa130d7
OT
1734 $validated = _privatemsg_validate_message($message);
1735 if ($validated['success']) {
e1d62afc 1736 $validated['message'] = _privatemsg_send($message);
93a1235e 1737 }
4fa130d7 1738 return $validated;
93a1235e
OT
1739}
1740
4fa130d7
OT
1741function _privatemsg_validate_message(&$message, $form = FALSE) {
1742 $messages = array('error' => array(), 'warning' => array());
1743 if (!privatemsg_user_access('write privatemsg', $message['author'])) {
93a1235e 1744 // no need to do further checks in this case...
4fa130d7
OT
1745 if ($form) {
1746 form_set_error('author', t('User @user is not allowed to write messages', array('@user' => $message['author']->name)));
1747 return array(
1748 'success' => FALSE,
1749 'messages' => $messages,
1750 );
1751 }
1752 else {
1753 $messages['error'][] = t('User @user is not allowed to write messages', array('@user' => $message['author']->name));
1754 return array(
1755 'success' => FALSE,
1756 'messages' => $messages,
1757 );
1758 }
129e8ef6
OT
1759 }
1760
93a1235e 1761 if (empty($message['subject'])) {
4fa130d7
OT
1762 if ($form) {
1763 form_set_error('subject', t('Disallowed to send a message without subject'));
1764 }
1765 else {
1766 $messages['error'][] = t('Disallowed to send a message without subject');
1767 }
93a1235e
OT
1768 }
1769
86ee5a80
SG
1770 // Don't allow replies without a body.
1771 if (!empty($message['thread_id']) && empty($message['body'])) {
1772 if ($form) {
1773 form_set_error('body', t('Disallowed to send reply without a message.'));
1774 }
1775 else {
1776 $messages['error'][] = t('Disallowed to send reply without a message.');
1777 }
1778 }
ed18b9f3
SG
1779 // Check if an allowed format is used. global $user needs to be changed since
1780 // it is not possible to do the check for a specific user.
1781 global $user;
1782 $original_user = drupal_clone($user);
1783 session_save_session(FALSE);
1784 $user = $message['author'];
1785
1786 if (!filter_access($message['format'])) {
1787 if ($form) {
1788 form_set_error('format', t('You are not allowed to use the specified input format.'));
1789 }
1790 else {
1791 $messages['error'][] = t('User @user is not allowed to use the specified input format.', array('@user' => $message['author']->name));
1792 }
1793 }
1794
1795 $user = $original_user;
1796 session_save_session(TRUE);
86ee5a80 1797
93a1235e 1798 if (empty($message['recipients']) || !is_array($message['recipients'])) {
4fa130d7 1799 if ($form) {
47b29935 1800 form_set_error('to', t('Disallowed to send a message without at least one valid recipient'));
4fa130d7
OT
1801 }
1802 else {
47b29935 1803 $messages['error'][] = t('Disallowed to send a message without at least one valid recipient');
4fa130d7 1804 }
93a1235e
OT
1805 }
1806
1807 if (!empty($message['recipients']) && is_array($message['recipients'])) {
8086d49e 1808 foreach (module_invoke_all('privatemsg_block_message', $message['author'], $message['recipients']) as $blocked) {
b9fca179 1809 unset($message['recipients'][$blocked['uid']]);
6f9a6e13
OT
1810 if ($form) {
1811 drupal_set_message($blocked['message'], 'warning');
8086d49e
SG
1812 }
1813 else {
6f9a6e13
OT
1814 $messages['warning'][] = $blocked['message'];
1815 }
93a1235e
OT
1816 }
1817 }
1818
1819 // Check again, give another error message if all recipients are blocked
1820 if (empty($message['recipients'])) {
4fa130d7
OT
1821 if ($form) {
1822 form_set_error('to', t('Disallowed to send message because all recipients are blocked'));
1823 }
1824 else {
1825 $messages['error'][] = t('Disallowed to send message because all recipients are blocked');
1826 }
93a1235e
OT
1827 }
1828
4fa130d7 1829 $messages += module_invoke_all('privatemsg_message_validate', $message, $form);
e1d62afc
OT
1830 // Check if there are errors in $messages or if $form is TRUE, there are form errors.
1831 $success = empty($messages['error']) || ($form && count((array)form_get_errors()) > 0);
4fa130d7
OT
1832 return array(
1833 'success' => $success,
1834 'messages' => $messages,
1835 );
129e8ef6
OT
1836}
1837
e1d62afc
OT
1838/**
1839 * Internal function to save a message.
1840 *
1841 * @param $message
1842 * A $message array with the data that should be saved. If a thread_id exists
1843 * it will be created as a reply to an existing thread. If not, a new thread
1844 * will be created.
1845 *
1846 * @return
1847 * The updated $message array.
1848 */
93a1235e
OT
1849function _privatemsg_send($message) {
1850
1851 drupal_alter('privatemsg_message_presave', $message);
1852
de0db33f
SG
1853 $index_sql = "INSERT INTO {pm_index} (mid, thread_id, uid, is_new, deleted) VALUES (%d, %d, %d, %d, 0)";
1854 if (isset($message['read_all']) && $message['read_all']) {
1855 // The message was sent in read all mode, add the author as recipient to all
1856 // existing messages.
1857 $query_messages = _privatemsg_assemble_query('messages', array($message['thread_id']), NULL);
1858 $conversation = db_query($query_messages['query']);
1859 while ($result = db_fetch_array($conversation)) {
1860 if (!db_query($index_sql, $result['mid'], $message['thread_id'], $message['author']->uid, 0)) {
1861 return FALSE;
1862 }
1863 }
1864 }
1865
93a1235e
OT
1866 // 1) Save the message body first.
1867 $args = array();
1868 $args[] = $message['subject'];
1869 $args[] = $message['author']->uid;
1870 $args[] = $message['body'];
811e9fd5 1871 $args[] = $message['format'];
93a1235e 1872 $args[] = $message['timestamp'];
de0db33f
SG
1873 $message_sql = "INSERT INTO {pm_message} (subject, author, body, format, timestamp) VALUES ('%s', %d, '%s', %d, %d)";
1874 db_query($message_sql, $args);
93a1235e
OT
1875 $mid = db_last_insert_id('pm_message', 'mid');
1876 $message['mid'] = $mid;
1877
1878 // Thread ID is the same as the mid if it's the first message in the thread.
1879 if (!isset($message['thread_id'])) {
1880 $message['thread_id'] = $mid;
1881 }
1882
1883 // 2) Save message to recipients.
1884 // Each recipient gets a record in the pm_index table.
93a1235e 1885 foreach ($message['recipients'] as $recipient) {
de0db33f 1886 if (!db_query($index_sql, $mid, $message['thread_id'], $recipient->uid, 1) ) {
bf1e88fc
OT
1887 // We assume if one insert failed then the rest may fail too against the
1888 // same table.
1889 return FALSE;
e1d62afc 1890 }
93a1235e 1891 }
0ffefdb2 1892
bf1e88fc
OT
1893 // When author is also the recipient, we want to set message to UNREAD.
1894 // All other times the message is set to READ.
0ffefdb2
OT
1895 $is_new = isset($message['recipients'][$message['author']->uid]) ? 1 : 0;
1896
f36e317a 1897 // Also add a record for the author to the pm_index table.
de0db33f 1898 if (!db_query($index_sql, $mid, $message['thread_id'], $message['author']->uid, $is_new)) {
bf1e88fc 1899 return FALSE;
e1d62afc 1900 }
93a1235e
OT
1901
1902 module_invoke_all('privatemsg_message_insert', $message);
1903
bf1e88fc
OT
1904 // If we reached here that means we were successful at writing all messages to db.
1905 return $message;
129e8ef6
OT
1906}
1907
93a1235e
OT
1908/**
1909 * Returns a link to send message form for a specific users.
1910 *
1911 * Contains permission checks of author/recipient, blocking and
1912 * if a anonymous user is involved.
1913 *
5605b147
OT
1914 * @param $recipient
1915 * Recipient of the message
1916 * @param $account
1917 * Sender of the message, defaults to the current user
1918 *
1919 * @return
1920 * Either FALSE or a URL string
93a1235e 1921 *
5605b147 1922 * @ingroup api
93a1235e 1923 */
68847a38 1924function privatemsg_get_link($recipients, $account = array(), $subject = NULL) {
93a1235e
OT
1925 if ($account == NULL) {
1926 global $user;
1927 $account = $user;
1928 }
76d28f2a 1929
68847a38
OT
1930 if (!is_array($recipients)) {
1931 $recipients = array($recipients);
1932 }
93a1235e 1933
68847a38 1934 if (!privatemsg_user_access('write privatemsg', $account) || $account->uid == 0) {
93a1235e
OT
1935 return FALSE;
1936 }
1937
68847a38
OT
1938 $validated = array();
1939 foreach ($recipients as $recipient) {
1940 if (!privatemsg_user_access('read privatemsg', $recipient)) {
1941 continue;
1942 }
593c76b8 1943 if (count(module_invoke_all('privatemsg_block_message', $account, array($recipient->uid => $recipient))) > 0) {
68847a38
OT
1944 continue;
1945 }
1946 $validated[] = $recipient->uid;
1947 }
1948 if (empty($validated)) {
93a1235e
OT
1949 return FALSE;
1950 }
68847a38
OT
1951 $url = 'messages/new/'. implode(',', $validated);
1952 if (!is_null($subject)) {
1953 $url .= '/'. $subject;
1954 }
1955 return $url;
93a1235e 1956}
efb7face
OT
1957
1958/**
5605b147 1959 * Load a single message.
efb7face 1960 *
5605b147
OT
1961 * @param $pmid
1962 * Message id, pm.mid field
1963 * @param $account
1964 * For which account the message should be loaded.
1965 * Defaults to the current user.
1966 *
1967 * @ingroup api
efb7face 1968 */
22ae69fd
SG
1969function privatemsg_message_load($pmid, $account = NULL) {
1970 $messages = privatemsg_message_load_multiple(array($pmid), $account);
1971 return current($messages);
1972}
1973
1974/**
1975 * Load multiple messages.
1976 *
1977 * @param $pmids
1978 * Array of Message ids, pm.mid field
1979 * @param $account
1980 * For which account the message should be loaded.
1981 * Defaults to the current user.
1982 *
1983 * @ingroup api
1984 */
1985function privatemsg_message_load_multiple($pmids, $account = NULL) {
1986 // Avoid SQL error that would happen with an empty pm.mid IN () clause.
1987 if (empty($pmids)) {
1988 return array();
1989 }
efb7face 1990
22ae69fd 1991 $query = _privatemsg_assemble_query('load', $pmids, $account);
efb7face 1992 $result = db_query($query['query']);
22ae69fd
SG
1993 $messages = array();
1994 while ($message = db_fetch_array($result)) {
bf1e88fc 1995 // Load author of message.
d2ea4477
SG
1996 if (!($message['author'] = user_load($message['author']))) {
1997 // If user does not exist, load anonymous user.
1998 $message['author'] = user_load(array('uid' => 0));
1999 }
bf1e88fc
OT
2000 $returned = module_invoke_all('privatemsg_message_load', $message);
2001 if (!empty($returned)) {
2002 $message = array_merge_recursive($returned, $message);
2003 }
22ae69fd 2004 $messages[$message['mid']] = $message;
efb7face 2005 }
22ae69fd 2006 return $messages;
efb7face
OT
2007}
2008
5605b147
OT
2009/**
2010 * Generates a query based on a query id.
2011 *
2012 * @param $query
2013 * Either be a string ('some_id') or an array('group_name', 'query_id'),
2014 * if a string is supplied, group_name defaults to 'privatemsg'.
2015 *
2016 * @return
2017 * Array with the keys query and count. count can be used to count the
2018 * elements which would be returned by query. count can be used together
2019 * with pager_query().
2020 *
2021 * @ingroup sql
2022 */
efb7face
OT
2023function _privatemsg_assemble_query($query) {
2024
2025 // Modules will be allowed to choose the prefix for the querybuilder, but if there is not one supplied, 'privatemsg' will be taken by default.
2026 if (is_array($query)) {
2027 $query_id = $query[0];
2028 $query_group = $query[1];
2029 }
2030 else {
2031 $query_id = $query;
2032 $query_group = 'privatemsg';
2033 }
2034
2035 $SELECT = array();
2036 $INNER_JOIN = array();
2037 $WHERE = array();
2038 $GROUP_BY = array();
c277e471 2039 $HAVING = array();
efb7face 2040 $ORDER_BY = array();
668e9c20 2041 $QUERY_ARGS = array('select' => array(), 'where' => array(), 'join' => array(), 'having' => array());
efb7face
OT
2042 $primary_table = '';
2043
2044 $fragments = array(
2045 'select' => $SELECT,
2046 'inner_join' => $INNER_JOIN,
2047 'where' => $WHERE,
2048 'group_by' => $GROUP_BY,
c277e471 2049 'having' => $HAVING,
efb7face
OT
2050 'order_by' => $ORDER_BY,
2051 'query_args' => $QUERY_ARGS,
2052 'primary_table' => $primary_table,
2053 );
2054
2055 /**
2056 * Begin: dynamic arguments
2057 */
2058 $args = func_get_args();
2059 unset($args[0]);
5605b147
OT
2060 // we do the merge because we call call_user_func_array and not drupal_alter
2061 // this is necessary because otherwise we would not be able to use $args correctly (otherwise it doesnt unfold)
efb7face
OT
2062 $alterargs = array(&$fragments);
2063 $query_function = $query_group .'_sql_'. $query_id;
2064 if (!empty($args)) {
2065 $alterargs = array_merge($alterargs, $args);
2066 }
2067 /**
2068 * END: Dynamic arguments
2069 */
2070 if (!function_exists($query_function)) {
2071 drupal_set_message(t('Query function %function does not exist', array('%function' => $query_function)), 'error');
2072 return FALSE;
2073 }
2074 call_user_func_array($query_function, $alterargs);
2075
2076 array_unshift($alterargs, $query_function);
2077 call_user_func_array('drupal_alter', $alterargs);
2078
2079 $SELECT = $fragments['select'];
2080 $INNER_JOIN = $fragments['inner_join'];
2081 $WHERE = $fragments['where'];
2082 $GROUP_BY = $fragments['group_by'];
c277e471 2083 $HAVING = $fragments['having'];
efb7face
OT
2084 $ORDER_BY = $fragments['order_by'];
2085 $QUERY_ARGS = $fragments['query_args'];
2086 $primary_table = $fragments['primary_table'];
2087
d3e3d31e
OT
2088 // pgsql has a case sensitive LIKE - replace it with ILIKE. see http://drupal.org/node/462982
2089 if ($GLOBALS['db_type'] == 'pgsql') {
2090 $WHERE = str_replace('LIKE', 'ILIKE', $WHERE);
2091 }
2092
efb7face
OT
2093 if (empty($primary_table)) {
2094 $primary_table = '{privatemsg} pm';
2095 }
2096
2097 // Perform the whole query assembly only if we have something to select.
2098 if (!empty($SELECT)) {
2099 $str_select = implode(", ", $SELECT);
2100 $query = "SELECT {$str_select} FROM ". $primary_table;
2101
2102 // Also build a count query which can be passed to pager_query to get a "page count" as that does not play well with queries including "GROUP BY".
2103 // In most cases, "COUNT(*)" is enough to get the count query, but in queries involving a GROUP BY, we want a count of the number of groups we have, not the count of elements inside each group.
2104 // So we test if there is GROUP BY and if there is, count the number of distinct groups. If not, we go the normal wal and do a plain COUNT(*).
2105 if (!empty($GROUP_BY)) {
2106 // PostgreSQL does not support COUNT(sometextfield, someintfield), so I'm only using the first one
2107 // Works fine for thread_id/list but may generate an error when a more complex GROUP BY is used.
2108 $str_group_by_count = current($GROUP_BY);
2109 $count = "SELECT COUNT(DISTINCT {$str_group_by_count}) FROM ". $primary_table;
2110 }
2111 else {
2112 $count = "SELECT COUNT(*) FROM ". $primary_table;
2113 }
2114
2115 if (!empty($INNER_JOIN)) {
2116 $str_inner_join = implode(' ', $INNER_JOIN);
2117 $query .= " {$str_inner_join}";
2118 $count .= " {$str_inner_join}";
2119 }
2120 if (!empty($WHERE)) {
2121 $str_where = '('. implode(') AND (', $WHERE) .')';
2122 $query .= " WHERE {$str_where}";
2123 $count .= " WHERE {$str_where}";
2124 }
2125 if (!empty($GROUP_BY)) {
2126 $str_group_by = ' GROUP BY '. implode(", ", $GROUP_BY) ;
2127 $query .= " {$str_group_by}";
2128 }
c277e471
OT
2129 if (!empty($HAVING)) {
2130 $str_having = '('. implode(') AND (', $HAVING) .')';
2131 $query .= " HAVING {$str_having}";
2132 // queries containing a HAVING break the count query on pgsql.
2133 // In this case, use the subquery method as outlined in http://drupal.org/node/303087#comment-1370752 .
2134 // The subquery method will work for all COUNT queries, but it is thought to be much slower, so we are only using it where other cross database approaches fail.
2135 $count = 'SELECT COUNT(*) FROM ('. $query .') as count';
2136 }
efb7face
OT
2137 if (!empty($ORDER_BY)) {
2138 $str_order_by = ' ORDER BY '. implode(", ", $ORDER_BY) ;
2139 $query .= " {$str_order_by}";
2140 }
668e9c20 2141 $QUERY_ARGS = array_merge($QUERY_ARGS['select'], $QUERY_ARGS['join'], $QUERY_ARGS['where'], $QUERY_ARGS['having']);
efb7face
OT
2142 if (!empty($QUERY_ARGS)) {
2143 _db_query_callback($QUERY_ARGS, TRUE);
2144 $query = preg_replace_callback(DB_QUERY_REGEXP, '_db_query_callback', $query);
2145 _db_query_callback($QUERY_ARGS, TRUE);
2146 $count = preg_replace_callback(DB_QUERY_REGEXP, '_db_query_callback', $count);
2147 }
2148 return array('query' => $query, 'count' => $count);
2149 }
2150 return FALSE;
2151}
4ba95ee6 2152
a926640f
OT
2153/**
2154 * Returns a form which handles and displays thread actions.
2155 *
2156 * Additional actions can be added with the privatemsg_thread_operations hook.
2157 * It is also possible to extend this form with additional buttons or other
2158 * elements, in that case, the definitions in the above hook need no label tag,
2159 * instead, the submit button key needs to match with the key of the operation.
2160 *
2161 * @see hook_privatemsg_thread_operations()
2162 *
2163 * @return
2164 * The FAPI definitions for the thread action form.
2165 */
4ba95ee6
OT
2166function _privatemsg_action_form() {
2167 $form = array(
2168 '#type' => 'fieldset',
2169 '#title' => t('Actions'),
2170 '#prefix' => '<div class="container-inline">',
2171 '#suffix' => '</div>',
2172 '#collapsible' => TRUE,
2173 '#collapsed' => FALSE,
2174 '#weight' => 15,
2175 );
668e9c20
OT
2176 if (privatemsg_user_access('delete privatemsg')) {
2177 $form['delete'] = array(
2178 '#type' => 'submit',
2179 '#value' => t('Delete'),
2180 );
2181 }
4ba95ee6 2182 // Display all operations which have a label.
68940e1a 2183 $options = array(0 => t('More actions...'));
4ba95ee6
OT
2184 foreach (module_invoke_all('privatemsg_thread_operations') as $operation => $array) {
2185 if (isset($array['label'])) {
2186 $options[$operation] = $array['label'];
2187 }
2188 }
2189 $form['operation'] = array(
2190 '#type' => 'select',
2191 '#options' => $options,
2192 '#default_value' => 0,
4ba95ee6
OT
2193 );
2194 $form['submit'] = array(
4cebacd8
SG
2195 '#prefix' => '<div class="privatemsg-op-button">',
2196 '#suffix' => '</div>',
4ba95ee6
OT
2197 '#type' => 'submit',
2198 '#value' => t('Execute'),
2199 '#submit' => array('privatemsg_list_submit'),
2200 '#attributes' => array('class' => 'privatemsg-action-button'),
2201 );
4cebacd8 2202 // JS for hiding the execute button.
4ba95ee6
OT
2203 drupal_add_js(drupal_get_path('module', 'privatemsg') .'/privatemsg-list.js');
2204 return $form;
2205}
2206
2207/**
7608a25a 2208 * Marks one or multiple threads as (un)read.
4ba95ee6
OT
2209 *
2210 * @param $threads
7608a25a 2211 * Array with thread id's or a single thread id.
4ba95ee6 2212 * @param $status
7608a25a 2213 * Either PRIVATEMSG_READ or PRIVATEMSG_UNREAD, sets the new status.
4ba95ee6 2214 * @param $account
8086d49e
SG
2215 * User object for which the threads should be deleted, defaults to the
2216 * current user.
4ba95ee6
OT
2217 */
2218function privatemsg_thread_change_status($threads, $status, $account = NULL) {
2219 if (!is_array($threads)) {
2220 $threads = array($threads);
2221 }
2222 if (empty($account)) {
2223 global $user;
2224 $account = drupal_clone($user);
2225 }
2226 // Merge status and uid with the threads list. array_merge() will not overwrite/ignore thread_id 1.
2227 $params = array_merge(array($status, $account->uid), $threads);
2228 db_query('UPDATE {pm_index} SET is_new = %d WHERE uid = %d AND thread_id IN ('. db_placeholders($threads) .')', $params);
2229
2230 if ($status == PRIVATEMSG_UNREAD) {
1c58d7b1 2231 drupal_set_message(format_plural(count($threads), 'Marked 1 thread as unread.', 'Marked @count threads as unread.'));
4ba95ee6
OT
2232 }
2233 else {
1c58d7b1 2234 drupal_set_message(format_plural(count($threads), 'Marked 1 thread as read.', 'Marked @count threads as read.'));
4ba95ee6
OT
2235 }
2236}
2237/**
2238 * Returns a table header definition based on the submitted keys.
2239 *
7bb3f078
OT
2240 * Uses @link theming theme patterns @endlink to theme single headers.
2241 *
4ba95ee6 2242 * @param $has_posts
8086d49e
SG
2243 * TRUE when there is at least one row. Decides if the select all checkbox
2244 * should be displayed.
7608a25a 2245 * @param $keys
4ba95ee6
OT
2246 * Array with the keys which are present in the query/should be displayed.
2247 * @return
2248 * Array with header defintions for tablesort_sql and theme('table').
2249 */
2250function _privatemsg_list_headers($has_posts, $keys) {
2251 $select_header = $has_posts ? theme('table_select_header_cell') : '';
2252 $select_header['#weight'] = -50;
2253
2254 // theme() doesn't include the theme file for patterns, we need to do it manually.
2255 include_once drupal_get_path('module', 'privatemsg') .'/privatemsg.theme.inc';
2256
2257 $header = array($select_header);
2258 foreach ($keys as $key) {
2259 // First, try to load a specific theme for that header, if not present, use the default.
2260 if ($return = theme(array('privatemsg_list_header__'. $key, 'privatemsg_list_header'))) {
2261 // The default theme returns nothing, only store the value if we have something.
2262 $header[$key] = $return;
2263 }
2264 }
609992ce
OT
2265 if (count($header) == 1) {
2266 // No header definition returned, fallback to the default.
2267 $header += _privatemsg_list_headers_fallback($keys);
2268 }
2269 return $header;
2270}
2271
2272/**
2273 * Table header definition for themes that don't support theme patterns.
e1d62afc 2274 *
609992ce
OT
2275 * @return
2276 * Array with the correct headers.
2277 */
2278function _privatemsg_list_headers_fallback($keys) {
2279 $header = array();
2280 foreach ($keys as $key) {
8086d49e 2281 $theme_function = 'phptemplate_privatemsg_list_header__' . $key;
609992ce
OT
2282 if (function_exists($theme_function)) {
2283 $header[$key] = $theme_function();
2284 }
2285 }
e1d62afc 2286
4ba95ee6
OT
2287 return $header;
2288}
2289
2290/**
2291 * Formats a row in the message list.
2292 *
7bb3f078 2293 * Uses @link theming theme patterns @endlink to theme single fields.
b9fca179 2294 *
4ba95ee6
OT
2295 * @param $thread
2296 * Array with the row data returned by the database.
2297 * @return
2298 * Row definition for use with theme('table')
2299 */
2300function _privatemsg_list_thread($thread) {
2301 $row = array('data' => array());
2302
2303 if (!empty($thread['is_new'])) {
2304 // Set the css class in the tr tag.
2305 $row['class'] = 'privatemsg-unread';
2306 }
2307 foreach ($thread as $key => $data) {
2308 // First, try to load a specific theme for that field, if not present, use the default.
2309 if ($return = theme(array('privatemsg_list_field__'. $key, 'privatemsg_list_field'), $thread)) {
2310 // The default theme returns nothing, only store the value if we have something.
2311 $row['data'][$key] = $return;
2312 }
2313 }
609992ce
OT
2314 if (empty($row['data'])) {
2315 $row['data'] = _privatemsg_list_thread_fallback($thread);
2316 }
4ba95ee6
OT
2317 return $row;
2318}
2319
2320/**
609992ce
OT
2321 * Table row definition for themes that don't support theme patterns.
2322 *
2323 * @return
2324 * Array with row data.
2325 */
2326function _privatemsg_list_thread_fallback($thread) {
2327 $row_data = array();
2328 foreach ($thread as $key => $data) {
8086d49e 2329 $theme_function = 'phptemplate_privatemsg_list_field__' . $key;
609992ce
OT
2330 if (function_exists($theme_function)) {
2331 $row_data[$key] = $theme_function($thread);
2332 }
2333 }
e1d62afc 2334
609992ce
OT
2335 return $row_data;
2336}
2337
2338/**
4ba95ee6
OT
2339 * Menu callback for messages/undo/action.
2340 *
8086d49e
SG
2341 * This function will test if an undo callback is stored in SESSION and
2342 * execute it.
4ba95ee6
OT
2343 */
2344function privatemsg_undo_action() {
2345 // Check if a undo callback for that user exists.
2346 if (isset($_SESSION['privatemsg']['undo callback']) && is_array($_SESSION['privatemsg']['undo callback'])) {
2347 $undo = $_SESSION['privatemsg']['undo callback'];
2348 // If the defined undo callback exists, execute it
2349 if (isset($undo['function']) && isset($undo['args'])) {
348e9d62
SG
2350 // Load the user object.
2351 if (isset($undo['args']['account']) && $undo['args']['account'] > 0) {
2352 $undo['args']['account'] = user_load((int)$undo['args']['account']);
2353 }
4ba95ee6
OT
2354 call_user_func_array($undo['function'], $undo['args']);
2355 }
2356 // Return back to the site defined by the destination GET param.
2357 drupal_goto();
2358 }
2359}
2360
2361/**
2362 * Process privatemsg_list form submissions.
2363 *
2364 * Execute the chosen action on the selected messages. This function is
2365 * based on node_admin_nodes_submit().
2366 */
2367function privatemsg_list_submit($form, &$form_state) {
2368 // Load all available operation definitions.
2369 $operations = module_invoke_all('privatemsg_thread_operations');
2370
2371 // Default "default" operation, which won't do anything.
2372 $operation = array('callback' => 0);
2373
2374 // Check if a valid operation has been submitted.
2375 if (isset($form_state['values']['operation']) && isset($operations[$form_state['values']['operation']])) {
2376 $operation = $operations[$form_state['values']['operation']];
2377 }
2378
2379 // Load all keys where the value is the current op.
2380 $keys = array_keys($form_state['values'], $form_state['values']['op']);
2381
2382 // The first one is op itself, we need to use the second.
2383 if (isset($keys[1]) && isset($operations[$keys[1]])) {
2384 $operation = $operations[$keys[1]];
2385 }
2386
6794baa1
SG
2387 // Only execute something if we have a valid callback and at least one checked thread.
2388 if (!empty($operation['callback'])) {
348e9d62 2389 privatemsg_operation_execute($operation, $form_state['values']['threads'], $form_state['values']['account']);
6794baa1
SG
2390 }
2391}
2392
2393/**
2394 * Execute an operation on a number of threads.
2395 *
2396 * @param $operation
2397 * The operation that should be executed.
2398 * @see hook_privatemsg_thread_operations()
2399 * @param $threads
2400 * An array of thread ids. The array is filtered before used, a checkboxes
2401 * array can be directly passed to it.
2402 */
348e9d62 2403function privatemsg_operation_execute($operation, $threads, $account = null) {
ad541119 2404 // Filter out unchecked threads, this gives us an array of "checked" threads.
6794baa1
SG
2405 $threads = array_filter($threads);
2406
2407 if (empty($threads)) {
2408 // Do not execute anything if there are no checked threads.
2409 return;
2410 }
2411 // Add in callback arguments if present.
2412 if (isset($operation['callback arguments'])) {
2413 $args = array_merge(array($threads), $operation['callback arguments']);
2414 }
2415 else {
2416 $args = array($threads);
2417 }
348e9d62
SG
2418
2419 // Add the user object to the arguments.
2420 if ($account) {
2421 $args[] = $account;
2422 }
2423
6794baa1
SG
2424 // Execute the chosen action and pass the defined arguments.
2425 call_user_func_array($operation['callback'], $args);
4ba95ee6 2426
6794baa1
SG
2427 // Check if that operation has defined an undo callback.
2428 if (isset($operation['undo callback']) && $undo_function = $operation['undo callback']) {
4ba95ee6 2429 // Add in callback arguments if present.
6794baa1
SG
2430 if (isset($operation['undo callback arguments'])) {
2431 $undo_args = array_merge(array($threads), $operation['undo callback arguments']);
4ba95ee6
OT
2432 }
2433 else {
6794baa1 2434 $undo_args = array($threads);
4ba95ee6 2435 }
348e9d62
SG
2436
2437 // Avoid saving the complete user object in the session.
2438 if ($account) {
2439 $undo_args['account'] = $account->uid;
2440 }
6794baa1
SG
2441 // Store the undo callback in the session and display a "Undo" link.
2442 // @todo: Provide a more flexible solution for such an undo action, operation defined string for example.
2443 $_SESSION['privatemsg']['undo callback'] = array('function' => $undo_function, 'args' => $undo_args);
2444 $undo = url('messages/undo/action', array('query' => drupal_get_destination()));
8086d49e 2445
6794baa1 2446 drupal_set_message(t('The previous action can be <a href="!undo">undone</a>.', array('!undo' => $undo)));
4ba95ee6
OT
2447 }
2448}
2449
2450/**
22107330 2451 * Delete or restore one or multiple threads.
4ba95ee6
OT
2452 *
2453 * @param $threads
22107330 2454 * Array with thread id's or a single thread id.
4ba95ee6 2455 * @param $delete
8086d49e
SG
2456 * Indicates if the threads should be deleted or restored.
2457 * 1 => delete, 0 => restore.
4ba95ee6 2458 * @param $account
8086d49e
SG
2459 * User object for which the threads should be deleted,
2460 * defaults to the current user.
4ba95ee6
OT
2461 */
2462function privatemsg_thread_change_delete($threads, $delete, $account = NULL) {
2463 if (!is_array($threads)) {
2464 $threads = array($threads);
2465 }
2466 if (empty($account)) {
2467 global $user;
2468 $account = drupal_clone($user);
2469 }
2470
2471 // Merge status and uid with the threads list. array_merge() will not overwrite/ignore thread_id 1.
2472 $params = array_merge(array($delete, $account->uid), $threads);
2473
2474 // Load all messages of those threads including the deleted.
2475 $query = _privatemsg_assemble_query('messages', $threads, $account, TRUE);
2476 $result = db_query($query['query']);
2477
2478 // Delete each message. We need to do that to trigger the delete hook.
2479 while ($row = db_fetch_array($result)) {
2480 privatemsg_message_change_delete($row['mid'], $delete, $account);
2481 }
2482
2483 if ($delete) {
1c58d7b1 2484 drupal_set_message(format_plural(count($threads), 'Deleted 1 thread.', 'Deleted @count threads.'));
4ba95ee6
OT
2485 }
2486 else {
1c58d7b1 2487 drupal_set_message(format_plural(count($threads), 'Restored 1 thread.', 'Restored @count threads.'));
4ba95ee6
OT
2488 }
2489}
2490
2491/**
8086d49e 2492 * Implements hook_privatemsg_thread_operations().
4ba95ee6
OT
2493 */
2494function privatemsg_privatemsg_thread_operations() {
2495 $operations = array(
2496 'mark as read' => array(
2497 'label' => t('Mark as read'),
2498 'callback' => 'privatemsg_thread_change_status',
2499 'callback arguments' => array('status' => PRIVATEMSG_READ),
2500 'undo callback' => 'privatemsg_thread_change_status',
2501 'undo callback arguments' => array('status' => PRIVATEMSG_UNREAD),
2502 ),
2503 'mark as unread' => array(
2504 'label' => t('Mark as unread'),
2505 'callback' => 'privatemsg_thread_change_status',
2506 'callback arguments' => array('status' => PRIVATEMSG_UNREAD),
2507 'undo callback' => 'privatemsg_thread_change_status',
2508 'undo callback arguments' => array('status' => PRIVATEMSG_READ),
2509 ),
668e9c20
OT
2510 );
2511 if (privatemsg_user_access('delete privatemsg')) {
2512 $operations['delete'] = array(
4ba95ee6
OT
2513 'callback' => 'privatemsg_thread_change_delete',
2514 'callback arguments' => array('delete' => 1),
2515 'undo callback' => 'privatemsg_thread_change_delete',
2516 'undo callback arguments' => array('delete' => 0),
668e9c20
OT
2517 );
2518 }
4ba95ee6
OT
2519 return $operations;
2520}
8b2d8ddf
SG
2521
2522/**
2523 * Implementation of hook_views_api().
2524 */
2525function privatemsg_views_api() {
2526 return array(
2527 'api' => 2,
2528 'path' => drupal_get_path('module', 'privatemsg') . '/views',
2529 );
3b333877 2530}