Code coverage for /20080809/modules/trigger/trigger.module

Line #Times calledCode
1
<?php
2
// $Id: trigger.module,v 1.17 2008/05/06 12:18:53 dries Exp $
3
4
/**
5
 * @file
6
 * Enables functions to be stored and executed at a later time when
7
 * triggered by other modules or by one of Drupal's core API hooks.
8
 */
9
10
/**
11
 * Implementation of hook_help().
12
 */
13185
function trigger_help($path, $arg) {
14131
  $explanation = '<p>' . t('Triggers are system events, such as when new
content is added or when a user logs in. Trigger module combines these
triggers with actions (functional tasks), such as unpublishing content or
e-mailing an administrator. The <a href="@url">Actions settings page</a>
contains a list of existing actions and provides the ability to create and
configure additional actions.', array('@url' =>
url('admin/settings/actions'))) . '</p>';
15
  switch ($path) {
16131
    case 'admin/build/trigger/comment':
170
      return $explanation . '<p>' . t('Below you can assign actions to run
when certain comment-related triggers happen. For example, you could
promote a post to the front page when a comment is added.') . '</p>';
18131
    case 'admin/build/trigger/node':
1942
      return $explanation . '<p>' . t('Below you can assign actions to run
when certain content-related triggers happen. For example, you could send
an e-mail to an administrator when a post is created or updated.') .
'</p>';
20131
    case 'admin/build/trigger/cron':
210
      return $explanation . '<p>' . t('Below you can assign actions to run
during each pass of a <a href="@cron">cron maintenance task</a>.',
array('@cron' => url('admin/reports/status'))) . '</p>';
22131
    case 'admin/build/trigger/taxonomy':
230
      return $explanation . '<p>' . t('Below you can assign actions to run
when certain taxonomy-related triggers happen. For example, you could send
an e-mail to an administrator when a term is deleted.') . '</p>';
24131
    case 'admin/build/trigger/user':
250
      return $explanation . '<p>' . t("Below you can assign actions to run
when certain user-related triggers happen. For example, you could send an
e-mail to an administrator when a user account is deleted.") . '</p>';
26131
    case 'admin/help#trigger':
2748
      $output = '<p>' . t('The Trigger module provides the ability to
trigger <a href="@actions">actions</a> upon system events, such as when new
content is added or when a user logs in.', array('@actions' =>
url('admin/settings/actions'))) . '</p>';
2848
      $output .= '<p>' . t('The combination of actions and triggers can
perform many useful tasks, such as e-mailing an administrator if a user
account is deleted, or automatically unpublishing comments that contain
certain words. By default, there are five "contexts" of events (Comments,
Content, Cron, Taxonomy, and Users), but more may be added by additional
modules.') . '</p>';
2948
      $output .= '<p>' . t('For more information, see the online handbook
entry for <a href="@trigger">Trigger module</a>.', array('@trigger' =>
'http://drupal.org/handbook/modules/trigger/')) . '</p>';
3048
      return $output;
310
  }
3289
}
33
34
/**
35
 * Implementation of hook_menu().
36
 */
37185
function trigger_menu() {
381
  $items['admin/build/trigger'] = array(
391
    'title' => 'Triggers',
401
    'description' => 'Tell Drupal when to execute actions.',
411
    'page callback' => 'trigger_assign',
421
    'access callback' => 'trigger_access_check',
431
    'access arguments' => array('node'),
44
  );
45
  // We don't use a menu wildcard here because these are tabs,
46
  // not invisible items.
471
  $items['admin/build/trigger/node'] = array(
481
    'title' => 'Content',
491
    'page callback' => 'trigger_assign',
501
    'page arguments' => array('node'),
511
    'access callback' => 'trigger_access_check',
521
    'access arguments' => array('node'),
531
    'type' => MENU_LOCAL_TASK,
54
  );
551
  $items['admin/build/trigger/user'] = array(
561
    'title' => 'Users',
571
    'page callback' => 'trigger_assign',
581
    'page arguments' => array('user'),
591
    'access callback' => 'trigger_access_check',
601
    'access arguments' => array('user'),
611
    'type' => MENU_LOCAL_TASK,
62
  );
631
  $items['admin/build/trigger/comment'] = array(
641
    'title' => 'Comments',
651
    'page callback' => 'trigger_assign',
661
    'page arguments' => array('comment'),
671
    'access callback' => 'trigger_access_check',
681
    'access arguments' => array('comment'),
691
    'type' => MENU_LOCAL_TASK,
70
  );
711
  $items['admin/build/trigger/taxonomy'] = array(
721
    'title' => 'Taxonomy',
731
    'page callback' => 'trigger_assign',
741
    'page arguments' => array('taxonomy'),
751
    'access callback' => 'trigger_access_check',
761
    'access arguments' => array('taxonomy'),
771
    'type' => MENU_LOCAL_TASK,
78
  );
791
  $items['admin/build/trigger/cron'] = array(
801
    'title' => 'Cron',
811
    'page callback' => 'trigger_assign',
821
    'page arguments' => array('cron'),
831
    'access arguments' => array('administer actions'),
841
    'type' => MENU_LOCAL_TASK,
85
  );
86
87
  // We want contributed modules to be able to describe
88
  // their hooks and have actions assignable to them.
891
  $hooks = module_invoke_all('hook_info');
901
  foreach ($hooks as $module => $hook) {
91
    // We've already done these.
921
    if (in_array($module, array('node', 'comment', 'user', 'system',
'taxonomy'))) {
931
      continue;
940
    }
950
    $info = db_result(db_query("SELECT info FROM {system} WHERE name =
'%s'", $module));
960
    $info = unserialize($info);
970
    $nice_name = $info['name'];
980
    $items["admin/build/trigger/$module"] = array(
990
      'title' => $nice_name,
1000
      'page callback' => 'trigger_assign',
1010
      'page arguments' => array($module),
1020
      'access arguments' => array($module),
1030
      'type' => MENU_LOCAL_TASK,
104
    );
1050
  }
1061
  $items['admin/build/trigger/unassign'] = array(
1071
    'title' => 'Unassign',
1081
    'description' => 'Unassign an action from a trigger.',
1091
    'page callback' => 'drupal_get_form',
1101
    'page arguments' => array('trigger_unassign'),
1111
    'access arguments' => array('administer actions'),
1121
    'type' => MENU_CALLBACK,
113
  );
114
1151
  return $items;
1160
}
117
118
/**
119
 * Access callback for menu system.
120
 */
121185
function trigger_access_check($module) {
12248
  return (module_exists($module) && user_access('administer actions'));
1230
}
124
125
/**
126
 * Get the aids of actions to be executed for a hook-op combination.
127
 *
128
 * @param $hook
129
 *   The name of the hook being fired.
130
 * @param $op
131
 *   The name of the operation being executed. Defaults to an empty string
132
 *   because some hooks (e.g., hook_cron()) do not have operations.
133
 * @return
134
 *   An array of action IDs.
135
 */
136185
function _trigger_get_hook_aids($hook, $op = '') {
13770
  $aids = array();
13870
  $result = db_query("SELECT aa.aid, a.type FROM {trigger_assignments} aa
LEFT JOIN {actions} a ON aa.aid = a.aid WHERE aa.hook = '%s' AND aa.op =
'%s' ORDER BY weight", $hook, $op);
13970
  while ($action = db_fetch_object($result)) {
1406
    $aids[$action->aid]['type'] = $action->type;
1416
  }
14270
  return $aids;
1430
}
144
145
/**
146
 * Implementation of hook_theme().
147
 */
148185
function trigger_theme() {
149
  return array(
150
    'trigger_display' => array(
1511
      'arguments' => array('element'),
1521
      'file' => 'trigger.admin.inc',
1531
    ),
1541
  );
1550
}
156
157
/**
158
 * Implementation of hook_forms(). We reuse code by using the
159
 * same assignment form definition for each node-op combination.
160
 */
161185
function trigger_forms() {
16254
  $hooks = module_invoke_all('hook_info');
16354
  foreach ($hooks as $module => $info) {
16454
    foreach ($hooks[$module] as $hook => $ops) {
16554
      foreach ($ops as $op => $description) {
16654
        $forms['trigger_' . $hook . '_' . $op . '_assign_form'] =
array('callback' => 'trigger_assign_form');
16754
      }
16854
    }
16954
  }
170
17154
  return $forms;
1720
}
173
174
/**
175
 * When an action is called in a context that does not match its type,
176
 * the object that the action expects must be retrieved. For example, when
177
 * an action that works on users is called during the node hook, the
178
 * user object is not available since the node hook doesn't pass it.
179
 * So here we load the object the action expects.
180
 *
181
 * @param $type
182
 *   The type of action that is about to be called.
183
 * @param $node
184
 *   The node that was passed via the nodeapi hook.
185
 * @return
186
 *   The object expected by the action that is about to be called.
187
 */
188185
function _trigger_normalize_node_context($type, $node) {
189
  switch ($type) {
190
    // If an action that works on comments is being called in a node
context,
191
    // the action is expecting a comment object. But we do not know which
comment
192
    // to give it. The first? The most recent? All of them? So comment
actions
193
    // in a node context are not supported.
194
195
    // An action that works on users is being called in a node context.
196
    // Load the user object of the node's author.
1970
    case 'user':
1980
      return user_load(array('uid' => $node->uid));
1990
  }
2000
}
201
202
/**
203
 * Implementation of hook_nodeapi().
204
 */
205185
function trigger_nodeapi(&$node, $op, $a3, $a4) {
206
  // Keep objects for reuse so that changes actions make to objects can
persist.
20723
  static $objects;
208
  // Prevent recursion by tracking which operations have already been
called.
20923
  static $recursion;
210
  // Support a subset of operations.
21123
  if (!in_array($op, array('view', 'update', 'presave', 'insert',
'delete')) || isset($recursion[$op])) {
21223
    return;
2130
  }
21416
  $recursion[$op] = TRUE;
215
21616
  $aids = _trigger_get_hook_aids('nodeapi', $op);
21716
  if (!$aids) {
21816
    return;
2190
  }
220
  $context = array(
2216
    'hook' => 'nodeapi',
2226
    'op' => $op,
2236
  );
224
225
  // We need to get the expected object if the action's type is not
'node'.
226
  // We keep the object in $objects so we can reuse it if we have multiple
actions
227
  // that make changes to an object.
2286
  foreach ($aids as $aid => $action_info) {
2296
    if ($action_info['type'] != 'node') {
2300
      if (!isset($objects[$action_info['type']])) {
2310
        $objects[$action_info['type']] =
_trigger_normalize_node_context($action_info['type'], $node);
2320
      }
233
      // Since we know about the node, we pass that info along to the
action.
2340
      $context['node'] = $node;
2350
      $result = actions_do($aid, $objects[$action_info['type']], $context,
$a4, $a4);
2360
    }
237
    else {
2386
      actions_do($aid, $node, $context, $a3, $a4);
239
    }
2406
  }
2416
}
242
243
/**
244
 * When an action is called in a context that does not match its type,
245
 * the object that the action expects must be retrieved. For example, when
246
 * an action that works on nodes is called during the comment hook, the
247
 * node object is not available since the comment hook doesn't pass it.
248
 * So here we load the object the action expects.
249
 *
250
 * @param $type
251
 *   The type of action that is about to be called.
252
 * @param $comment
253
 *   The comment that was passed via the comment hook.
254
 * @return
255
 *   The object expected by the action that is about to be called.
256
 */
257185
function _trigger_normalize_comment_context($type, $comment) {
258
  switch ($type) {
259
    // An action that works with nodes is being called in a comment
context.
2600
    case 'node':
2610
      return node_load(is_array($comment) ? $comment['nid'] :
$comment->nid);
262
263
    // An action that works on users is being called in a comment context.
2640
    case 'user':
2650
      return user_load(array('uid' => is_array($comment) ? $comment['uid']
: $comment->uid));
2660
  }
2670
}
268
269
/**
270
 * Implementation of hook_comment().
271
 */
272185
function trigger_comment($a1, $op) {
273
  // Keep objects for reuse so that changes actions make to objects can
persist.
2740
  static $objects;
275
  // We support a subset of operations.
2760
  if (!in_array($op, array('insert', 'update', 'delete', 'view'))) {
2770
    return;
2780
  }
2790
  $aids = _trigger_get_hook_aids('comment', $op);
280
  $context = array(
2810
    'hook' => 'comment',
2820
    'op' => $op,
2830
  );
284
  // We need to get the expected object if the action's type is not
'comment'.
285
  // We keep the object in $objects so we can reuse it if we have multiple
actions
286
  // that make changes to an object.
2870
  foreach ($aids as $aid => $action_info) {
2880
    if ($action_info['type'] != 'comment') {
2890
      if (!isset($objects[$action_info['type']])) {
2900
        $objects[$action_info['type']] =
_trigger_normalize_comment_context($action_info['type'], $a1);
2910
      }
292
      // Since we know about the comment, we pass it along to the action
293
      // in case it wants to peek at it.
2940
      $context['comment'] = (object) $a1;
2950
      actions_do($aid, $objects[$action_info['type']], $context);
2960
    }
297
    else {
2980
      actions_do($aid, (object) $a1, $context);
299
    }
3000
  }
3010
}
302
303
/**
304
 * Implementation of hook_cron().
305
 */
306185
function trigger_cron() {
3070
  $aids = _trigger_get_hook_aids('cron');
308
  $context = array(
3090
    'hook' => 'cron',
3100
    'op' => '',
3110
  );
312
  // Cron does not act on any specific object.
3130
  $object = NULL;
3140
  actions_do(array_keys($aids), $object, $context);
3150
}
316
317
/**
318
 * When an action is called in a context that does not match its type,
319
 * the object that the action expects must be retrieved. For example, when
320
 * an action that works on nodes is called during the user hook, the
321
 * node object is not available since the user hook doesn't pass it.
322
 * So here we load the object the action expects.
323
 *
324
 * @param $type
325
 *   The type of action that is about to be called.
326
 * @param $account
327
 *   The account object that was passed via the user hook.
328
 * @return
329
 *   The object expected by the action that is about to be called.
330
 */
331185
function _trigger_normalize_user_context($type, $account) {
332
  switch ($type) {
333
    // If an action that works on comments is being called in a user
context,
334
    // the action is expecting a comment object. But we have no way of
335
    // determining the appropriate comment object to pass. So comment
336
    // actions in a user context are not supported.
337
338
    // An action that works with nodes is being called in a user context.
339
    // If a single node is being viewed, return the node.
3400
    case 'node':
341
      // If we are viewing an individual node, return the node.
3420
      if ((arg(0) == 'node') && is_numeric(arg(1)) && (arg(2) == NULL)) {
3430
        return node_load(array('nid' => arg(1)));
3440
      }
3450
  }
3460
}
347
348
/**
349
 * Implementation of hook_user().
350
 */
351185
function trigger_user($op, &$edit, &$account, $category = NULL) {
352
  // Keep objects for reuse so that changes actions make to objects can
persist.
353173
  static $objects;
354
  // We support a subset of operations.
355173
  if (!in_array($op, array('login', 'logout', 'insert', 'update', 'delete',
'view'))) {
356156
    return;
3570
  }
35854
  $aids = _trigger_get_hook_aids('user', $op);
359
  $context = array(
36054
    'hook' => 'user',
36154
    'op' => $op,
36254
    'form_values' => &$edit,
36354
  );
36454
  foreach ($aids as $aid => $action_info) {
3650
    if ($action_info['type'] != 'user') {
3660
      if (!isset($objects[$action_info['type']])) {
3670
        $objects[$action_info['type']] =
_trigger_normalize_user_context($action_info['type'], $account);
3680
      }
3690
      $context['account'] = $account;
3700
      actions_do($aid, $objects[$action_info['type']], $context);
3710
    }
372
    else {
3730
      actions_do($aid, $account, $context, $category);
374
    }
3750
  }
37654
}
377
378
/**
379
 * Implementation of hook_taxonomy().
380
 */
381185
function trigger_taxonomy($op, $type, $array) {
3820
  if ($type != 'term') {
3830
    return;
3840
  }
3850
  $aids = _trigger_get_hook_aids('taxonomy', $op);
386
  $context = array(
3870
    'hook' => 'taxonomy',
388
    'op' => $op
3890
  );
3900
  foreach ($aids as $aid => $action_info) {
3910
    actions_do($aid, (object) $array, $context);
3920
  }
3930
}
394
395
/**
396
 * Often we generate a select field of all actions. This function
397
 * generates the options for that select.
398
 *
399
 * @param $type
400
 *   One of 'node', 'user', 'comment'.
401
 * @return
402
 *   Array keyed by action ID.
403
 */
404185
function trigger_options($type = 'all') {
4050
  $options = array(t('Choose an action'));
4060
  foreach (actions_actions_map(actions_get_all_actions()) as $aid =>
$action) {
4070
    $options[$action['type']][$aid] = $action['description'];
4080
  }
409
4100
  if ($type == 'all') {
4110
    return $options;
4120
  }
413
  else {
4140
    return $options[$type];
415
  }
4160
}
417
418
/**
419
 * Implementation of hook_actions_delete().
420
 *
421
 * Remove all trigger entries for the given action, when deleted.
422
 */
423185
function trigger_actions_delete($aid) {
4240
  db_query("DELETE FROM {trigger_assignments} WHERE aid = '%s'", $aid);
4250
}
426185