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