'Content subscriptions', 'type' => MENU_LOCAL_TASK, 'page callback' => 'drupal_get_form', 'page arguments' => array('notifications_content_settings_form'), 'access arguments' => array('administer site configuration'), 'file' => 'notifications_content.pages.inc', ); // User pages, will be disabled by default $items['user/%user/notifications/thread'] = array( 'type' => MENU_LOCAL_TASK, 'access callback' => FALSE, 'access arguments' => FALSE, 'title' => 'Thread', 'page callback' => 'notifications_content_page_thread', 'page arguments' => array(1), 'weight' => 10, 'file' => 'notifications_content.pages.inc', ); $items['user/%user/notifications/nodetype'] = array( 'type' => MENU_LOCAL_TASK, 'access callback' => FALSE, 'title' => 'Content type', 'page callback' => 'notifications_content_page_nodetype', 'page arguments' => array(1), 'weight' => 10, 'file' => 'notifications_content.pages.inc', ); $items['user/%user/notifications/author'] = array( 'type' => MENU_LOCAL_TASK, 'access callback' => FALSE, 'title' => t('Author'), 'page callback' => 'notifications_content_page_author', 'pàge arguments' => array(1), 'weight' => 10, 'file' => 'notifications_content.pages.inc', ); return $items; } /** * Menu access callback */ function notifications_content_access($account, $perm) { global $user; return ($account->uid && $account->uid == $user->uid && user_access($perm)) || (user_access('administer notifications') && user_access($perm, $account)); } /** * Implementation of hook_perm() */ function notifications_content_perm() { return array('subscribe to content', 'subscribe to content type', 'subscribe to author', 'skip notifications'); } /** * Implementation of hook_help() */ function notifications_content_help($path, $arg) { if ($path == 'admin/messaging/notifications/content') { $output .= '

'. t('To determine the available subscription types for each content type you can also use the !content-type-settings', array('!content-type-settings' => l(t('content types settings page'), 'admin/content/types'))) .'

'; return $output; } elseif (array($arg[0], $arg[1], $arg[2], $arg[3]) == array('admin', 'messaging', 'template', 'edit') && ($group = $arg[4])) { switch ($group) { case 'notifications-digest-node-nid': case 'notifications-digest-node-type': $help = '

' . t('This is the format for each digest group. A message may consist on one or many of these groups:') . '

'; $help .= '
';
        $help .= t('Group title') . "\n";
        $help .= '- ' . t('Digest line.'). "\n";
        $help .= '- ' . t('Digest line.'). "\n";
        $help .= '-  ...'. "\n";
        $help .= t('Group footer') . "\n";
        $help .= '
'; return $help; } } } /** * Implementation of hook_form_alter(). */ function notifications_content_form_alter(&$form, &$form_state, $form_id) { switch ($form_id) { case 'comment_form': // Load the node which is possibly cached to get the node type $node = node_load($form['nid']['#value']); if (notifications_content_type_enabled($node->type)) { $form['notifications']['notifications_content_disable'] = array( '#type' => 'checkbox', '#title' => t('Do not send notifications for this comment.'), '#default_value' => 0, '#access' => user_access('skip notifications'), ); } break; case 'node_type_form': if (isset($form['identity']['type'])) { // Hack for modules with different weights to add options here if (!isset($form['notifications'])) $form['notifications'] = array(); $form['notifications'] += array( '#type' => 'fieldset', '#title' => t('Subscription settings'), '#collapsible' => TRUE, '#collapsed' => TRUE, ); $form['notifications']['notifications_content_type'] = array( '#type' => 'checkboxes', '#title' => t('Allowed subscription types'), '#default_value' => notifications_content_type_enabled($form['#node_type']->type), '#options' => _notifications_content_type_options(), '#description' => t('Enable different subscription options for this content type.'), '#weight' => -10, ); if (!variable_get('notifications_content_per_type', 0)) { $form['notifications']['notifications_content_type']['#disabled'] = TRUE; $form['notifications']['notifications_content_type']['#description'] .= ' ' . t('To enable these options check the Notifications content settings', array('@notifications-settings' => url('admin/messaging/notifications/content'))) . ''; } } break; default: // Node form. Option to disable notifications if (isset($form['type']) && isset($form['#node']) && $form['type']['#value'] .'_node_form' == $form_id) { $types = notifications_content_types(NULL); if (notifications_content_type_enabled($node->type)) { $noprevious = !isset($form['notifications']); $form['notifications']['#type'] = 'fieldset'; $form['notifications']['#title'] = t('Notifications'); $form['notifications']['#collapsible'] = TRUE; $form['notifications']['#weight'] = 1; //don't want to hide/change access on modules that already used the notifications fieldset for something if ($noprevious) { $form['notifications']['#collapsed'] = TRUE; $form['notifications']['#access'] = user_access('skip notifications'); } $form['notifications']['notifications_content_disable'] = array( '#type' => 'checkbox', '#title' => t('Do not send notifications for this update.'), '#default_value' => 0, '#access' => user_access('skip notifications'), ); } } } } /** * Implementation of hook_theme() */ function notifications_content_theme() { return array( 'notifications_content_type_settings' => array( 'arguments' => array('element' => NULL), 'file' => 'notifications_content.pages.inc', ), ); } /** * Implementation of hook_notifications() */ function notifications_content_notifications($op, &$arg0, $arg1 = NULL, $arg2 = NULL) { switch ($op) { case 'names': $subs = &$arg0; if ($subs->event_type == 'node') { $subs->type_name = t('Content'); if (!empty($subs->fields['type'])) { $subs->names['type'] = t('Content type: @type', array('@type' => node_get_types('name', $subs->fields['type']))); } if (!empty($subs->fields['author']) && ($author = user_load(array('uid' => $subs->fields['author'])))) { $subs->names['author'] = t('Author: @name', array('@name' => $author->name)); } if (!empty($subs->fields['nid']) && ($node = node_load($subs->fields['nid']))) { $subs->names['thread'] = t('Thread: @title', array('@title' => $node->title)); } } break; case 'subscription types': $types['thread'] = array( 'event_type' => 'node', 'title' => t('Thread'), 'access' => 'subscribe to content', 'page callback' => 'notifications_content_page_thread', 'user page' => 'user/%user/notifications/thread', 'fields' => array('nid'), 'description' => t('Subscribe to all changes and comments for a thread.') ); $types['nodetype'] = array( 'event_type' => 'node', 'title' => t('Content type'), 'access' => 'subscribe to content type', 'page callback' => 'notifications_content_page_nodetype', 'user page' => 'user/%user/notifications/nodetype', 'fields' => array('type'), 'description' => t('Subscribe to all content of a given type.') ); $types['author'] = array( 'event_type' => 'node', 'title' => t('Author'), 'access' => 'subscribe to author', 'page callback' => 'notifications_content_page_author', 'user page' => 'user/%user/notifications/author', 'fields' => array('author'), 'description' => t('Subscribe to all content submitted by a user.') ); // This is a complex type, combining two fields $types['typeauthor'] = array( 'event_type' => 'node', 'title' => t('Content type and Author'), 'access' => 'subscribe to content type and author', //'page callback' => 'notifications_content_page_author', 'fields' => array('author', 'type'), 'description' => t('Subscribe to all content of a given type submitted by a user.') ); return $types; case 'subscription fields': // Information about available fields for subscriptions // - format callback => will be used to convert the value into a displayable output // - value callback => will be used to convert autocomplete name into field value // - autocomplete path => path for autocomplete field // - options callback / arguments => used to produce a drop down field $fields['nid'] = array( 'name' => t('Node'), 'field' => 'nid', 'type' => 'int', 'autocomplete path' => 'notifications/autocomplete/node/title', 'autocomplete callback' => 'notifications_node_nid2autocomplete', 'format callback' => 'notifications_node_nid2title', 'value callback' => 'notifications_node_title2nid', ); $fields['author'] = array( 'name' => t('Author'), 'field' => 'author', 'type' => 'int', 'autocomplete path' => 'user/autocomplete', 'autocomplete callback' => 'notifications_content_author_name', 'format callback' => 'notifications_content_author_name', 'value callback' => 'notifications_content_author_uid', ); $fields['type'] = array( 'name' => t('Node type'), 'field' => 'type', 'type' => 'string', 'options callback' => 'notifications_content_types', ); return $fields; case 'query': // $arg2 is $event array. if ($arg0 == 'event' && $arg1 == 'node' && ($node = $arg2->node) || $arg0 == 'user' && $arg1 == 'node' && ($node = $arg2)) { $query[]['fields'] = array( 'nid' => $node->nid, 'type' => $node->type, 'author' => $node->uid, ); return $query; } break; case 'node options': // Subscription options for a node, args will be account and node return _notifications_content_node_options($arg0, $arg1); case 'user options': // Subscription options for a user account, args will be account and author return _notifications_content_user_options($arg0, $arg1); case 'event load': // $arg0 is event $event = &$arg0; $load = array(); if ($event->type == 'node') { if (!empty($event->params['nid'])) { $event->objects['node'] = node_load($event->params['nid']); } if (!empty($event->params['cid'])) { $event->objects['comment'] = notifications_content_comment_load($event->params['cid']); } } break; case 'event objects': return array('node' => t('Node')); case 'event types': // Node inserts are not grouped by node but all together. The digest will look like: // New content has been submitted // - Story Title1 by Author1 // - Event Title2 by Author2 $types[] = array( 'type' => 'node', 'action' => 'insert', 'name' => t('New content of type [type-name] has been submitted'), 'line' => t('[type-name] [title] by [author-name]'), 'digest' => array('node', 'type'), 'description' => t('Node creation'), ); // These other events are grouped for each node. The digest will look like: // Story: Title of the story // - The story has been updated // - New comment by User: Comment title $types[] = array( 'type' => 'node', 'action' => 'update', 'name' => t('[type-name]: [title]'), 'line' => t('The [type-name] has been updated'), 'digest' => array('node', 'nid'), 'description' => t('Node update'), ); $types[] = array( 'type' => 'node', 'action' => 'comment', 'name' => t('[type-name]: [title]'), 'line' => t('New comment by [comment-author-name]: [comment-title]'), 'digest' => array('node', 'nid'), 'description' => t('Node comment'), ); return $types; case 'access': // Return an array with some TRUE value if the user has access to this event objects or subscription type $type = $arg0; $account = &$arg1; $object = &$arg2; $access = TRUE; // For events we check that node and comment are allowed if ($type == 'event' && $object->type == 'node') { if (!empty($object->objects['node'])) { $access = notifications_content_node_allow($account, $object->objects['node']); } // If no access to node, we don't check more if ($access && !empty($object->objects['comment'])) { $access = $access && notifications_content_comment_allow($account, $object->objects['comment']); } // For node subscriptions we check that user can view the node } elseif ($type == 'subscription') { $access = TRUE; if (!empty($object->fields['nid'])) { if ($node = node_load($object->fields['nid'])) { $access = notifications_content_node_allow($account, $node); } else { $access = FALSE; } } if (!empty($object->fields['type'])) { $access = $access && array_key_exists($object->fields['type'], notifications_content_types()); } } // We return an array that will be merged with the ones from other modules return array($access); break; } } /** * Field name callback, author uid to user name */ function notifications_content_author_name($uid, $html = FALSE) { if ($account = user_load($uid)) { return $html ? theme('username', $account) : check_plain($account->name); } } function notifications_content_author_uid($name, $field = NULL) { if ($account = user_load(array('name' => $name))) { return $account->uid; } elseif ($field) { form_set_error($field, t('User name not found.')); } } /** * Implementation of hook_messaging() */ function notifications_content_messaging($op, $arg1 = NULL, $arg2 = NULL, $arg3 = NULL, $arg4 = NULL) { switch ($op) { case 'message groups': $help = t('The Header and Footer will be taken from Notification events.'); $help_digest = $help . ' ' . t('The Digest line will be used when composing Short digests on which each event will be just a line.'); // Generic notifications event $info['notifications-event-node'] = array( 'module' => 'notifications_content', 'name' => t('Notifications for node events'), 'description' => t('Defaults for all notifications related to node events.'), 'help' => $help_digest, 'fallback' => 'notifications-event', ); $info['notifications-event-node-insert'] = array( 'module' => 'notifications_content', 'name' => t('Notifications for node creation'), 'description' => t('Notifications produced when a new node is created.'), 'help' => $help_digest, 'fallback' => 'notifications-event-node', ); $info['notifications-event-node-update'] = array( 'module' => 'notifications_content', 'name' => t('Notifications for node updates'), 'description' => t('Notifications produced when a node is updated.'), 'help' => $help_digest, 'fallback' => 'notifications-event-node', ); $info['notifications-event-node-comment'] = array( 'module' => 'notifications_content', 'name' => t('Notifications for node comments'), 'description' => t('Notifications produced when a comment is posted to a node.'), 'help' => $help_digest, 'fallback' => 'notifications-event-node', ); // Node group digests, will have specific help text in hook_help() $info['notifications-digest-node-nid'] = array( 'module' => 'notifications-content', 'name' => t('Groups digests per node'), 'description' => t('Group of events digested for each node.'), 'fallback' => 'notifications-digest', ); $info['notifications-digest-node-type'] = array( 'module' => 'notifications-content', 'name' => t('Groups digests per node type'), 'description' => t('Group of events digested for each node type.'), 'fallback' => 'notifications-digest', ); return $info; case 'message keys': $type = $arg1; switch ($type) { case 'notifications-event-node': case 'notifications-event-node-insert': case 'notifications-event-node-update': case 'notifications-event-node-comment': // Some parts will be re-used from 'notifications-event' group // So we specify only subject and main message return array( 'subject' => t('Subject'), 'main' => t('Content'), 'digest' => t('Digest line'), ); case 'notifications-digest-node-nid': case 'notifications-digest-node-type': $parts['title'] = t('Group title'); $parts['footer'] = t('Group footer'); return $parts; } break; case 'messages': $type = $arg1; // Event notifications switch ($type) { case 'notifications-event-node': case 'notifications-event-node-update': return array( 'subject' => t('Update for [type-name]: [title]'), 'main' => array( '[node-teaser]', t('Read more [node-url]'), ), 'digest' => array( '[title]', 'Read more [node-url]', ), ); case 'notifications-event-node-insert': return array( 'subject' => t('New [type-name]: [title]'), 'main' => array( '[node-teaser]', t('Read more [node-url]'), ), 'digest' => array( '[title]', 'Read more [node-url]', ), ); case 'notifications-event-node-comment': return array( 'subject' => t('Comment for [type-name]: [title]'), 'main' => array( t('Comment by [comment-author-name]: [comment-title]'), '[comment-body]', t('Read more [comment-url]'), ), 'digest' => array( t('New Comment on [title] by [comment-author-name] titled [comment-title]'), t('Read more [comment-url]'), ), ); case 'notifications-digest-node-nid': return array( 'title' => t('Updates for [type-name]: [title]'), 'footer' => t('Read more [node-url]'), ); case 'notifications-digest-node-type': return array( 'title' => t('New content of type [type-name] has been submitted'), 'footer' => '', ); } break; case 'tokens': $type = explode('-', $arg1); $tokens = array(); // These are the token groups that will be used for this module's messages if ($type[0] == 'notifications' && $type[2] == 'node') { if ($type[1] == 'event') { $tokens[] = 'node'; if ($type[3] == 'comment') { $tokens[] = 'comment'; } } elseif ($type[1] == 'digest') { if ($type[3] == 'nid') { $tokens[] = 'node'; } elseif ($type[3] == 'type') { // Special format for isolated tokens: array('token type', 'token id'). // In this case, as messages are digested by node type the only common element will be node-type $tokens[] = array('node', 'type-name'); } } } return $tokens; } } /** * Subscribe / unsubscribe options to specific node. */ function _notifications_content_node_options($account, $node) { $options = array(); // Thread if (notifications_content_type_enabled($node->type, 'thread')) { $options[] = array( 'name' => t('This post'), 'type' => 'thread', 'fields' => array('nid' => $node->nid), ); } // Content type if (notifications_content_type_enabled($node->type, 'nodetype')) { $options[] = array( 'name' => t('Posts of type @type', array('@type' => node_get_types('name', $node->type))), 'type' => 'nodetype', 'fields' => array('type' => $node->type), ); } // Author if (notifications_content_type_enabled($node->type, 'author')) { $options[] = array( 'name' => t('Posts by @name', array('@name' => $node->name)), 'type' => 'author', 'fields' => array('author' => $node->uid), ); } return $options; } /** * Subscribe / unsubscribe options for specific user * * @param $account * User who is subscribing / unsubscribing to * @param $author * User we are subscribing too */ function _notifications_content_user_options($account, $author) { $options = array(); // All posts by author $options[] = array( 'name' => t('All posts by @name', array('@name' => $author->name)), 'type' => 'author', 'fields' => array('author' => $author->uid), ); // Content types with author subscriptions foreach (notifications_content_types('name', 'typeauthor') as $type => $name) { $options[] = array( 'name' => t('@type posts by @name', array('@name' => $author->name, '@type' => $name)), 'type' => 'typeauthor', 'fields' => array('author' => $author->uid, 'type' => $type), ); } return $options; } /** * List subscription options for content types */ function _notifications_content_type_options() { return _notifications_subscription_types('long', array('event_type' => 'node')); } /** * Implementation of hook_nodeapi() */ function notifications_content_nodeapi(&$node, $op, $arg = 0) { global $user; switch ($op) { case 'load': // Store current status for later reference $node->old_status = $node->status; break; case 'update': case 'insert': // Notifications just for published nodes. If we don't have any option enabled for this content type, skip the event if ($node->status && empty($node->notifications_content_disable) && notifications_content_type_enabled($node->type)) { $event = array( 'module' => 'node', 'uid' => $node->uid, 'oid' => $node->nid, 'type' => 'node', 'action' => $op, 'node' => $node, 'params' => array('nid' => $node->nid), ); if ($op == 'update') { // If the node has been published the 'update' will become a 'insert' (first post) if (!isset($node->old_status)) { // We try to find out previous status with the cached node. $oldnode = node_load($node->nid); $node->old_status = $oldnode->status; } if (!$node->old_status) { $event['action'] = 'insert'; } // If immediate sending is active, need to reset the node cache so we don't send old versions of the node if (variable_get('notifications_send_immediate', 0)) { node_load(0, NULL, TRUE); } } notifications_event($event); } break; case 'delete': // Remove all subscriptions for this node notifications_delete_subscriptions(array('event_type' => 'node'), array('nid' => $node->nid), FALSE); } } /** * Implementation of hook_comment(). */ function notifications_content_comment($comment, $op) { // $comment can be an object or an array. $comment = (object)$comment; if (($op == 'update' || $op == 'publish') && empty($comment->notifications_content_disable) && empty($comment->status)) { $node = node_load($comment->nid); if (notifications_content_type_enabled($node->type)) { $event = array( 'module' => 'node', 'uid' => $comment->uid, 'type' => 'node', 'action' => 'comment', 'node' => $node, 'comment' => $comment, 'params' => array('nid' => $comment->nid, 'cid' => $comment->cid), ); } notifications_event($event); } } /** * Implementation of hook node_type */ function notifications_content_node_type($op, $info) { if ($op == 'delete') { // Remove all subscriptions for this node type notifications_delete_subscriptions(array('event_type' => 'node'), array('type' => $info->type)); } } /** * Load comments with caching * @ TODO See if this may be some use, or drop */ function notifications_content_comment_load($cid) { static $cache; if (!isset($cache[$cid])) { $comment = db_fetch_object(db_query('SELECT c.*, u.uid, u.name AS registered_name, u.data FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.cid = %d', $cid)); $comment = drupal_unpack($comment); $comment->name = $comment->uid ? $comment->registered_name : $comment->name; $cache[$cid] = $comment; } return $cache[$cid]; } /** * Get content types available for subscriptions to content type * * @param $field * Optional field to return as array value. If none it will return the full objects. */ function notifications_content_types($field = 'name', $subs_type = 'nodetype') { // Get list of available node types, all of them will be allowed by default $types = array(); foreach (node_get_types() as $key => $data) { if (notifications_content_type_enabled($key, $subs_type)) { $types[$key] = $data; } } if ($field) { foreach (array_keys($types) as $type) { $types[$type] = $types[$type]->$field; } } return $types; } /** * Get subscription options for this content type * - All enabled options if ($option = NULL) * - TRUE / FALSE for a given $option */ function notifications_content_type_enabled($type = NULL, $option = NULL) { $defaults = variable_get('notifications_content_type', array()); if ($type && variable_get('notifications_content_per_type', 0)) { $settings = variable_get('notifications_content_type_' . $type, $defaults); } else { $settings = $defaults; } if ($option) { return in_array($option, $settings, TRUE); } else { return $settings; } } /** * Implementation of hook_token_list(). Documents the individual * tokens handled by the module. */ function notifications_content_token_list($type = 'all') { $tokens = array(); if ($type == 'node' || $type == 'all') { $tokens['node']['node-teaser'] = t('The node teaser.'); $tokens['node']['node-body'] = t('The node body.'); $tokens['node']['node-url'] = t('The node view url for read more links.'); $tokens['node']['node-teaser-raw'] = t('Unfiltered node teaser. WARNING - raw user input.'); $tokens['node']['node-body-raw'] = t('Unfiltered node body. WARNING - raw user input.'); } if ($type == 'comment' || $type == 'all') { $tokens['comment']['comment-url'] = t('The comment view url.'); $tokens['comment']['comment-reply-url'] = t('The comment reply url.'); } return $tokens; } /** * Implementation of hook_token_values() */ function notifications_content_token_values($type, $object = NULL, $options = array()) { switch ($type) { case 'node': if ($node = $object) { $values['node-teaser'] = $node->teaser ? check_markup($node->teaser, $node->format, FALSE) : ''; $values['node-body'] = $node->body ? check_markup($node->body, $node->format, FALSE) : ''; $values['node-url'] = url('node/'. $node->nid, array('absolute' => TRUE)); $values['node-teaser-raw'] = $node->teaser ? $node->teaser : ''; $values['node-body-raw'] = $node->body ? $node->body : ''; return $values; } break; case 'comment': if ($comment = (object)$object) { $values['comment-url'] = url('node/'. $comment->nid, array('fragment' =>'comment-'. $comment->cid, 'absolute' => TRUE)); $values['comment-reply-url'] = url('comment/reply/'. $comment->nid .'/'. $comment->cid, array('absolute' => TRUE)); return $values; } break; } } /** * Determine whether the specified user may view the specified node. * * Does a user switching and checks for node permissions. Looking for a better way * but it seems that all the node_access hooks cant be invokes without this. */ function notifications_content_node_allow($account, $node) { static $access; if (!$node) return FALSE; if (!isset($access[$account->uid][$node->nid])) { $access[$account->uid][$node->nid] = node_access('view', $node, $account); } return $access[$account->uid][$node->nid]; } /** * Determine whether the specified user may view the specified comment. * * Does a user switching and checks for node permissions. Looking for a better way * but it seems that all the node_access hooks cant be invokes without this. */ function notifications_content_comment_allow($account, $comment) { static $access; $comment = is_object($comment) ? $comment : db_fetch_object(db_query("SELECT * FROM {comments} WHERE cid = %d", $comment)); if (!isset($access[$account->uid][$comment->cid])) { if (($account->uid == $comment->uid || $comment->status == COMMENT_PUBLISHED) && user_access('access comments', $account) || user_access('administer comments', $account)) { $access[$account->uid][$comment->cid] = TRUE; } else { $access[$account->uid][$comment->cid] = FALSE; } } return $access[$account->uid][$comment->cid]; }