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

Line #Times calledCode
1
<?php
2
// $Id: trigger.module,v 1.20 2008/10/06 12:55:56 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
 * Simple wrapper function to make user hooks work with new entry points.
204
 *
205
 * @TODO: Take advantage of the new API and reorganise/remove this
function. 
206
 */
207185
function _trigger_nodeapi(&$node, $op, $a3, $a4) {
208
  // Keep objects for reuse so that changes actions make to objects can
persist.
20916
  static $objects;
210
  // Prevent recursion by tracking which operations have already been
called.
21116
  static $recursion;
212
  // Support a subset of operations.
21316
  if (!in_array($op, array('view', 'update', 'presave', 'insert',
'delete')) || isset($recursion[$op])) {
2140
    return;
2150
  }
21616
  $recursion[$op] = TRUE;
217
21816
  $aids = _trigger_get_hook_aids('nodeapi', $op);
21916
  if (!$aids) {
22016
    return;
2210
  }
222
  $context = array(
2236
    'hook' => 'nodeapi',
2246
    'op' => $op,
2256
  );
226
227
  // We need to get the expected object if the action's type is not 'node'.
228
  // We keep the object in $objects so we can reuse it if we have multiple
actions
229
  // that make changes to an object.
2306
  foreach ($aids as $aid => $action_info) {
2316
    if ($action_info['type'] != 'node') {
2320
      if (!isset($objects[$action_info['type']])) {
2330
        $objects[$action_info['type']] =
_trigger_normalize_node_context($action_info['type'], $node);
2340
      }
235
      // Since we know about the node, we pass that info along to the
action.
2360
      $context['node'] = $node;
2370
      $result = actions_do($aid, $objects[$action_info['type']], $context,
$a4, $a4);
2380
    }
239
    else {
2406
      actions_do($aid, $node, $context, $a3, $a4);
241
    }
2426
  }
2436
}
244
 
245
/**
246
 * Implementation of hook_nodeapi_view().
247
 */
248185
function trigger_nodeapi_view(&$node, $a3, $a4) {
24910
  _trigger_nodeapi($node, 'view', $a3, $a4);
25010
}
251
252
/**
253
 * Implementation of hook_nodeapi_update().
254
 */
255185
function trigger_nodeapi_update(&$node, $a3, $a4) {
2560
  _trigger_nodeapi($node, 'update', $a3, $a4);
2570
}
258
259
/**
260
 * Implementation of hook_nodeapi_presave().
261
 */
262185
function trigger_nodeapi_presave(&$node, $a3, $a4) {
2636
  _trigger_nodeapi($node, 'presave', $a3, $a4);
2646
}
265
266
/**
267
 * Implementation of hook_nodeapi_insert().
268
 */
269185
function trigger_nodeapi_insert(&$node, $a3, $a4) {
2706
  _trigger_nodeapi($node, 'insert', $a3, $a4);
2716
}
272
273
/**
274
 * Implementation of hook_nodeapi_delete().
275
 */
276185
function trigger_nodeapi_delete(&$node, $a3, $a4) {
2770
  _trigger_nodeapi($node, 'delete', $a3, $a4);
2780
}
279
280
/**
281
 * When an action is called in a context that does not match its type,
282
 * the object that the action expects must be retrieved. For example, when
283
 * an action that works on nodes is called during the comment hook, the
284
 * node object is not available since the comment hook doesn't pass it.
285
 * So here we load the object the action expects.
286
 *
287
 * @param $type
288
 *   The type of action that is about to be called.
289
 * @param $comment
290
 *   The comment that was passed via the comment hook.
291
 * @return
292
 *   The object expected by the action that is about to be called.
293
 */
294185
function _trigger_normalize_comment_context($type, $comment) {
295
  switch ($type) {
296
    // An action that works with nodes is being called in a comment
context.
2970
    case 'node':
2980
      return node_load(is_array($comment) ? $comment['nid'] :
$comment->nid);
299
300
    // An action that works on users is being called in a comment context.
3010
    case 'user':
3020
      return user_load(array('uid' => is_array($comment) ? $comment['uid']
: $comment->uid));
3030
  }
3040
}
305
306
/**
307
 * Implementation of hook_comment().
308
 */
309185
function trigger_comment($a1, $op) {
310
  // Keep objects for reuse so that changes actions make to objects can
persist.
3110
  static $objects;
312
  // We support a subset of operations.
3130
  if (!in_array($op, array('insert', 'update', 'delete', 'view'))) {
3140
    return;
3150
  }
3160
  $aids = _trigger_get_hook_aids('comment', $op);
317
  $context = array(
3180
    'hook' => 'comment',
3190
    'op' => $op,
3200
  );
321
  // We need to get the expected object if the action's type is not
'comment'.
322
  // We keep the object in $objects so we can reuse it if we have multiple
actions
323
  // that make changes to an object.
3240
  foreach ($aids as $aid => $action_info) {
3250
    if ($action_info['type'] != 'comment') {
3260
      if (!isset($objects[$action_info['type']])) {
3270
        $objects[$action_info['type']] =
_trigger_normalize_comment_context($action_info['type'], $a1);
3280
      }
329
      // Since we know about the comment, we pass it along to the action
330
      // in case it wants to peek at it.
3310
      $context['comment'] = (object) $a1;
3320
      actions_do($aid, $objects[$action_info['type']], $context);
3330
    }
334
    else {
3350
      $a1 = (object) $a1;
3360
      actions_do($aid, $a1, $context);
337
    }
3380
  }
3390
}
340
341
/**
342
 * Implementation of hook_cron().
343
 */
344185
function trigger_cron() {
3450
  $aids = _trigger_get_hook_aids('cron');
346
  $context = array(
3470
    'hook' => 'cron',
3480
    'op' => '',
3490
  );
350
  // Cron does not act on any specific object.
3510
  $object = NULL;
3520
  actions_do(array_keys($aids), $object, $context);
3530
}
354
355
/**
356
 * When an action is called in a context that does not match its type,
357
 * the object that the action expects must be retrieved. For example, when
358
 * an action that works on nodes is called during the user hook, the
359
 * node object is not available since the user hook doesn't pass it.
360
 * So here we load the object the action expects.
361
 *
362
 * @param $type
363
 *   The type of action that is about to be called.
364
 * @param $account
365
 *   The account object that was passed via the user hook.
366
 * @return
367
 *   The object expected by the action that is about to be called.
368
 */
369185
function _trigger_normalize_user_context($type, $account) {
370
  switch ($type) {
371
    // If an action that works on comments is being called in a user
context,
372
    // the action is expecting a comment object. But we have no way of
373
    // determining the appropriate comment object to pass. So comment
374
    // actions in a user context are not supported.
375
376
    // An action that works with nodes is being called in a user context.
377
    // If a single node is being viewed, return the node.
3780
    case 'node':
379
      // If we are viewing an individual node, return the node.
3800
      if ((arg(0) == 'node') && is_numeric(arg(1)) && (arg(2) == NULL)) {
3810
        return node_load(array('nid' => arg(1)));
3820
      }
3830
  }
3840
}
385
386
/**
387
 * trigger_user_login 
388
 */
389185
function trigger_user_login(&$edit, &$account, $category) {
39018
  _trigger_user('login', $edit, $account, $category); 
39118
}
392
393
/**
394
 * Implementation of hook_user_logout().
395
 */
396185
function trigger_user_logout(&$edit, &$account, $category) {
39717
  _trigger_user('logout', $edit, $account, $category); 
39817
}
399
400
/**
401
 * Implementation of hook_user_insert().
402
 */
403185
function trigger_user_insert(&$edit, &$account, $category) {
4041
  _trigger_user('insert', $edit, $account, $category); 
4051
}
406
407
/**
408
 * Implementation of hook_user_update().
409
 */
410185
function trigger_user_update(&$edit, &$account, $category) {
4110
  _trigger_user('update', $edit, $account, $category); 
4120
}
413
414
/**
415
 * Implementation of hook_user_delete().
416
 */
417185
function trigger_user_delete(&$edit, &$account, $category) {
4180
  _trigger_user('delete', $edit, $account, $category); 
4190
}
420
421
/**
422
 * Implementation of hook_user_view().
423
 */
424185
function trigger_user_view(&$edit, &$account, $category) {
42518
  _trigger_user('view', $edit, $account, $category); 
42618
}
427
428
/**
429
 * Simple wrapper function to make user hooks work with new entry points.
430
 *
431
 * @TODO: Take advantage of the new API and reorganise/remove this
function. 
432
 */
433185
function _trigger_user($op, &$edit, &$account, $category = NULL) {
434
  // Keep objects for reuse so that changes actions make to objects can
persist.
43554
  static $objects;
436
  // We support a subset of operations.
43754
  if (!in_array($op, array('login', 'logout', 'insert', 'update', 'delete',
'view'))) {
4380
    return;
4390
  }
44054
  $aids = _trigger_get_hook_aids('user', $op);
441
  $context = array(
44254
    'hook' => 'user',
44354
    'op' => $op,
44454
    'form_values' => &$edit,
44554
  );
44654
  foreach ($aids as $aid => $action_info) {
4470
    if ($action_info['type'] != 'user') {
4480
      if (!isset($objects[$action_info['type']])) {
4490
        $objects[$action_info['type']] =
_trigger_normalize_user_context($action_info['type'], $account);
4500
      }
4510
      $context['account'] = $account;
4520
      actions_do($aid, $objects[$action_info['type']], $context);
4530
    }
454
    else {
4550
      actions_do($aid, $account, $context, $category);
456
    }
4570
  }
45854
}
459
460
/**
461
 * Implementation of hook_taxonomy().
462
 */
463185
function trigger_taxonomy($op, $type, $array) {
4640
  if ($type != 'term') {
4650
    return;
4660
  }
4670
  $aids = _trigger_get_hook_aids('taxonomy', $op);
468
  $context = array(
4690
    'hook' => 'taxonomy',
470
    'op' => $op
4710
  );
4720
  $_array = (object) $array;
4730
  foreach ($aids as $aid => $action_info) {
4740
    actions_do($aid, $_array, $context);
4750
  }
4760
}
477
478
/**
479
 * Often we generate a select field of all actions. This function
480
 * generates the options for that select.
481
 *
482
 * @param $type
483
 *   One of 'node', 'user', 'comment'.
484
 * @return
485
 *   Array keyed by action ID.
486
 */
487185
function trigger_options($type = 'all') {
4880
  $options = array(t('Choose an action'));
4890
  foreach (actions_actions_map(actions_get_all_actions()) as $aid =>
$action) {
4900
    $options[$action['type']][$aid] = $action['description'];
4910
  }
492
4930
  if ($type == 'all') {
4940
    return $options;
4950
  }
496
  else {
4970
    return $options[$type];
498
  }
4990
}
500
501
/**
502
 * Implementation of hook_actions_delete().
503
 *
504
 * Remove all trigger entries for the given action, when deleted.
505
 */
506185
function trigger_actions_delete($aid) {
5070
  db_query("DELETE FROM {trigger_assignments} WHERE aid = '%s'", $aid);
5080
}
509185