Commit fd1c63b5 authored by Dries's avatar Dries

- Patch ##525540 by jvandyk, sun, jhodgdon, fago | webchick, TheRec, Dave...

- Patch ##525540 by jvandyk, sun, jhodgdon, fago | webchick, TheRec, Dave Reid, brianV, sun.core, cweagans, Dries: gave trigger.module and includes/actions.inc an API overhaul.  Simplified definitions of actions and triggers and removed dependency on the combination of hooks and operations. Triggers now directly map to module hooks.
parent a557b0de
......@@ -182,6 +182,10 @@ Drupal 7.0, xxxx-xx-xx (development version)
them complete access to the site.
* Access control affects both published and unpublished nodes.
* Numerous other improvements to the node access system.
- Actions system
* Simplified definitions of actions and triggers.
* Removed dependency on the combination of hooks and operations. Triggers
now directly map to module hooks.
Drupal 6.0, 2008-02-13
----------------------
......
This diff is collapsed.
......@@ -2295,50 +2295,22 @@ function vancode2int($c = '00') {
return base_convert(substr($c, 1), 36, 10);
}
/**
* Implement hook_hook_info().
*/
function comment_hook_info() {
return array(
'comment' => array(
'comment' => array(
'insert' => array(
'runs when' => t('After saving a new comment'),
),
'update' => array(
'runs when' => t('After saving an updated comment'),
),
'delete' => array(
'runs when' => t('After deleting a comment')
),
'view' => array(
'runs when' => t('When a comment is being viewed by an authenticated user')
),
),
),
);
}
/**
* Implement hook_action_info().
*/
function comment_action_info() {
return array(
'comment_unpublish_action' => array(
'description' => t('Unpublish comment'),
'label' => t('Unpublish comment'),
'type' => 'comment',
'configurable' => FALSE,
'hooks' => array(
'comment' => array('insert', 'update'),
)
'triggers' => array('comment_insert', 'comment_update'),
),
'comment_unpublish_by_keyword_action' => array(
'description' => t('Unpublish comment containing keyword(s)'),
'label' => t('Unpublish comment containing keyword(s)'),
'type' => 'comment',
'configurable' => TRUE,
'hooks' => array(
'comment' => array('insert', 'update'),
)
'triggers' => array('comment_insert', 'comment_update'),
)
);
}
......
......@@ -2753,33 +2753,6 @@ function node_forms() {
return $forms;
}
/**
* Implement hook_hook_info().
*/
function node_hook_info() {
return array(
'node' => array(
'node' => array(
'presave' => array(
'runs when' => t('When either saving a new post or updating an existing post'),
),
'insert' => array(
'runs when' => t('After saving a new post'),
),
'update' => array(
'runs when' => t('After saving an updated post'),
),
'delete' => array(
'runs when' => t('After deleting a post')
),
'view' => array(
'runs when' => t('When content is viewed by an authenticated user')
),
),
),
);
}
/**
* Implement hook_action_info().
*/
......@@ -2787,90 +2760,84 @@ function node_action_info() {
return array(
'node_publish_action' => array(
'type' => 'node',
'description' => t('Publish post'),
'label' => t('Publish content'),
'configurable' => FALSE,
'behavior' => array('changes_node_property'),
'hooks' => array(
'node' => array('presave'),
'comment' => array('insert', 'update'),
),
'triggers' => array('node_presave', 'comment_insert', 'comment_update'),
),
'node_unpublish_action' => array(
'type' => 'node',
'description' => t('Unpublish post'),
'label' => t('Unpublish content'),
'configurable' => FALSE,
'behavior' => array('changes_node_property'),
'hooks' => array(
'node' => array('presave'),
'comment' => array('delete', 'insert', 'update'),
'triggers' => array(
'node_presave',
'comment_insert',
'comment_update',
'comment_delete'
),
),
'node_make_sticky_action' => array(
'type' => 'node',
'description' => t('Make post sticky'),
'label' => t('Make content sticky'),
'configurable' => FALSE,
'behavior' => array('changes_node_property'),
'hooks' => array(
'node' => array('presave'),
'comment' => array('insert', 'update'),
),
'triggers' => array('node_presave', 'comment_insert', 'comment_update'),
),
'node_make_unsticky_action' => array(
'type' => 'node',
'description' => t('Make post unsticky'),
'label' => t('Make content unsticky'),
'configurable' => FALSE,
'behavior' => array('changes_node_property'),
'hooks' => array(
'node' => array('presave'),
'comment' => array('delete', 'insert', 'update'),
'triggers' => array(
'node_presave',
'comment_insert',
'comment_update',
'comment_delete'
),
),
'node_promote_action' => array(
'type' => 'node',
'description' => t('Promote post to front page'),
'label' => t('Promote content to front page'),
'configurable' => FALSE,
'behavior' => array('changes_node_property'),
'hooks' => array(
'node' => array('presave'),
'comment' => array('insert', 'update'),
),
'triggers' => array('node_presave', 'comment_insert', 'comment_update'),
),
'node_unpromote_action' => array(
'type' => 'node',
'description' => t('Remove post from front page'),
'label' => t('Remove content from front page'),
'configurable' => FALSE,
'behavior' => array('changes_node_property'),
'hooks' => array(
'node' => array('presave'),
'comment' => array('delete', 'insert', 'update'),
'triggers' => array(
'node_presave',
'comment_insert',
'comment_update',
'comment_delete'
),
),
'node_assign_owner_action' => array(
'type' => 'node',
'description' => t('Change the author of a post'),
'label' => t('Change the author of content'),
'configurable' => TRUE,
'behavior' => array('changes_node_property'),
'hooks' => array(
'any' => TRUE,
'node' => array('presave'),
'comment' => array('delete', 'insert', 'update'),
'triggers' => array(
'node_presave',
'comment_insert',
'comment_update',
'comment_delete',
),
),
'node_save_action' => array(
'type' => 'node',
'description' => t('Save post'),
'label' => t('Save content'),
'configurable' => FALSE,
'hooks' => array(
'comment' => array('delete', 'insert', 'update'),
),
'triggers' => array('comment_delete', 'comment_insert', 'comment_update'),
),
'node_unpublish_by_keyword_action' => array(
'type' => 'node',
'description' => t('Unpublish post containing keyword(s)'),
'label' => t('Unpublish content containing keyword(s)'),
'configurable' => TRUE,
'hooks' => array(
'node' => array('presave', 'insert', 'update'),
),
'triggers' => array('node_presave', 'node_insert', 'node_update'),
),
);
}
......
......@@ -26,27 +26,27 @@ class ActionsConfigurationTestCase extends DrupalWebTestCase {
// Make a POST request to the individual action configuration page.
$edit = array();
$action_description = $this->randomName();
$edit['actions_description'] = $action_description;
$action_label = $this->randomName();
$edit['actions_label'] = $action_label;
$edit['url'] = 'admin';
$this->drupalPost('admin/config/system/actions/configure/' . md5('system_goto_action'), $edit, t('Save'));
// Make sure that the new complex action was saved properly.
$this->assertText(t('The action has been successfully saved.'), t("Make sure we get a confirmation that we've successfully saved the complex action."));
$this->assertText($action_description, t("Make sure the action description appears on the configuration page after we've saved the complex action."));
$this->assertText($action_label, t("Make sure the action label appears on the configuration page after we've saved the complex action."));
// Make another POST request to the action edit page.
$this->clickLink(t('configure'));
$edit = array();
$new_action_description = $this->randomName();
$edit['actions_description'] = $new_action_description;
$new_action_label = $this->randomName();
$edit['actions_label'] = $new_action_label;
$edit['url'] = 'admin';
$this->drupalPost('admin/config/system/actions/configure/1', $edit, t('Save'));
// Make sure that the action updated properly.
$this->assertText(t('The action has been successfully saved.'), t("Make sure we get a confirmation that we've successfully updated the complex action."));
$this->assertNoText($action_description, t("Make sure the old action description does NOT appear on the configuration page after we've updated the complex action."));
$this->assertText($new_action_description, t("Make sure the action description appears on the configuration page after we've updated the complex action."));
$this->assertNoText($action_label, t("Make sure the old action label does NOT appear on the configuration page after we've updated the complex action."));
$this->assertText($new_action_label, t("Make sure the action label appears on the configuration page after we've updated the complex action."));
// Make sure that deletions work properly.
$this->clickLink(t('delete'));
......@@ -54,9 +54,9 @@ class ActionsConfigurationTestCase extends DrupalWebTestCase {
$this->drupalPost('admin/config/system/actions/delete/1', $edit, t('Delete'));
// Make sure that the action was actually deleted.
$this->assertRaw(t('Action %action was deleted', array('%action' => $new_action_description)), t('Make sure that we get a delete confirmation message.'));
$this->assertRaw(t('Action %action was deleted', array('%action' => $new_action_label)), t('Make sure that we get a delete confirmation message.'));
$this->drupalGet('admin/config/system/actions/manage');
$this->assertNoText($new_action_description, t("Make sure the action description does not appear on the overview page after we've deleted the action."));
$this->assertNoText($new_action_label, t("Make sure the action label does not appear on the overview page after we've deleted the action."));
$exists = db_query('SELECT aid FROM {actions} WHERE callback = :callback', array(':callback' => 'drupal_goto_action'))->fetchField();
$this->assertFalse($exists, t('Make sure the action is gone from the database after being deleted.'));
}
......
......@@ -2,15 +2,13 @@
// $Id$
/**
* Implement hook_hook_info().
* Implement hook_trigger_info().
*/
function actions_loop_test_hook_info() {
function actions_loop_test_trigger_info() {
return array(
'actions_loop_test' => array(
'watchdog' => array(
'run' => array(
'runs when' => t('When a message is logged'),
),
'label' => t('When a message is logged'),
),
),
);
......@@ -26,13 +24,11 @@ function actions_loop_test_watchdog(array $log_entry) {
}
// Get all the action ids assigned to the trigger on the watchdog hook's
// "run" event.
$aids = _trigger_get_hook_aids('watchdog', 'run');
$aids = trigger_get_assigned_actions('watchdog');
// We can pass in any applicable information in $context. There isn't much in
// this case, but we'll pass in the hook name and the operation name as the
// bare minimum.
// this case, but we'll pass in the hook name as the bare minimum.
$context = array(
'hook' => 'watchdog',
'op' => 'run',
);
// Fire the actions on the associated object ($log_entry) and the context
// variable.
......@@ -54,12 +50,10 @@ function actions_loop_test_init() {
function actions_loop_test_action_info() {
return array(
'actions_loop_test_log' => array(
'description' => t('Write a message to the log.'),
'label' => t('Write a message to the log.'),
'type' => 'system',
'configurable' => FALSE,
'hooks' => array(
'any' => TRUE,
)
'triggers' => array('any'),
),
);
}
......
......@@ -2296,3 +2296,276 @@ function theme_system_themes_form($form) {
$output .= drupal_render_children($form);
return $output;
}
/**
* Menu callback; Displays an overview of available and configured actions.
*/
function system_actions_manage() {
actions_synchronize();
$actions = actions_list();
$actions_map = actions_actions_map($actions);
$options = array(t('Choose an advanced action'));
$unconfigurable = array();
foreach ($actions_map as $key => $array) {
if ($array['configurable']) {
$options[$key] = $array['label'] . '...';
}
else {
$unconfigurable[] = $array;
}
}
$row = array();
$instances_present = db_query("SELECT aid FROM {actions} WHERE parameters <> ''")->fetchField();
$header = array(
array('data' => t('Action type'), 'field' => 'type'),
array('data' => t('Label'), 'field' => 'label'),
array('data' => $instances_present ? t('Operations') : '', 'colspan' => '2')
);
$query = db_select('actions')->extend('PagerDefault')->extend('TableSort');
$result = $query
->fields('actions')
->limit(50)
->orderByHeader($header)
->execute();
foreach ($result as $action) {
$row[] = array(
array('data' => $action->type),
array('data' => $action->label),
array('data' => $action->parameters ? l(t('configure'), "admin/config/system/actions/configure/$action->aid") : ''),
array('data' => $action->parameters ? l(t('delete'), "admin/config/system/actions/delete/$action->aid") : '')
);
}
if ($row) {
$pager = theme('pager', NULL);
if (!empty($pager)) {
$row[] = array(array('data' => $pager, 'colspan' => '3'));
}
$build['system_actions_header'] = array('#markup' => '<h3>' . t('Actions available to Drupal:') . '</h3>');
$build['system_actions_table'] = array('#markup' => theme('table', $header, $row));
}
if ($actions_map) {
$build['system_actions_manage_form'] = drupal_get_form('system_actions_manage_form', $options);
}
return $build;
}
/**
* Define the form for the actions overview page.
*
* @param $form_state
* An associative array containing the current state of the form; not used.
* @param $options
* An array of configurable actions.
* @return
* Form definition.
*
* @ingroup forms
* @see system_actions_manage_form_submit()
*/
function system_actions_manage_form($form, &$form_state, $options = array()) {
$form['parent'] = array(
'#type' => 'fieldset',
'#title' => t('Make a new advanced action available'),
'#prefix' => '<div class="container-inline">',
'#suffix' => '</div>',
);
$form['parent']['action'] = array(
'#type' => 'select',
'#default_value' => '',
'#options' => $options,
'#description' => '',
);
$form['parent']['buttons']['submit'] = array(
'#type' => 'submit',
'#value' => t('Create'),
);
return $form;
}
/**
* Process system_actions_manage form submissions.
*
* @see system_actions_manage_form()
*/
function system_actions_manage_form_submit($form, &$form_state) {
if ($form_state['values']['action']) {
$form_state['redirect'] = 'admin/config/system/actions/configure/' . $form_state['values']['action'];
}
}
/**
* Menu callback; Creates the form for configuration of a single action.
*
* We provide the "Description" field. The rest of the form is provided by the
* action. We then provide the Save button. Because we are combining unknown
* form elements with the action configuration form, we use an 'actions_' prefix
* on our elements.
*
* @param $action
* md5 hash of an action ID or an integer. If it is an md5 hash, we are
* creating a new instance. If it is an integer, we are editing an existing
* instance.
* @return
* A form definition.
*
* @see system_actions_configure_validate()
* @see system_actions_configure_submit()
*/
function system_actions_configure($form, &$form_state, $action = NULL) {
if ($action === NULL) {
drupal_goto('admin/config/system/actions');
}
$actions_map = actions_actions_map(actions_list());
$edit = array();
// Numeric action denotes saved instance of a configurable action.
if (is_numeric($action)) {
$aid = $action;
// Load stored parameter values from database.
$data = db_query("SELECT * FROM {actions} WHERE aid = :aid", array(':aid' => $aid))->fetch();
$edit['actions_label'] = $data->label;
$edit['actions_type'] = $data->type;
$function = $data->callback;
$action = md5($data->callback);
$params = unserialize($data->parameters);
if ($params) {
foreach ($params as $name => $val) {
$edit[$name] = $val;
}
}
}
// Otherwise, we are creating a new action instance.
else {
$function = $actions_map[$action]['callback'];
$edit['actions_label'] = $actions_map[$action]['label'];
$edit['actions_type'] = $actions_map[$action]['type'];
}
$form['actions_label'] = array(
'#type' => 'textfield',
'#title' => t('Label'),
'#default_value' => $edit['actions_label'],
'#maxlength' => '255',
'#description' => t('A unique label for this advanced action. This label will be displayed in the interface of modules that integrate with actions, such as Trigger module.'),
'#weight' => -10
);
$action_form = $function . '_form';
$form = array_merge($form, $action_form($edit));
$form['actions_type'] = array(
'#type' => 'value',
'#value' => $edit['actions_type'],
);
$form['actions_action'] = array(
'#type' => 'hidden',
'#value' => $action,
);
// $aid is set when configuring an existing action instance.
if (isset($aid)) {
$form['actions_aid'] = array(
'#type' => 'hidden',
'#value' => $aid,
);
}
$form['actions_configured'] = array(
'#type' => 'hidden',
'#value' => '1',
);
$form['buttons']['submit'] = array(
'#type' => 'submit',
'#value' => t('Save'),
'#weight' => 13
);
return $form;
}
/**
* Validate system_actions_configure() form submissions.
*/
function system_actions_configure_validate($form, &$form_state) {
$function = actions_function_lookup($form_state['values']['actions_action']) . '_validate';
// Hand off validation to the action.
if (function_exists($function)) {
$function($form, $form_state);
}
}
/**
* Process system_actions_configure() form submissions.
*/
function system_actions_configure_submit($form, &$form_state) {
$function = actions_function_lookup($form_state['values']['actions_action']);
$submit_function = $function . '_submit';
// Action will return keyed array of values to store.
$params = $submit_function($form, $form_state);
$aid = isset($form_state['values']['actions_aid']) ? $form_state['values']['actions_aid'] : NULL;
actions_save($function, $form_state['values']['actions_type'], $params, $form_state['values']['actions_label'], $aid);
drupal_set_message(t('The action has been successfully saved.'));
$form_state['redirect'] = 'admin/config/system/actions/manage';
}
/**
* Create the form for confirmation of deleting an action.
*
* @see system_actions_delete_form_submit()
* @ingroup forms
*/
function system_actions_delete_form($form, &$form_state, $action) {
$form['aid'] = array(
'#type' => 'hidden',
'#value' => $action->aid,
);
return confirm_form($form,
t('Are you sure you want to delete the action %action?', array('%action' => $action->label)),
'admin/config/system/actions/manage',
t('This cannot be undone.'),
t('Delete'),
t('Cancel')
);
}
/**
* Process system_actions_delete form submissions.
*
* Post-deletion operations for action deletion.
*/
function system_actions_delete_form_submit($form, &$form_state) {
$aid = $form_state['values']['aid'];
$action = actions_load($aid);
actions_delete($aid);
$label = check_plain($action->label);
watchdog('user', 'Deleted action %aid (%action)', array('%aid' => $aid, '%action' => $label));
drupal_set_message(t('Action %action was deleted', array('%action' => $label)));
$form_state['redirect'] = 'admin/config/system/actions/manage';
}
/**
* Post-deletion operations for deleting action orphans.
*
* @param $orphaned
* An array of orphaned actions.
*/
function system_action_delete_orphans_post($orphaned) {
foreach ($orphaned as $callback) {
drupal_set_message(t("Deleted orphaned action (%action).", array('%action' => $callback)));
}
}
/**
* Remove actions that are in the database but not supported by any enabled module.
*/
function system_actions_remove_orphans() {
actions_synchronize(TRUE);
drupal_goto('admin/config/system/actions/manage');
}
......@@ -2210,6 +2210,90 @@ function hook_file_mimetype_mapping_alter(&$mapping) {
$mapping['extensions']['ogg'] = 189;
}
/**
* Declares information about actions.
*
* Any module can define actions, and then call actions_do() to make those
* actions happen in response to events. The trigger module provides a user
* interface for associating actions with module-defined triggers, and it makes
* sure the core triggers fire off actions when their events happen.
*
* An action consists of two or three parts:
* - an action definition (returned by this hook)
* - a function which performs the action (which by convention is named
* MODULE_description-of-function_action)
* - an optional form definition function that defines a configuration form
* (which has the name of the action function with '_form' appended to it.)
*
* The action function takes two to four arguments, which come from the input
* arguments to actions_do().
*
* @return
* An associative array of action descriptions. The keys of the array
* are the names of the action functions, and each corresponding value
* is an associative array with the following key-value pairs:
* - 'type': The type of object this action acts upon. Core actions have types
* 'node', 'user', 'comment', and 'system'.
* - 'label': The human-readable name of the action, which should be passed
* through the t() function for translation.
* - 'configurable': If FALSE, then the action doesn't require any extra
* configuration. If TRUE, then your module must define a form function with
* the same name as the action function with '_form' appended (e.g., the
* form for 'node_assign_owner_action' is 'node_assign_owner_action_form'.)
* This function takes $context as its only parameter, and is paired with
* the usual _submit function, and possibly a _validate function.
* - 'triggers': An array of the events (that is, hooks) that can trigger this
* action. For example: array('node_insert', 'user_update'). You can also
* declare support for any trigger by returning array('any') for this value.
* - 'behavior': (optional) machine-readable array of behaviors of this
* action, used to signal additional actions that may need to be triggered.
* Currently recognized behaviors by Trigger module:
* - 'changes_node_property': If an action with this behavior is assigned to
* a trigger other than 'node_presave', any node save actions also
* assigned to this trigger are moved later in the list. If a node save
* action is not present, one will be added.
*/
function hook_action_info() {
return array(
'comment_unpublish_action' => array(
'type' => 'comment',
'label' => t('Unpublish comment'),
'configurable' => FALSE,
'triggers' => array('comment_insert', 'comment_update'),
),
'comment_unpublish_by_keyword_action' => array(
'type' => 'comment',
'label' => t('Unpublish comment containing keyword(s)'),
'configurable' => TRUE,
'triggers' => array('comment_insert', 'comment_update'),
),
);
}
/**
* Executes code after an action is deleted.
*
* @param $aid
* The action ID.
*/
function hook_actions_delete($aid) {
db_delete('actions_assignments')
->condition('aid', $aid)
->execute();
}
/**
* Alters the actions declared by another module.
*
* Called by actions_list() to allow modules to alter the return values from
* implementations of hook_action_info().
*
* @see trigger_example_action_info_alter().
*/
function hook_action_info_alter(&$actions) {
$actions['node_unpublish_action']['label'] = t('Unpublish and remove from public view.');
}
/**
* @} End of "addtogroup hooks".
*/
......@@ -574,8 +574,8 @@ function system_schema() {
'not null' => TRUE,
'size' => 'big',
),
'description' => array(
'description' => 'Description of the action.',
'label' => array(
'description' => 'Label of the action.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
......@@ -2492,6 +2492,15 @@ function system_update_7037() {
return $ret;
}
/**
* Rename action description to label.