privatemsg.module

  1. 1 privatemsg.module
  2. 7-1 privatemsg.module
  3. 7-2 privatemsg.module
  4. 6-2 privatemsg.module

Allows users to send private messages to other users.

File

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

Functions

Namesort descending Description
pm_preview
pm_send Submit callback for the privatemsg_new form.
pm_send_validate
privatemsg_block
privatemsg_cron Implements hook_cron().
privatemsg_delete
privatemsg_delete_submit
privatemsg_get_link Returns a link to send message form for a specific users.
privatemsg_list List messages.
privatemsg_list_submit Process privatemsg_list form submissions.
privatemsg_menu Implements hook_menu().
privatemsg_message_change_delete Delete or restore a message.
privatemsg_message_change_status Changes the read/new status of a single message.
privatemsg_message_load Load a single message.
privatemsg_message_load_multiple Load multiple messages.
privatemsg_new
privatemsg_new_thread Send a new message.
privatemsg_operation_execute Execute an operation on a number of threads.
privatemsg_perm Implements hook_perm().
privatemsg_privatemsg_thread_operations Implements hook_privatemsg_thread_operations().
privatemsg_privatemsg_view_template Implements hook_privatemsg_view_template().
privatemsg_reply Send a reply message
privatemsg_sql_autocomplete Query definition to search for username autocomplete suggestions.
privatemsg_sql_deleted Query Builder function to load all messages that should be flushed.
privatemsg_sql_list
privatemsg_sql_load Query function for loading a single or multiple messages.
privatemsg_sql_messages Query definition to load messages of one or multiple threads.
privatemsg_sql_participants Load all participants of a thread.
privatemsg_sql_unread_count Query definition to count unread messages.
privatemsg_theme
privatemsg_thread_change_delete Delete or restore one or multiple threads.
privatemsg_thread_change_status Marks one or multiple threads as (un)read.
privatemsg_thread_load Load a thread with all the messages and participants.
privatemsg_title_callback
privatemsg_undo_action Menu callback for messages/undo/action.
privatemsg_unread_count Return number of unread messages for an account.
privatemsg_user
privatemsg_user_access Privatemsg wrapper for user_access.
privatemsg_user_name_autocomplete Return autocomplete results for usernames.
privatemsg_view Menu callback for viewing a thread.
privatemsg_views_api Implementation of hook_views_api().
privatemsg_view_access Check access to the view messages page.
private_message_settings
private_message_settings_submit
private_message_view_options
template_preprocess_privatemsg_recipients
template_preprocess_privatemsg_view
_privatemsg_action_form Returns a form which handles and displays thread actions.
_privatemsg_assemble_query Generates a query based on a query id.
_privatemsg_block_menu
_privatemsg_block_new
_privatemsg_format_participants Format an array of user objects.
_privatemsg_generate_user_array Generate aray of user objects based on a string.
_privatemsg_list_headers Returns a table header definition based on the submitted keys.
_privatemsg_list_headers_fallback Table header definition for themes that don't support theme patterns.
_privatemsg_list_thread Formats a row in the message list.
_privatemsg_list_thread_fallback Table row definition for themes that don't support theme patterns.
_privatemsg_load_thread_participants Load all participants of a thread, optionally without author.
_privatemsg_parse_userstring Extract the valid usernames of a string and loads them.
_privatemsg_send Internal function to save a message.
_privatemsg_validate_message

Constants

Namesort descending Description
PRIVATEMSG_READ Status constant for read messages.
PRIVATEMSG_UNLIMITED Show unlimited messages in a thread.
PRIVATEMSG_UNREAD Status constant for unread messages.