/[drupal]/contributions/modules/actions/actions.module
ViewVC logotype

Contents of /contributions/modules/actions/actions.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.31 - (show annotations) (download) (as text)
Mon Mar 17 04:21:38 2008 UTC (20 months, 1 week ago) by jvandyk
Branch: MAIN
CVS Tags: DRUPAL-5--2-0
Changes since 1.30: +1340 -431 lines
File MIME type: text/x-php
backport of Drupal 6 actions and triggers by sorenjones and jvandyk
1 <?php
2 // $Id$
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 include_once(drupal_get_path('module', 'actions') .'/actions.inc');
11
12 /**
13 * Implementation of hook_help().
14 */
15 function actions_help($section) {
16 $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>';
17 switch ($section) {
18 case 'admin/settings/actions':
19 case 'admin/settings/actions/manage':
20 $output = '<p>'. t('Actions are individual tasks that the system can do, such as unpublishing a piece of content or banning a user. Modules, such as the trigger module, can fire these actions when certain system events happen; for example, when a new post is added or when a user logs in. Modules may also provide additional actions.') .'</p>';
21 $output .= '<p>'. t('There are two types of actions: simple and advanced. Simple actions do not require any additional configuration, and are listed here automatically. Advanced actions can do more than simple actions; for example, send an e-mail to a specified address, or check for certain words within a piece of content. These actions need to be created and configured first before they may be used. To create an advanced action, select the action from the drop-down below and click the <em>Create</em> button.') .'</p>';
22 $output .= '<p>'. t('You may proceed to the <a href="@url">Triggers</a> page to assign these actions to system events.', array('@url' => url('admin/build/trigger'))) .'</p>';
23 return $output;
24 case 'admin/settings/actions/configure':
25 return t('An advanced action offers additional configuration options which may be filled out below. Changing the <em>Description</em> field is recommended, in order to better identify the precise action taking place. This description will be displayed in modules such as the trigger module when assigning actions to system events, so it is best if it is as descriptive as possible (for example, "Send e-mail to Moderation Team" rather than simply "Send e-mail").');
26 case 'admin/build/trigger/comment':
27 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>';
28 case 'admin/build/trigger/node':
29 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>';
30 case 'admin/build/trigger/cron':
31 return $explanation .'<p>'. t('Below you can assign actions to run during each pass of a cron maintenance task.</p>');
32 case 'admin/build/trigger/taxonomy':
33 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>';
34 case 'admin/build/trigger/user':
35 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>';
36 case 'admin/help#trigger':
37 $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>';
38 $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>';
39 $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>';
40 return $output;
41 }
42 }
43
44 /**
45 * Implementation of hook_menu().
46 */
47 function actions_menu($may_cache) {
48 $items = array();
49 $access = user_access('administer_actions');
50
51 if ($may_cache) {
52 $items[] = array(
53 'path' => 'admin/settings/actions',
54 'title' => t('Actions'),
55 'description' => t('Manage the actions defined for your site.'),
56 'callback' => 'actions_manage',
57 'access' => $access,
58 );
59 $items[] = array(
60 'path' => 'admin/settings/actions/manage',
61 'title' => t('Manage actions'),
62 'description' => t('Manage the actions defined for your site.'),
63 'callback' => 'actions_manage',
64 'type' => MENU_DEFAULT_LOCAL_TASK,
65 'weight' => -2,
66 );
67 $items[] = array(
68 'path' => 'admin/settings/actions/configure',
69 'title' => t('Configure an advanced action'),
70 'callback' => 'drupal_get_form',
71 'callback arguments' => array('actions_configure'),
72 'type' => MENU_CALLBACK,
73 );
74 $items[] = array(
75 'path' => 'admin/settings/actions/delete',
76 'title' => t('Delete action'),
77 'description' => t('Delete an action.'),
78 'callback' => 'drupal_get_form',
79 'callback arguments' => array('actions_delete_form'),
80 'type' => MENU_CALLBACK,
81 );
82 $items[] = array(
83 'path' => 'admin/settings/actions/orphan',
84 'title' => t('Remove orphans'),
85 'callback' => 'actions_remove_orphans',
86 'type' => MENU_CALLBACK,
87 );
88 $items[] = array(
89 'path' => 'admin/build/trigger',
90 'title' => t('Triggers'),
91 'description' => t('Tell Drupal when to execute actions.'),
92 'callback' => 'actions_assign',
93 'access' => $access,
94 );
95 // We don't use a menu wildcard here because these are tabs,
96 // not invisible items.
97 $items[] = array(
98 'path' => 'admin/build/trigger/node',
99 'title' => t('Content'),
100 'callback' => 'actions_assign',
101 'callback arguments' => array('node'),
102 'access' => $access,
103 'type' => MENU_LOCAL_TASK,
104 );
105 $items[] = array(
106 'path' => 'admin/build/trigger/user',
107 'title' => t('Users'),
108 'callback' => 'actions_assign',
109 'callback arguments' => array('user'),
110 'access' => $access,
111 'type' => MENU_LOCAL_TASK,
112 );
113 $items[] = array(
114 'path' => 'admin/build/trigger/comment',
115 'title' => t('Comments'),
116 'callback' => 'actions_assign',
117 'callback arguments' => array('comment'),
118 'access' => $access,
119 'type' => MENU_LOCAL_TASK,
120 );
121 $items[] = array(
122 'path' => 'admin/build/trigger/taxonomy',
123 'title' => t('Taxonomy'),
124 'callback' => 'actions_assign',
125 'callback arguments' => array('taxonomy'),
126 'access' => $access,
127 'type' => MENU_LOCAL_TASK,
128 );
129 $items[] = array(
130 'path' => 'admin/build/trigger/cron',
131 'title' => t('Cron'),
132 'callback' => 'actions_assign',
133 'callback arguments' => array('cron'),
134 'type' => MENU_LOCAL_TASK,
135 );
136
137 // We want contributed modules to be able to describe
138 // their hooks and have actions assignable to them.
139 $hooks = module_invoke_all('hook_info');
140 foreach ($hooks as $module => $hook) {
141 // We've already done these.
142 if (in_array($module, array('node', 'comment', 'user', 'system', 'taxonomy'))) {
143 continue;
144 }
145 $info = db_result(db_query("SELECT info FROM {system} WHERE name = '%s'", $module));
146 $info = unserialize($info);
147 $nice_name = $info['name'];
148 $items[] = array(
149 'path' => 'admin/build/trigger/'. $module,
150 'title' => $nice_name,
151 'callback' => 'actions_assign',
152 'callback arguments' => array($module),
153 'access' => $module,
154 'type' => MENU_LOCAL_TASK,
155 );
156 }
157 $items[] = array(
158 'path' => 'admin/build/trigger/unassign',
159 'title' => t('Unassign'),
160 'description' => t('Unassign an action from a trigger.'),
161 'callback' => 'drupal_get_form',
162 'callback arguments' => array('actions_unassign'),
163 'type' => MENU_CALLBACK,
164 );
165
166 return $items;
167 }
168 }
169
170 /**
171 * Implementation of hook_perm().
172 */
173 function actions_perm() {
174 return array('administer_actions');
175 }
176
177 /**
178 * Access callback for menu system.
179 */
180 function actions_access_check($module) {
181 return (module_exists($module) && user_access('administer actions'));
182 }
183
184 /**
185 * Menu callback. Display an overview of available and configured actions.
186 */
187 function actions_manage() {
188 $output = '';
189 $actions = actions_list();
190 actions_synchronize($actions);
191 $actions_map = actions_actions_map($actions);
192 $options = array(t('Choose an advanced action'));
193 $unconfigurable = array();
194
195 foreach ($actions_map as $key => $array) {
196 if ($array['configurable']) {
197 $options[$key] = $array['description'] .'...';
198 }
199 else {
200 $unconfigurable[] = $array;
201 }
202 }
203
204 $row = array();
205 $instances_present = db_fetch_object(db_query("SELECT aid FROM {actions} WHERE parameters != ''"));
206 $header = array(
207 array('data' => t('Action type'), 'field' => 'type'),
208 array('data' => t('Description'), 'field' => 'description'),
209 array('data' => $instances_present ? t('Operations') : '', 'colspan' => '2')
210 );
211 $sql = 'SELECT * FROM {actions}';
212 $result = pager_query($sql . tablesort_sql($header), 50);
213 while ($action = db_fetch_object($result)) {
214 $row[] = array(
215 array('data' => $action->type),
216 array('data' => $action->description),
217 array('data' => $action->parameters ? l(t('configure'), "admin/settings/actions/configure/$action->aid") : ''),
218 array('data' => $action->parameters ? l(t('delete'), "admin/settings/actions/delete/$action->aid") : '')
219 );
220 }
221
222 if ($row) {
223 $pager = theme('pager', NULL, 50, 0);
224 if (!empty($pager)) {
225 $row[] = array(array('data' => $pager, 'colspan' => '3'));
226 }
227 $output .= '<h3>'. t('Actions available to Drupal:') .'</h3>';
228 $output .= theme('table', $header, $row);
229 }
230
231 if ($actions_map) {
232 $output .= drupal_get_form('actions_manage_form', $options);
233 }
234
235 return $output;
236 }
237
238 /**
239 * Define the form for the actions overview page.
240 *
241 * @see actions_manage_form_submit()
242 * @ingroup forms
243 * @param $options
244 * An array of configurable actions.
245 * @return
246 * Form definition.
247 */
248 function actions_manage_form($options = array()) {
249 $form['parent'] = array(
250 '#type' => 'fieldset',
251 '#title' => t('Make a new advanced action available'),
252 '#prefix' => '<div class="container-inline">',
253 '#suffix' => '</div>',
254 );
255 $form['parent']['action'] = array(
256 '#type' => 'select',
257 '#default_value' => '',
258 '#options' => $options,
259 '#description' => '',
260 );
261 $form['parent']['buttons']['submit'] = array(
262 '#type' => 'submit',
263 '#value' => t('Create'),
264 );
265 return $form;
266 }
267
268 /**
269 * Process actions_manage form submissions.
270 */
271 function actions_manage_form_submit($form_id, $form_values) {
272 if ($form_values['action']) {
273 return 'admin/settings/actions/configure/'. $form_values['action'];
274 }
275 }
276
277 /**
278 * Menu callback. Create the form for configuration of a single action.
279 *
280 * We provide the "Description" field. The rest of the form
281 * is provided by the action. We then provide the Save button.
282 * Because we are combining unknown form elements with the action
283 * configuration form, we use actions_ prefix on our elements.
284 *
285 * @see actions_configure_validate()
286 * @see actions_configure_submit()
287 * @param $action
288 * md5 hash of action ID or an integer. If it's an md5 hash, we
289 * are creating a new instance. If it's an integer, we're editing
290 * an existing instance.
291 * @return
292 * Form definition.
293 */
294 function actions_configure($action = NULL) {
295 if ($action === NULL) {
296 drupal_goto('admin/settings/actions');
297 }
298
299 $actions_map = actions_actions_map(actions_list());
300 $edit = array();
301
302 // Numeric action denotes saved instance of a configurable action;
303 // else we are creating a new action instance.
304 if (is_numeric($action)) {
305 $aid = $action;
306 // Load stored parameter values from database.
307 $data = db_fetch_object(db_query("SELECT * FROM {actions} WHERE aid = %d", intval($aid)));
308 $edit['actions_description'] = $data->description;
309 $edit['actions_type'] = $data->type;
310 $function = $data->callback;
311 $action = md5($data->callback);
312 $params = unserialize($data->parameters);
313 if ($params) {
314 foreach ($params as $name => $val) {
315 $edit[$name] = $val;
316 }
317 }
318 }
319 else {
320 $function = $actions_map[$action]['callback'];
321 $edit['actions_description'] = $actions_map[$action]['description'];
322 $edit['actions_type'] = $actions_map[$action]['type'];
323 }
324
325 $form = array();
326 $form['actions_description'] = array(
327 '#type' => 'textfield',
328 '#title' => t('Description'),
329 '#default_value' => $edit['actions_description'],
330 '#maxlength' => '255',
331 '#description' => t('A unique description for this advanced action. This description will be displayed in the interface of modules that integrate with actions, such as Trigger module.'),
332 '#weight' => -10
333 );
334 $action_form = $function .'_form';
335 $form = array_merge($form, $action_form($edit));
336 $form['actions_type'] = array(
337 '#type' => 'value',
338 '#value' => $edit['actions_type'],
339 );
340 $form['actions_action'] = array(
341 '#type' => 'hidden',
342 '#value' => $action,
343 );
344 // $aid is set when configuring an existing action instance.
345 if (isset($aid)) {
346 $form['actions_aid'] = array(
347 '#type' => 'hidden',
348 '#value' => $aid,
349 );
350 }
351 $form['actions_configured'] = array(
352 '#type' => 'hidden',
353 '#value' => '1',
354 );
355 $form['buttons']['submit'] = array(
356 '#type' => 'submit',
357 '#value' => t('Save'),
358 '#weight' => 13
359 );
360
361 return $form;
362 }
363
364 /**
365 * Validate actions_configure form submissions.
366 */
367 function actions_configure_validate($form_id, $form_values) {
368 $function = actions_function_lookup($form_values['actions_action']) .'_validate';
369 // Hand off validation to the action.
370 if (function_exists($function)) {
371 $function('validate', $form_values);
372 }
373 }
374
375 /**
376 * Process actions_configure form submissions.
377 */
378 function actions_configure_submit($form_id, $form_values) {
379 $function = actions_function_lookup($form_values['actions_action']);
380 $submit_function = $function .'_submit';
381
382 // Action will return keyed array of values to store.
383 $params = $submit_function($form_id, $form_values);
384 $aid = isset($form_values['actions_aid']) ? $form_values['actions_aid'] : NULL;
385
386 actions_save($function, $form_values['actions_type'], $params, $form_values['actions_description'], $aid);
387 drupal_set_message(t('The action has been successfully saved.'));
388
389 return 'admin/settings/actions/manage';
390 }
391
392 /**
393 * Create the form for confirmation of deleting an action.
394 *
395 * @ingroup forms
396 * @see actions_delete_form_submit()
397 */
398 function actions_delete_form($aid) {;
399 $action = actions_load($aid);
400 $form['aid'] = array(
401 '#type' => 'hidden',
402 '#value' => $action->aid,
403 );
404 return confirm_form($form,
405 t('Are you sure you want to delete the action %action?', array('%action' => $action->description)),
406 'admin/settings/actions/manage',
407 t('This cannot be undone.'),
408 t('Delete'), t('Cancel')
409 );
410 }
411
412 /**
413 * Process actions_delete form submissions.
414 *
415 * Post-deletion operations for action deletion.
416 */
417 function actions_delete_form_submit($form_id, $form_values) {
418 $aid = $form_values['aid'];
419 $action = actions_load($aid);
420 actions_delete($aid);
421 $description = check_plain($action->description);
422 watchdog('user', 'Deleted action %aid (%action)', array('%aid' => $aid, '%action' => $description));
423 drupal_set_message(t('Action %action was deleted', array('%action' => $description)));
424 return 'admin/settings/actions/manage';
425 }
426
427 /**
428 * Post-deletion operations for deleting action orphans.
429 *
430 * @param $orphaned
431 * An array of orphaned actions.
432 */
433 function actions_action_delete_orphans_post($orphaned) {
434 foreach ($orphaned as $callback) {
435 drupal_set_message(t("Deleted orphaned action (%action).", array('%action' => $callback)));
436 }
437 }
438
439 /**
440 * Remove actions that are in the database but not supported by any enabled module.
441 */
442 function actions_remove_orphans() {
443 actions_synchronize(actions_list(), TRUE);
444 drupal_goto('admin/settings/actions/manage');
445 }
446
447 /**
448 * Admin page callbacks for the trigger module.
449 */
450
451 /**
452 * Build the form that allows users to assign actions to hooks.
453 *
454 * @param $type
455 * Name of hook.
456 * @return
457 * HTML form.
458 */
459 function actions_assign($type = NULL) {
460
461 // If no type is specified we default to node actions, since they
462 // are the most common.
463 if (!isset($type)) {
464 drupal_goto('admin/build/trigger/node');
465 }
466 if ($type == 'node') {
467 $type = 'nodeapi';
468 }
469
470 $output = '';
471 $hooks = module_invoke_all('hook_info');
472 foreach ($hooks as $module => $hook) {
473 if (isset($hook[$type])) {
474 foreach ($hook[$type] as $op => $description) {
475 $form_id = 'actions_'. $type .'_'. $op .'_assign_form';
476 $output .= drupal_get_form($form_id, $type, $op, $description['runs when']);
477 }
478 }
479 }
480 return $output;
481 }
482
483 /**
484 * Confirm removal of an assigned action.
485 *
486 * @param $hook
487 * @param $op
488 * @param $aid
489 * The action ID.
490 * @ingroup forms
491 * @see actions_unassign_submit()
492 */
493 function actions_unassign($hook = NULL, $op = NULL, $aid = NULL, $form_values = NULL) {
494 if (!($hook && $op && $aid)) {
495 drupal_goto('admin/build/trigger/assign');
496 }
497
498 $form['hook'] = array(
499 '#type' => 'value',
500 '#value' => $hook,
501 );
502 $form['operation'] = array(
503 '#type' => 'value',
504 '#value' => $op,
505 );
506 $form['aid'] = array(
507 '#type' => 'value',
508 '#value' => $aid,
509 );
510
511 $action = db_result(db_query("SELECT aid FROM {actions} WHERE MD5(aid) = '%s' AND parameters != ''", $aid));
512 $actions = actions_get_all_actions();
513
514 $destination = 'admin/build/trigger/'. ($hook == 'nodeapi' ? 'node' : $hook);
515
516 return confirm_form($form,
517 t('Are you sure you want to unassign the action %title?', array('%title' => $actions[$action]['description'])),
518 $destination,
519 t('You can assign it again later if you wish.'),
520 t('Unassign'), t('Cancel')
521 );
522 }
523
524 function actions_unassign_submit($form_id, $form_values) {
525 if ($form_values['confirm'] == 1) {
526 $aid = db_result(db_query("SELECT aid FROM {actions} WHERE MD5(aid) = '%s'", $form_values['aid']));
527 db_query("DELETE FROM {actions_assignments} WHERE hook = '%s' AND op = '%s' AND aid = '%s'", $form_values['hook'], $form_values['operation'], $aid);
528 watchdog('actions', t('Action %action has been unassigned.', array('%action' => check_plain($actions[$aid]['description']))));
529 drupal_set_message(t('Action %action has been unassigned.', array('%action' => $actions[$aid]['description'])));
530 $hook = $form_values['hook'] == 'nodeapi' ? 'node' : $form_values['hook'];
531 return 'admin/build/trigger/'. $hook;
532 }
533 else {
534 drupal_goto('admin/build/trigger');
535 }
536 }
537
538 /**
539 * Create the form definition for assigning an action to a hook-op combination.
540 *
541 * @param $form_values
542 * Information about the current form.
543 * @param $hook
544 * The name of the hook, e.g., 'nodeapi'.
545 * @param $op
546 * The name of the hook operation, e.g., 'insert'.
547 * @param $description
548 * A plain English description of what this hook operation does.
549 * @return
550 *
551 * @ingoup forms
552 * @see actions_assign_form_validate()
553 * @see actions_assign_form_submit()
554 */
555 function actions_assign_form($hook, $op, $description, $form_value = NULL) {
556 $form['hook'] = array(
557 '#type' => 'hidden',
558 '#value' => $hook,
559 );
560 $form['operation'] = array(
561 '#type' => 'hidden',
562 '#value' => $op,
563 );
564 // All of these forms use the same validate and submit functions.
565 $form['#validate'] = array('actions_assign_form_validate' => array($form_id, &$form));
566 $form['#submit'] = array('actions_assign_form_submit' => array($form_id, &$form));
567
568 $options = array();
569 $functions = array();
570 // Restrict the options list to actions that declare support for this hook-op
571 // combination.
572 foreach (actions_list() as $func => $metadata) {
573 if (isset($metadata['hooks']['any']) || (isset($metadata['hooks'][$hook]) && is_array($metadata['hooks'][$hook]) && (in_array($op, $metadata['hooks'][$hook])))) {
574 $functions[] = $func;
575 }
576 }
577 foreach (actions_actions_map(actions_get_all_actions()) as $aid => $action) {
578 if (in_array($action['callback'], $functions)) {
579 $options[$action['type']][$aid] = $action['description'];
580 }
581 }
582
583 $form[$op] = array(
584 '#type' => 'fieldset',
585 '#title' => t('Trigger: ') . $description,
586 '#theme' => 'actions_display'
587 );
588 // Retrieve actions that are already assigned to this hook-op combination.
589 $actions = _actions_get_hook_actions($hook, $op);
590 $form[$op]['assigned']['#type'] = 'value';
591 $form[$op]['assigned']['#value'] = array();
592 foreach ($actions as $aid => $description) {
593 $form[$op]['assigned']['#value'][$aid] = array(
594 'description' => $description,
595 'link' => l(t('unassign'), "admin/build/trigger/unassign/$hook/$op/". md5($aid))
596 );
597 }
598
599 $form[$op]['parent'] = array(
600 '#prefix' => "<div class='container-inline'>",
601 '#suffix' => '</div>',
602 );
603 // List possible actions that may be assigned.
604 if (count($options) != 0) {
605 array_unshift($options, t('Choose an action'));
606 $form[$op]['parent']['aid'] = array(
607 '#type' => 'select',
608 '#options' => $options,
609 );
610 $form[$op]['parent']['submit'] = array(
611 '#type' => 'submit',
612 '#value' => t('Assign')
613 );
614 }
615 else {
616 $form[$op]['none'] = array(
617 '#value' => t('No available actions for this trigger.')
618 );
619 }
620 return $form;
621 }
622
623 /**
624 * Validation function for actions_assign_form().
625 *
626 * Makes sure that the user is not re-assigning an action to an event.
627 */
628 function actions_assign_form_validate($form_id, $form_values) {
629 if (!empty($form_values['aid'])) {
630 $aid = actions_function_lookup($form_values['aid']);
631 if (db_result(db_query("SELECT aid FROM {actions_assignments} WHERE hook = '%s' AND op = '%s' AND aid = '%s'", $form_values['hook'], $form_values['operation'], $aid))) {
632 form_set_error($form_values['operation'], t('The action you chose is already assigned to that trigger.'));
633 }
634 }
635 }
636
637 /**
638 * Submit function for actions_assign_form().
639 */
640 function actions_assign_form_submit($form_id, $form_values) {
641 if (!empty($form_values['aid'])) {
642 $callback = actions_function_lookup($form_values['aid']);
643 $aid = db_result(db_query("SELECT aid FROM {actions} WHERE MD5(aid) = '%s'", $form_values['aid']));
644 $weight = db_result(db_query("SELECT MAX(weight) FROM {actions_assignments} WHERE hook = '%s' AND op = '%s'", $form_values['hook'], $form_values['operation']));
645 db_query("INSERT INTO {actions_assignments} values ('%s', '%s', '%s', %d)", $form_values['hook'], $form_values['operation'], $aid, $weight + 1);
646 // If this action changes a node property, we need to save the node
647 // so the change will persist.
648 $actions = actions_list();
649 if (isset($actions[$callback]['behavior']) && in_array('changes_node_property', $actions[$callback]['behavior']) && ($form_values['operation'] != 'presave')) {
650 // Delete previous node_save_action if it exists, and re-add a new one at a higher weight.
651 $save_post_action_assigned = db_result(db_query("SELECT aid FROM {actions_assignments} WHERE hook = '%s' AND op = '%s' AND aid = 'node_save_action'", $form_values['hook'], $form_values['operation']));
652 if ($save_post_action_assigned) {
653 db_query("DELETE FROM {actions_assignments} WHERE hook = '%s' AND op = '%s' AND aid = 'node_save_action'", $form_values['hook'], $form_values['operation']);
654 }
655 db_query("INSERT INTO {actions_assignments} VALUES ('%s', '%s', '%s', %d)", $form_values['hook'], $form_values['operation'], 'node_save_action', $weight + 2);
656 if (!$save_post_action_assigned) {
657 drupal_set_message(t('You have added an action that changes a the property of a post. A Save post action has been added so that the property change will be saved.'));
658 }
659 }
660 }
661 }
662
663 /**
664 * Display actions assigned to this hook-op combination in a table.
665 *
666 * @param array $element
667 * The fieldset including all assigned actions.
668 * @return
669 * The rendered form with the table prepended.
670 *
671 * @ingroup themeable
672 */
673 function theme_actions_display($element) {
674 $header = array();
675 $rows = array();
676 if (count($element['assigned']['#value'])) {
677 $header = array(array('data' => t('Name')), array('data' => t('Operation')));
678 $rows = array();
679 foreach ($element['assigned']['#value'] as $aid => $info) {
680 $rows[] = array(
681 $info['description'],
682 $info['link']
683 );
684 }
685 }
686
687 if (count($rows)) {
688 $output = theme('table', $header, $rows) . drupal_render($element);
689 }
690 else {
691 $output = drupal_render($element);
692 }
693 return $output;
694 }
695
696
697 /**
698 * Get the actions that have already been defined for this
699 * type-hook-op combination.
700 *
701 * @param $type
702 * One of 'node', 'user', 'comment'.
703 * @param $hook
704 * The name of the hook for which actions have been assigned,
705 * e.g. 'nodeapi'.
706 * @param $op
707 * The hook operation for which the actions have been assigned,
708 * e.g., 'view'.
709 * @return
710 * An array of action descriptions keyed by action IDs.
711 */
712 function _actions_get_hook_actions($hook, $op, $type = NULL) {
713 $actions = array();
714 if ($type) {
715 $result = db_query("SELECT h.aid, a.description FROM {actions_assignments} h LEFT JOIN {actions} a on a.aid = h.aid WHERE a.type = '%s' AND h.hook = '%s' AND h.op = '%s' ORDER BY h.weight", $type, $hook, $op);
716 }
717 else {
718 $result = db_query("SELECT h.aid, a.description FROM {actions_assignments} h LEFT JOIN {actions} a on a.aid = h.aid WHERE h.hook = '%s' AND h.op = '%s' ORDER BY h.weight", $hook, $op);
719 }
720 while ($action = db_fetch_object($result)) {
721 $actions[$action->aid] = $action->description;
722 }
723 return $actions;
724 }
725
726 /**
727 * Get the aids of actions to be executed for a hook-op combination.
728 *
729 * @param $hook
730 * The name of the hook being fired.
731 * @param $op
732 * The name of the operation being executed. Defaults to an empty string
733 * because some hooks (e.g., hook_cron()) do not have operations.
734 * @return
735 * An array of action IDs.
736 */
737 function _actions_get_hook_aids($hook, $op = '') {
738 $aids = array();
739 $result = db_query("SELECT aa.aid, a.type FROM {actions_assignments} aa LEFT JOIN {actions} a ON aa.aid = a.aid WHERE aa.hook = '%s' AND aa.op = '%s' ORDER BY weight", $hook, $op);
740 while ($action = db_fetch_object($result)) {
741 $aids[$action->aid]['type'] = $action->type;
742 }
743 return $aids;
744 }
745
746 /**
747 * Implementation of hook_theme().
748 */
749 function actions_theme() {
750 return array(
751 'actions_display' => array(
752 'arguments' => array('element'),
753 ),
754 );
755 }
756
757 /**
758 * Implementation of hook_forms(). We reuse code by using the
759 * same assignment form definition for each node-op combination.
760 */
761 function actions_forms() {
762 $hooks = module_invoke_all('hook_info');
763 foreach ($hooks as $module => $info) {
764 foreach ($hooks[$module] as $hook => $ops) {
765 foreach ($ops as $op => $description) {
766 $forms['actions_'. $hook .'_'. $op .'_assign_form'] = array('callback' => 'actions_assign_form');
767 }
768 }
769 }
770
771 return $forms;
772 }
773
774 /**
775 * When an action is called in a context that does not match its type,
776 * the object that the action expects must be retrieved. For example, when
777 * an action that works on users is called during the node hook, the
778 * user object is not available since the node hook doesn't pass it.
779 * So here we load the object the action expects.
780 *
781 * @param $type
782 * The type of action that is about to be called.
783 * @param $node
784 * The node that was passed via the nodeapi hook.
785 * @return
786 * The object expected by the action that is about to be called.
787 */
788 function _actions_normalize_node_context($type, $node) {
789 switch ($type) {
790 // If an action that works on comments is being called in a node context,
791 // the action is expecting a comment object. But we do not know which comment
792 // to give it. The first? The most recent? All of them? So comment actions
793 // in a node context are not supported.
794
795 // An action that works on users is being called in a node context.
796 // Load the user object of the node's author.
797 case 'user':
798 return user_load(array('uid' => $node->uid));
799 }
800 }
801
802 /**
803 * Implementation of hook_nodeapi().
804 */
805 function actions_nodeapi(&$node, $op, $a3, $a4) {
806 // Keep objects for reuse so that changes actions make to objects can persist.
807 static $objects;
808 // Prevent recursion by tracking which operations have already been called.
809 static $recursion;
810 // Support a subset of operations.
811 if (!in_array($op, array('view', 'update', 'presave', 'insert', 'delete')) || isset($recursion[$op])) {
812 return;
813 }
814 $recursion[$op] = TRUE;
815
816 $aids = _actions_get_hook_aids('nodeapi', $op);
817 if (!$aids) {
818 return;
819 }
820 $context = array(
821 'hook' => 'nodeapi',
822 'op' => $op,
823 );
824
825 // We need to get the expected object if the action's type is not 'node'.
826 // We keep the object in $objects so we can reuse it if we have multiple actions
827 // that make changes to an object.
828 foreach ($aids as $aid => $action_info) {
829 if ($action_info['type'] != 'node') {
830 if (!isset($objects[$action_info['type']])) {
831 $objects[$action_info['type']] = _actions_normalize_node_context($action_info['type'], $node);
832 }
833 // Since we know about the node, we pass that info along to the action.
834 $context['node'] = $node;
835 $result = actions_do($aid, $objects[$action_info['type']], $context, $a4, $a4);
836 }
837 else {
838 actions_do($aid, $node, $context, $a3, $a4);
839 }
840 }
841 }
842
843 /**
844 * When an action is called in a context that does not match its type,
845 * the object that the action expects must be retrieved. For example, when
846 * an action that works on nodes is called during the comment hook, the
847 * node object is not available since the comment hook doesn't pass it.
848 * So here we load the object the action expects.
849 *
850 * @param $type
851 * The type of action that is about to be called.
852 * @param $comment
853 * The comment that was passed via the comment hook.
854 * @return
855 * The object expected by the action that is about to be called.
856 */
857 function _actions_normalize_comment_context($type, $comment) {
858 switch ($type) {
859 // An action that works with nodes is being called in a comment context.
860 case 'node':
861 return node_load(is_array($comment) ? $comment['nid'] : $comment->nid);
862
863 // An action that works on users is being called in a comment context.
864 case 'user':
865 return user_load(array('uid' => is_array($comment) ? $comment['uid'] : $comment->uid));
866 }
867 }
868
869 /**
870 * Implementation of hook_comment().
871 */
872 function actions_comment($a1, $op) {
873 // Keep objects for reuse so that changes actions make to objects can persist.
874 static $objects;
875 // We support a subset of operations.
876 if (!in_array($op, array('insert', 'update', 'delete', 'view'))) {
877 return;
878 }
879 $aids = _actions_get_hook_aids('comment', $op);
880 $context = array(
881 'hook' => 'comment',
882 'op' => $op,
883 );
884 // We need to get the expected object if the action's type is not 'comment'.
885 // We keep the object in $objects so we can reuse it if we have multiple actions
886 // that make changes to an object.
887 foreach ($aids as $aid => $action_info) {
888 if ($action_info['type'] != 'comment') {
889 if (!isset($objects[$action_info['type']])) {
890 $objects[$action_info['type']] = _actions_normalize_comment_context($action_info['type'], $a1);
891 }
892 // Since we know about the comment, we pass it along to the action
893 // in case it wants to peek at it.
894 $context['comment'] = (object) $a1;
895 actions_do($aid, $objects[$action_info['type']], $context);
896 }
897 else {
898 $comment = (object) $a1;
899 actions_do($aid, $comment, $context);
900 }
901 }
902 }
903
904 /**
905 * Implementation of hook_cron().
906 */
907 function actions_cron() {
908 $aids = _actions_get_hook_aids('cron');
909 $context = array(
910 'hook' => 'cron',
911 'op' => '',
912 );
913 // Cron does not act on any specific object.
914 $object = NULL;
915 actions_do(array_keys($aids), $object, $context);
916 }
917
918 /**
919 * When an action is called in a context that does not match its type,
920 * the object that the action expects must be retrieved. For example, when
921 * an action that works on nodes is called during the user hook, the
922 * node object is not available since the user hook doesn't pass it.
923 * So here we load the object the action expects.
924 *
925 * @param $type
926 * The type of action that is about to be called.
927 * @param $account
928 * The account object that was passed via the user hook.
929 * @return
930 * The object expected by the action that is about to be called.
931 */
932 function _actions_normalize_user_context($type, $account) {
933 switch ($type) {
934 // If an action that works on comments is being called in a user context,
935 // the action is expecting a comment object. But we have no way of
936 // determining the appropriate comment object to pass. So comment
937 // actions in a user context are not supported.
938
939 // An action that works with nodes is being called in a user context.
940 // If a single node is being viewed, return the node.
941 case 'node':
942 // If we are viewing an individual node, return the node.
943 if ((arg(0) == 'node') && is_numeric(arg(1)) && (arg(2) == NULL)) {
944 return node_load(array('nid' => arg(1)));
945 }
946 }
947 }
948
949 /**
950 * Implementation of hook_user().
951 */
952 function actions_user($op, &$edit, &$account, $category = NULL) {
953 // Keep objects for reuse so that changes actions make to objects can persist.
954 static $objects;
955 // We support a subset of operations.
956 if (!in_array($op, array('login', 'logout', 'insert', 'update', 'delete', 'view'))) {
957 return;
958 }
959 $aids = _actions_get_hook_aids('user', $op);
960 $context = array(
961 'hook' => 'user',
962 'op' => $op,
963 'form_values' => &$edit,
964 );
965 foreach ($aids as $aid => $action_info) {
966 if ($action_info['type'] != 'user') {
967 if (!isset($objects[$action_info['type']])) {
968 $objects[$action_info['type']] = _actions_normalize_user_context($action_info['type'], $account);
969 }
970 $context['account'] = $account;
971 actions_do($aid, $objects[$action_info['type']], $context);
972 }
973 else {
974 actions_do($aid, $account, $context, $category);
975 }
976 }
977 }
978
979 /**
980 * Implementation of hook_taxonomy().
981 */
982 function actions_taxonomy($op, $type, $array) {
983 if ($type != 'term') {
984 return;
985 }
986 $aids = _actions_get_hook_aids('taxonomy', $op);
987 $context = array(
988 'hook' => 'taxonomy',
989 'op' => $op
990 );
991 foreach ($aids as $aid => $action_info) {
992 $taxonomy_object = (object) $array;
993 actions_do($aid, $taxonomy_object, $context);
994 }
995 }
996
997 /**
998 * Often we generate a select field of all actions. This function
999 * generates the options for that select.
1000 *
1001 * @param $type
1002 * One of 'node', 'user', 'comment'.
1003 * @return
1004 * Array keyed by action ID.
1005 */
1006 function actions_options($type = 'all') {
1007 $options = array(t('Choose an action'));
1008 foreach (actions_actions_map(actions_get_all_actions()) as $aid => $action) {
1009 $options[$action['type']][$aid] = $action['description'];
1010 }
1011
1012 if ($type == 'all') {
1013 return $options;
1014 }
1015 else {
1016 return $options[$type];
1017 }
1018 }
1019
1020 /**
1021 * Implementation of hook_actions_delete().
1022 *
1023 * Remove all trigger entries for the given action, when deleted.
1024 */
1025 function actions_actions_delete($aid) {
1026 db_query("DELETE FROM {actions_assignments} WHERE aid = '%s'", $aid);
1027 }
1028
1029 /**
1030 * Implementation of hook_hook_info().
1031 */
1032 function system_hook_info() {
1033 return array(
1034 'system' => array(
1035 'cron' => array(
1036 'run' => array(
1037 'runs when' => t('When cron runs'),
1038 ),
1039 ),
1040 ),
1041 );
1042 }
1043
1044 /**
1045 * Implementation of hook_action_info().
1046 */
1047 function system_action_info() {
1048 return array(
1049 'system_message_action' => array(
1050 'type' => 'system',
1051 'description' => t('Display a message to the user'),
1052 'configurable' => TRUE,
1053 'hooks' => array(
1054 'nodeapi' => array('view', 'insert', 'update', 'delete'),
1055 'comment' => array('view', 'insert', 'update', 'delete'),
1056 'user' => array('view', 'insert', 'update', 'delete', 'login'),
1057 'taxonomy' => array('insert', 'update', 'delete'),
1058 ),
1059 ),
1060 'system_send_email_action' => array(
1061 'description' => t('Send e-mail'),
1062 'type' => 'system',
1063 'configurable' => TRUE,
1064 'hooks' => array(
1065 'nodeapi' => array('view', 'insert', 'update', 'delete'),
1066 'comment' => array('view', 'insert', 'update', 'delete'),
1067 'user' => array('view', 'insert', 'update', 'delete', 'login'),
1068 'taxonomy' => array('insert', 'update', 'delete'),
1069 )
1070 ),
1071 'system_goto_action' => array(
1072 'description' => t('Redirect to URL'),
1073 'type' => 'system',
1074 'configurable' => TRUE,
1075 'hooks' => array(
1076 'nodeapi' => array('view', 'insert', 'update', 'delete'),
1077 'comment' => array('view', 'insert', 'update', 'delete'),
1078 'user' => array('view', 'insert', 'update', 'delete', 'login'),
1079 )
1080 )
1081 );
1082 }
1083
1084 /**
1085 * Return a form definition so the Send email action can be configured.
1086 *
1087 * @see system_send_email_action_validate()
1088 * @see system_send_email_action_submit()
1089 * @param $context
1090 * Default values (if we are editing an existing action instance).
1091 * @return
1092 * Form definition.
1093 */
1094 function system_send_email_action_form($context) {
1095 // Set default values for form.
1096 if (!isset($context['recipient'])) {
1097 $context['recipient'] = '';
1098 }
1099 if (!isset($context['subject'])) {
1100 $context['subject'] = '';
1101 }
1102 if (!isset($context['message'])) {
1103 $context['message'] = '';
1104 }
1105
1106 $form['recipient'] = array(
1107 '#type' => 'textfield',
1108 '#title' => t('Recipient'),
1109 '#default_value' => $context['recipient'],
1110 '#maxlength' => '254',
1111 '#description' => t('The email address to which the message should be sent OR enter %author if you would like to send an e-mail to the author of the original post.', array('%author' => '%author')),
1112 );
1113 $form['subject'] = array(
1114 '#type' => 'textfield',
1115 '#title' => t('Subject'),
1116 '#default_value' => $context['subject'],
1117 '#maxlength' => '254',
1118 '#description' => t('The subject of the message.'),
1119 );
1120 $form['message'] = array(
1121 '#type' => 'textarea',
1122 '#title' => t('Message'),
1123 '#default_value' => $context['message'],
1124 '#cols' => '80',
1125 '#rows' => '20',
1126 '#description' => t('The message that should be sent. You may include the following variables: %site_name, %username, %node_url, %node_type, %title, %teaser, %body. Not all variables will be available in all contexts.'),
1127 );
1128 return $form;
1129 }
1130
1131 /**
1132 * Validate system_send_email_action form submissions.
1133 */
1134 function system_send_email_action_validate($form_id, $form_values) {
1135 // Validate the configuration form.
1136 if (!valid_email_address($form_values['recipient']) && $form_values['recipient'] != '%author') {
1137 // We want the literal %author placeholder to be emphasized in the error message.
1138 form_set_error('recipient', t('Please enter a valid email address or %author.', array('%author' => '%author')));
1139 }
1140 }
1141
1142 /**
1143 * Process system_send_email_action form submissions.
1144 */
1145 function system_send_email_action_submit($form_id, $form_values) {
1146 // Process the HTML form to store configuration. The keyed array that
1147 // we return will be serialized to the database.
1148 $params = array(
1149 'recipient' => $form_values['recipient'],
1150 'subject' => $form_values['subject'],
1151 'message' => $form_values['message'],
1152 );
1153 return $params;
1154 }
1155
1156 /**
1157 * Implementation of a configurable Drupal action. Sends an email.
1158 */
1159 function system_send_email_action($object, $context) {
1160 global $user;
1161
1162 switch ($context['hook']) {
1163 case 'nodeapi':
1164 // Because this is not an action of type 'node' the node
1165 // will not be passed as $object, but it will still be available
1166 // in $context.
1167 $node = $context['node'];
1168 break;
1169 // The comment hook provides nid, in $context.
1170 case 'comment':
1171 $comment = $context['comment'];
1172 $node = node_load($comment->nid);
1173