/[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.40 - (show annotations) (download) (as text)
Sun Aug 3 03:41:44 2008 UTC (15 months, 3 weeks ago) by jvandyk
Branch: MAIN
Changes since 1.39: +6 -6 lines
File MIME type: text/x-php
#286445 by deekayen: administer_actions becomes administer actions
#286542 by deekayen: code cleanup (thanks, coder.module!)
1 <?php
2 // $Id: actions.module,v 1.39 2008/05/05 15:08:38 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']) // The action supports any hook.
578 || (isset($metadata['hooks'][$hook]) // The action has declared which hooks it supports
579 && is_array($metadata['hooks'][$hook]) // by defining an array.
580 // Either the current op must be in the array or the token 'any' must be in the array.
581 && ((in_array($op, $metadata['hooks'][$hook])) || in_array('any', $metadata['hooks'][$hook])))) {
582 $functions[] = $func;
583 }
584 }
585 foreach (actions_actions_map(actions_get_all_actions()) as $aid => $action) {
586 if (in_array($action['callback'], $functions)) {
587 $options[$action['type']][$aid] = $action['description'];
588 }
589 }
590
591 $form[$op] = array(
592 '#type' => 'fieldset',
593 '#title' => t('Trigger: ') . $description,
594 '#theme' => 'actions_display'
595 );
596 // Retrieve actions that are already assigned to this hook-op combination.
597 $actions = _actions_get_hook_actions($hook, $op);
598 $form[$op]['assigned']['#type'] = 'value';
599 $form[$op]['assigned']['#value'] = array();
600 foreach ($actions as $aid => $description) {
601 $form[$op]['assigned']['#value'][$aid] = array(
602 'description' => $description,
603 'link' => l(t('unassign'), "admin/build/trigger/unassign/$hook/$op/". md5($aid))
604 );
605 }
606
607 $form[$op]['parent'] = array(
608 '#prefix' => "<div class='container-inline'>",
609 '#suffix' => '</div>',
610 );
611 // List possible actions that may be assigned.
612 if (count($options) != 0) {
613 array_unshift($options, t('Choose an action'));
614 $form[$op]['parent']['aid'] = array(
615 '#type' => 'select',
616 '#options' => $options,
617 );
618 $form[$op]['parent']['submit'] = array(
619 '#type' => 'submit',
620 '#value' => t('Assign')
621 );
622 }
623 else {
624 $form[$op]['none'] = array(
625 '#value' => t('No available actions for this trigger.')
626 );
627 }
628 return $form;
629 }
630
631 /**
632 * Validation function for actions_assign_form().
633 *
634 * Makes sure that the user is not re-assigning an action to an event.
635 */
636 function actions_assign_form_validate($form_id, $form_values) {
637 if (!empty($form_values['aid'])) {
638 $aid = actions_function_lookup($form_values['aid']);
639 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))) {
640 form_set_error($form_values['operation'], t('The action you chose is already assigned to that trigger.'));
641 }
642 }
643 }
644
645 /**
646 * Submit function for actions_assign_form().
647 */
648 function actions_assign_form_submit($form_id, $form_values) {
649 if (!empty($form_values['aid'])) {
650 $callback = actions_function_lookup($form_values['aid']);
651 $aid = db_result(db_query("SELECT aid FROM {actions} WHERE MD5(aid) = '%s'", $form_values['aid']));
652 $weight = db_result(db_query("SELECT MAX(weight) FROM {actions_assignments} WHERE hook = '%s' AND op = '%s'", $form_values['hook'], $form_values['operation']));
653 db_query("INSERT INTO {actions_assignments} values ('%s', '%s', '%s', %d)", $form_values['hook'], $form_values['operation'], $aid, $weight + 1);
654 // If this action changes a node property, we need to save the node
655 // so the change will persist.
656 $actions = actions_list();
657 if (isset($actions[$callback]['behavior']) && in_array('changes_node_property', $actions[$callback]['behavior']) && ($form_values['operation'] != 'presave')) {
658 // Delete previous node_save_action if it exists, and re-add a new one at a higher weight.
659 $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']));
660 if ($save_post_action_assigned) {
661 db_query("DELETE FROM {actions_assignments} WHERE hook = '%s' AND op = '%s' AND aid = 'node_save_action'", $form_values['hook'], $form_values['operation']);
662 }
663 db_query("INSERT INTO {actions_assignments} VALUES ('%s', '%s', '%s', %d)", $form_values['hook'], $form_values['operation'], 'node_save_action', $weight + 2);
664 if (!$save_post_action_assigned) {
665 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.'));
666 }
667 }
668 }
669 }
670
671 /**
672 * Display actions assigned to this hook-op combination in a table.
673 *
674 * @param array $element
675 * The fieldset including all assigned actions.
676 * @return
677 * The rendered form with the table prepended.
678 *
679 * @ingroup themeable
680 */
681 function theme_actions_display($element) {
682 $header = array();
683 $rows = array();
684 if (count($element['assigned']['#value'])) {
685 $header = array(array('data' => t('Name')), array('data' => t('Operation')));
686 $rows = array();
687 foreach ($element['assigned']['#value'] as $aid => $info) {
688 $rows[] = array(
689 $info['description'],
690 $info['link']
691 );
692 }
693 }
694
695 if (count($rows)) {
696 $output = theme('table', $header, $rows) . drupal_render($element);
697 }
698 else {
699 $output = drupal_render($element);
700 }
701 return $output;
702 }
703
704
705 /**
706 * Get the actions that have already been defined for this
707 * type-hook-op combination.
708 *
709 * @param $type
710 * One of 'node', 'user', 'comment'.
711 * @param $hook
712 * The name of the hook for which actions have been assigned,
713 * e.g. 'nodeapi'.
714 * @param $op
715 * The hook operation for which the actions have been assigned,
716 * e.g., 'view'.
717 * @return
718 * An array of action descriptions keyed by action IDs.
719 */
720 function _actions_get_hook_actions($hook, $op, $type = NULL) {
721 $actions = array();
722 if ($type) {
723 $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);
724 }
725 else {
726 $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);
727 }
728 while ($action = db_fetch_object($result)) {
729 $actions[$action->aid] = $action->description;
730 }
731 return $actions;
732 }
733
734 /**
735 * Get the aids of actions to be executed for a hook-op combination.
736 *
737 * @param $hook
738 * The name of the hook being fired.
739 * @param $op
740 * The name of the operation being executed. Defaults to an empty string
741 * because some hooks (e.g., hook_cron()) do not have operations.
742 * @return
743 * An array of action IDs.
744 */
745 function _actions_get_hook_aids($hook, $op = '') {
746 $aids = array();
747 $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);
748 while ($action = db_fetch_object($result)) {
749 $aids[$action->aid]['type'] = $action->type;
750 }
751 return $aids;
752 }
753
754 /**
755 * Implementation of hook_theme().
756 */
757 function actions_theme() {
758 return array(
759 'actions_display' => array(
760 'arguments' => array('element'),
761 ),
762 );
763 }
764
765 /**
766 * Implementation of hook_forms(). We reuse code by using the
767 * same assignment form definition for each node-op combination.
768 */
769 function actions_forms() {
770 $hooks = module_invoke_all('hook_info');
771 foreach ($hooks as $module => $info) {
772 foreach ($hooks[$module] as $hook => $ops) {
773 foreach ($ops as $op => $description) {
774 $forms['actions_'. $hook .'_'. $op .'_assign_form'] = array('callback' => 'actions_assign_form');
775 }
776 }
777 }
778
779 return $forms;
780 }
781
782 /**
783 * When an action is called in a context that does not match its type,
784 * the object that the action expects must be retrieved. For example, when
785 * an action that works on users is called during the node hook, the
786 * user object is not available since the node hook doesn't pass it.
787 * So here we load the object the action expects.
788 *
789 * @param $type
790 * The type of action that is about to be called.
791 * @param $node
792 * The node that was passed via the nodeapi hook.
793 * @return
794 * The object expected by the action that is about to be called.
795 */
796 function _actions_normalize_node_context($type, $node) {
797 switch ($type) {
798 // If an action that works on comments is being called in a node context,
799 // the action is expecting a comment object. But we do not know which comment
800 // to give it. The first? The most recent? All of them? So comment actions
801 // in a node context are not supported.
802
803 // An action that works on users is being called in a node context.
804 // Load the user object of the node's author.
805 case 'user':
806 return user_load(array('uid' => $node->uid));
807 }
808 }
809
810 /**
811 * Implementation of hook_nodeapi().
812 */
813 function actions_nodeapi(&$node, $op, $a3, $a4) {
814 // Keep objects for reuse so that changes actions make to objects can persist.
815 static $objects;
816 // Prevent recursion by tracking which operations have already been called.
817 static $recursion;
818 // Support a subset of operations.
819 if (!in_array($op, array('view', 'update', 'presave', 'insert', 'delete')) || isset($recursion[$op][$node->nid])) {
820 return;
821 }
822 $recursion[$op][$node->nid] = TRUE;
823
824 $aids = _actions_get_hook_aids('nodeapi', $op);
825 if (!$aids) {
826 return;
827 }
828 $context = array(
829 'hook' => 'nodeapi',
830 'op' => $op,
831 );
832
833 // We need to get the expected object if the action's type is not 'node'.
834 // We keep the object in $objects so we can reuse it if we have multiple actions
835 // that make changes to an object.
836 foreach ($aids as $aid => $action_info) {
837 if ($action_info['type'] != 'node') {
838 if (!isset($objects[$action_info['type']])) {
839 $objects[$action_info['type']] = _actions_normalize_node_context($action_info['type'], $node);
840 }
841 // Since we know about the node, we pass that info along to the action.
842 $context['node'] = $node;
843 $result = actions_do($aid, $objects[$action_info['type']], $context, $a4, $a4);
844 }
845 else {
846 actions_do($aid, $node, $context, $a3, $a4);
847 }
848 }
849 }
850
851 /**
852 * When an action is called in a context that does not match its type,
853 * the object that the action expects must be retrieved. For example, when
854 * an action that works on nodes is called during the comment hook, the
855 * node object is not available since the comment hook doesn't pass it.
856 * So here we load the object the action expects.
857 *
858 * @param $type
859 * The type of action that is about to be called.
860 * @param $comment
861 * The comment that was passed via the comment hook.
862 * @return
863 * The object expected by the action that is about to be called.
864 */
865 function _actions_normalize_comment_context($type, $comment) {
866 switch ($type) {
867 // An action that works with nodes is being called in a comment context.
868 case 'node':
869 return node_load(is_array($comment) ? $comment['nid'] : $comment->nid);
870
871 // An action that works on users is being called in a comment context.
872 case 'user':
873 return user_load(array('uid' => is_array($comment) ? $comment['uid'] : $comment->uid));
874 }
875 }
876
877 /**
878 * Implementation of hook_comment().
879 */
880 function actions_comment($a1, $op) {
881 // Keep objects for reuse so that changes actions make to objects can persist.
882 static $objects;
883 // We support a subset of operations.
884 if (!in_array($op, array('insert', 'update', 'delete', 'view'))) {
885 return;
886 }
887 $aids = _actions_get_hook_aids('comment', $op);
888 $context = array(
889 'hook' => 'comment',
890 'op' => $op,
891 );
892 // We need to get the expected object if the action's type is not 'comment'.
893 // We keep the object in $objects so we can reuse it if we have multiple actions
894 // that make changes to an object.
895 foreach ($aids as $aid => $action_info) {
896 if ($action_info['type'] != 'comment') {
897 if (!isset($objects[$action_info['type']])) {
898 $objects[$action_info['type']] = _actions_normalize_comment_context($action_info['type'], $a1);
899 }
900 // Since we know about the comment, we pass it along to the action
901 // in case it wants to peek at it.
902 $context['comment'] = (object) $a1;
903 actions_do($aid, $objects[$action_info['type']], $context);
904 }
905 else {
906 $comment = (object) $a1;
907 actions_do($aid, $comment, $context);
908 }
909 }
910 }
911
912 /**
913 * Implementation of hook_cron().
914 */
915 function actions_cron() {
916 $aids = _actions_get_hook_aids('cron');
917 $context = array(
918 'hook' => 'cron',
919 'op' => '',
920 );
921 // Cron does not act on any specific object.
922 $object = NULL;
923 actions_do(array_keys($aids), $object, $context);
924 }
925
926 /**
927 * When an action is called in a context that does not match its type,
928 * the object that the action expects must be retrieved. For example, when
929 * an action that works on nodes is called during the user hook, the
930 * node object is not available since the user hook doesn't pass it.
931 * So here we load the object the action expects.
932 *
933 * @param $type
934 * The type of action that is about to be called.
935 * @param $account
936 * The account object that was passed via the user hook.
937 * @return
938 * The object expected by the action that is about to be called.
939 */
940 function _actions_normalize_user_context($type, $account) {
941 switch ($type) {
942 // If an action that works on comments is being called in a user context,
943 // the action is expecting a comment object. But we have no way of
944 // determining the appropriate comment object to pass. So comment
945 // actions in a user context are not supported.
946
947 // An action that works with nodes is being called in a user context.
948 // If a single node is being viewed, return the node.
949 case 'node':
950 // If we are viewing an individual node, return the node.
951 if ((arg(0) == 'node') && is_numeric(arg(1)) && (arg(2) == NULL)) {
952 return node_load(array('nid' => arg(1)));
953 }
954 }
955 }
956
957 /**
958 * Implementation of hook_user().
959 */
960 function actions_user($op, &$edit, &$account, $category = NULL) {
961 // Keep objects for reuse so that changes actions make to objects can persist.
962 static $objects;
963 // We support a subset of operations.
964 if (!in_array($op, array('login', 'logout', 'insert', 'update', 'delete', 'view'))) {
965 return;
966 }
967 $aids = _actions_get_hook_aids('user', $op);
968 $context = array(
969 'hook' => 'user',
970 'op' => $op,
971 'form_values' => &$edit,
972 );
973 foreach ($aids as $aid => $action_info) {
974 if ($action_info['type'] != 'user') {
975 if (!isset($objects[$action_info['type']])) {
976 $objects[$action_info['type']] = _actions_normalize_user_context($action_info['type'], $account);
977 }
978 $context['account'] = $account;
979 actions_do($aid, $objects[$action_info['type']], $context);
980 }
981 else {
982 actions_do($aid, $account, $context, $category);
983 }
984 }
985 }
986
987 /**
988 * Implementation of hook_taxonomy().
989 */
990 function actions_taxonomy($op, $type, $array) {
991 if ($type != 'term') {
992 return;
993 }
994 $aids = _actions_get_hook_aids('taxonomy', $op);
995 $context = array(
996 'hook' => 'taxonomy',
997 'op' => $op
998 );
999 foreach ($aids as $aid => $action_info) {
1000 $taxonomy_object = (object) $array;
1001 actions_do($aid, $taxonomy_object, $context);
1002 }
1003 }
1004
1005 /**
1006 * Often we generate a select field of all actions. This function
1007 * generates the options for that select.
1008 *
1009 * @param $type
1010 * One of 'node', 'user', 'comment'.
1011 * @return
1012 * Array keyed by action ID.
1013 */
1014 function actions_options($type = 'all') {
1015 $options = array(t('Choose an action'));
1016 foreach (actions_actions_map(actions_get_all_actions()) as $aid => $action) {
1017 $options[$action['type']][$aid] = $action['description'];
1018 }
1019
1020 if ($type == 'all') {
1021 return $options;
1022 }
1023 else {
1024 return $options[$type];
1025 }
1026 }
1027
1028 /**
1029 * Implementation of hook_actions_delete().
1030 *
1031 * Remove all trigger entries for the given action, when deleted.
1032 */
1033 function actions_actions_delete($aid) {
1034 db_query("DELETE FROM {actions_assignments} WHERE aid = '%s'", $aid);
1035 }
1036
1037 /**
1038 * Implementation of hook_hook_info().
1039 */
1040 function system_hook_info() {
1041 return array(
1042 'system' => array(
1043 'cron' => array(
1044 'run' => array(
1045 'runs when' => t('When cron runs'),
1046 ),
1047 ),
1048 ),
1049 );
1050 }
1051
1052 /**
1053 * Return a form definition so the Send email action can be configured.
1054 *
1055 * @see system_send_email_action_validate()
1056 * @see system_send_email_action_submit()
1057 * @param $context
1058 * Default values (if we are editing an existing action instance).
1059 * @return
1060 * Form definition.
1061 */
1062 function system_send_email_action_form($context) {
1063 // Set default values for form.
1064 if (!isset($context['recipient'])) {
1065 $context['recipient'] = '';
1066 }
1067 if (!isset($context['subject'])) {
1068 $context['subject'] = '';
1069 }
1070 if (!isset($context['message'])) {
1071 $context['message'] = '';
1072 }
1073
1074 $form['recipient'] = array(
1075 '#type' => 'textfield',
1076 '#title' => t('Recipient'),
1077 '#default_value' => $context['recipient'],
1078 '#maxlength' => '254',
1079 '#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')),
1080 );
1081 $form['subject'] = array(
1082 '#type' => 'textfield',
1083 '#title' => t('Subject'),
1084 '#default_value' => $context['subject'],
1085 '#maxlength' => '254',
1086 '#description' => t('The subject of the message.'),
1087 );
1088 $form['message'] = array(
1089 '#type' => 'textarea',
1090 '#title' => t('Message'),
1091 '#default_value' => $context['message'],
1092 '#cols' => '80',
1093 '#rows' => '20',
1094 '#description' => t('The message that should be sent. You may include the following variables: %site_name, %username, %uid, %node_url, %node_alias, %node_type, %title, %teaser, %body. Not all variables will be available in all contexts.'),
1095 );
1096 return $form;
1097 }
1098
1099 /**
1100 * Validate system_send_email_action form submissions.
1101 */
1102 function system_send_email_action_validate($form_id, $form_values) {
1103 // Validate the configuration form.
1104 if (!valid_email_address($form_values['recipient']) && $form_values['recipient'] != '%author') {
1105 // We want the literal %author placeholder to be emphasized in the error message.
1106 form_set_error('recipient', t('Please enter a valid email address or %author.', array('%author' => '%author')));
1107 }
1108 }
1109
1110 /**
1111 * Process system_send_email_action form submissions.
1112 */
1113 function system_send_email_action_submit($form_id, $form_values) {
1114 // Process the HTML form to store configuration. The keyed array that
1115 // we return will be serialized to the database.
1116 $params = array(
1117 'recipient' => $form_values['recipient'],
1118 'subject' => $form_values['subject'],
1119 'message' => $form_values['message'],
1120 );
1121 return $params;
1122 }
1123
1124 /**
1125 * Implementation of a configurable Drupal action. Sends an email.
1126 */
1127 function system_send_email_action($object, $context) {
1128 global $user;
1129
1130 switch ($context['hook']) {
1131 case 'nodeapi':
1132 // Because this is not an action of type 'node' the node
1133 // will not be passed as $object, but it will still be available
1134 // in $context.
1135 $node = $context['node'];
1136 break;
1137 case 'comment':
1138 // The comment hook provides nid, in $context.
1139 $comment = $context['comment'];
1140 $node = node_load($comment->nid);
1141 break;
1142 case 'user':
1143 // Because this is not an action of type 'user' the user
1144 // object is not passed as $object, but it will still be available
1145 // in $context.
1146 $account = $context['account'];
1147 if (isset($context['node'])) {
1148 $node = $context['node'];
1149 }
1150 elseif ($context['recipient'] == '%author') {
1151 // If we don't have a node, we don't have a node author.
1152 watchdog('error', t("Cannot use '%author' token in this context."));
1153 return;
1154 }
1155 break;
1156 default:
1157 // Check context for node.
1158 if (!isset($object) && isset($context['node'])) {
1159 $node = $context['node'];
1160 }
1161 else {
1162 // We are being called directly.
1163 $node = $object;
1164 }
1165 }
1166
1167 $recipient = $context['recipient'];
1168
1169 if (isset($node)) {
1170 if (!isset($account)) {
1171 $account = user_load(array('uid' => $node->uid));
1172 }
1173 if ($recipient == '%author') {
1174 $recipient = $account->mail;
1175 }
1176 }
1177
1178 if (!isset($account)) {
1179 $account = $user;
1180 }
1181
1182 $subject = $context['subject'];
1183 $message = $context['message'];