summaryrefslogtreecommitdiff
path: root/includes/actions.inc
diff options
context:
space:
mode:
authorDries Buytaert <dries@buytaert.net>2008-06-29 12:07:15 +0000
committerDries Buytaert <dries@buytaert.net>2008-06-29 12:07:15 +0000
commitaa433a093bda05f4b3a03854d7db6ca0db7595bf (patch)
treedd95178c7db7bed817d239a52e132ba17f661806 /includes/actions.inc
parent1d97b23ae4323537ae8a3fc1a5aca98739410e3b (diff)
downloadbrdo-aa433a093bda05f4b3a03854d7db6ca0db7595bf.tar.gz
brdo-aa433a093bda05f4b3a03854d7db6ca0db7595bf.tar.bz2
- Restored actions.inc
Diffstat (limited to 'includes/actions.inc')
-rw-r--r--includes/actions.inc414
1 files changed, 356 insertions, 58 deletions
diff --git a/includes/actions.inc b/includes/actions.inc
index fc829a8e1..27f7bdf27 100644
--- a/includes/actions.inc
+++ b/includes/actions.inc
@@ -1,65 +1,363 @@
<?php
+// $Id$
-class ActionsConfigurationTestCase extends DrupalWebTestCase {
- /**
- * Implementation of getInfo().
- */
- function getInfo() {
- return array(
- 'name' => t('Actions configuration'),
- 'description' => t('Tests complex actions configuration by adding, editing, and deleting a complex action.'),
- 'group' => t('System'),
+/**
+ * @file
+ * This is the actions engine for executing stored actions.
+ */
+
+/**
+ * Perform a given list of actions by executing their callback functions.
+ *
+ * Given the IDs of actions to perform, find out what the callbacks
+ * for the actions are by querying the database. Then call each callback
+ * using the function call $function($object, $context, $a1, $2)
+ * where $function is the name of a function written in compliance with
+ * the action specification; that is, foo($object, $context).
+ *
+ * @param $action_ids
+ * The ID of the action to perform. Can be a single action ID or an array
+ * of IDs. IDs of instances will be numeric; IDs of singletons will be
+ * function names.
+ * @param $object
+ * Parameter that will be passed along to the callback. Typically the
+ * object that the action will act on; a node, user or comment object.
+ * @param $context
+ * Parameter that will be passed along to the callback. $context is a
+ * keyed array containing extra information about what is currently
+ * happening at the time of the call. Typically $context['hook'] and
+ * $context['op'] will tell which hook-op combination resulted in this
+ * call to actions_do().
+ * @param $a1
+ * Parameter that will be passed along to the callback.
+ * @param $a2
+ * Parameter that will be passed along to the callback.
+ *
+ * @return
+ * An associative array containing the result of the function that
+ * performs the action, keyed on action ID.
+ */
+function actions_do($action_ids, $object = NULL, $context = NULL, $a1 = NULL, $a2 = NULL) {
+ static $stack;
+ $stack++;
+ if ($stack > variable_get('actions_max_stack', 35)) {
+ watchdog('actions', 'Stack overflow: too many calls to actions_do(). Aborting to prevent infinite recursion.', array(), WATCHDOG_ERROR);
+ return;
+ }
+ $actions = array();
+ $available_actions = actions_list();
+ $result = array();
+ if (is_array($action_ids)) {
+ $where = array();
+ $where_values = array();
+ foreach ($action_ids as $action_id) {
+ if (is_numeric($action_id)) {
+ $where[] = 'OR aid = %d';
+ $where_values[] = $action_id;
+ }
+ elseif (isset($available_actions[$action_id])) {
+ $actions[$action_id] = $available_actions[$action_id];
+ }
+ }
+
+ // When we have action instances we must go to the database to
+ // retrieve instance data.
+ if ($where) {
+ $where_clause = implode(' ', $where);
+ // Strip off leading 'OR '.
+ $where_clause = '(' . strstr($where_clause, " ") . ')';
+ $result_db = db_query('SELECT * FROM {actions} WHERE ' . $where_clause, $where_values);
+ while ($action = db_fetch_object($result_db)) {
+ $actions[$action->aid] = $action->parameters ? unserialize($action->parameters) : array();
+ $actions[$action->aid]['callback'] = $action->callback;
+ $actions[$action->aid]['type'] = $action->type;
+ }
+ }
+
+ // Fire actions, in no particular order.
+ foreach ($actions as $action_id => $params) {
+ if (is_numeric($action_id)) { // Configurable actions need parameters.
+ $function = $params['callback'];
+ $context = array_merge($context, $params);
+ $result[$action_id] = $function($object, $context, $a1, $a2);
+ }
+ // Singleton action; $action_id is the function name.
+ else {
+ $result[$action_id] = $action_id($object, $context, $a1, $a2);
+ }
+ }
+ }
+ // Optimized execution of single action.
+ else {
+ // If it's a configurable action, retrieve stored parameters.
+ if (is_numeric($action_ids)) {
+ $action = db_fetch_object(db_query("SELECT * FROM {actions} WHERE aid = %d", $action_ids));
+ $function = $action->callback;
+ $context = array_merge($context, unserialize($action->parameters));
+ $result[$action_ids] = $function($object, $context, $a1, $a2);
+ }
+ // Singleton action; $action_ids is the function name.
+ else {
+ $result[$action_ids] = $action_ids($object, $context, $a1, $a2);
+ }
+ }
+ return $result;
+}
+
+
+/**
+ * Discover all action functions by invoking hook_action_info().
+ *
+ * mymodule_action_info() {
+ * return array(
+ * 'mymodule_functiondescription_action' => array(
+ * 'type' => 'node',
+ * 'description' => t('Save node'),
+ * 'configurable' => FALSE,
+ * 'hooks' => array(
+ * 'nodeapi' => array('delete', 'insert', 'update', 'view'),
+ * 'comment' => array('delete', 'insert', 'update', 'view'),
+ * )
+ * )
+ * );
+ * }
+ *
+ * The description is used in presenting possible actions to the user for
+ * configuration. The type is used to present these actions in a logical
+ * grouping and to denote context. Some types are 'node', 'user', 'comment',
+ * and 'system'. If an action is configurable it will provide form,
+ * validation and submission functions. The hooks the action supports
+ * are declared in the 'hooks' array.
+ *
+ * @param $reset
+ * Reset the action info static cache.
+ *
+ * @return
+ * An associative array keyed on function name. The value of each key is
+ * an array containing information about the action, such as type of
+ * action and description of the action, e.g.,
+ *
+ * @code
+ * $actions['node_publish_action'] = array(
+ * 'type' => 'node',
+ * 'description' => t('Publish post'),
+ * 'configurable' => FALSE,
+ * 'hooks' => array(
+ * 'nodeapi' => array('presave', 'insert', 'update', 'view'),
+ * 'comment' => array('delete', 'insert', 'update', 'view'),
+ * ),
+ * );
+ * @endcode
+ */
+function actions_list($reset = FALSE) {
+ static $actions;
+ if (!isset($actions) || $reset) {
+ $actions = module_invoke_all('action_info');
+ drupal_alter('action_info', $actions);
+ }
+
+ // See module_implements for explanations of this cast.
+ return (array)$actions;
+}
+
+/**
+ * Retrieve all action instances from the database.
+ *
+ * Compare with actions_list() which gathers actions by
+ * invoking hook_action_info(). The two are synchronized
+ * by visiting /admin/build/actions (when actions.module is
+ * enabled) which runs actions_synchronize().
+ *
+ * @return
+ * Associative array keyed by action ID. Each value is
+ * an associative array with keys 'callback', 'description',
+ * 'type' and 'configurable'.
+ */
+function actions_get_all_actions() {
+ $actions = array();
+ $result = db_query("SELECT * FROM {actions}");
+ while ($action = db_fetch_object($result)) {
+ $actions[$action->aid] = array(
+ 'callback' => $action->callback,
+ 'description' => $action->description,
+ 'type' => $action->type,
+ 'configurable' => (bool) $action->parameters,
);
}
+ return $actions;
+}
+
+/**
+ * Create an associative array keyed by md5 hashes of function names.
+ *
+ * Hashes are used to prevent actual function names from going out into
+ * HTML forms and coming back.
+ *
+ * @param $actions
+ * An associative array with function names as keys and associative
+ * arrays with keys 'description', 'type', etc. as values. Generally
+ * the output of actions_list() or actions_get_all_actions() is given
+ * as input to this function.
+ *
+ * @return
+ * An associative array keyed on md5 hash of function name. The value of
+ * each key is an associative array of function, description, and type
+ * for the action.
+ */
+function actions_actions_map($actions) {
+ $actions_map = array();
+ foreach ($actions as $callback => $array) {
+ $key = md5($callback);
+ $actions_map[$key]['callback'] = isset($array['callback']) ? $array['callback'] : $callback;
+ $actions_map[$key]['description'] = $array['description'];
+ $actions_map[$key]['type'] = $array['type'];
+ $actions_map[$key]['configurable'] = $array['configurable'];
+ }
+ return $actions_map;
+}
+
+/**
+ * Given an md5 hash of a function name, return the function name.
+ *
+ * Faster than actions_actions_map() when you only need the function name.
+ *
+ * @param $hash
+ * MD5 hash of a function name
+ *
+ * @return
+ * Function name
+ */
+function actions_function_lookup($hash) {
+ $actions_list = actions_list();
+ foreach ($actions_list as $function => $array) {
+ if (md5($function) == $hash) {
+ return $function;
+ }
+ }
- /**
- * Test the configuration of advanced actions through the administration
- * interface.
- */
- function testActionConfiguration() {
- // Create a user with permission to view the actions administration pages.
- $user = $this->drupalCreateUser(array('administer actions'));
- $this->drupalLogin($user);
-
- // Make a POST request to admin/settings/actions/manage.
- $edit = array();
- $edit['action'] = md5('system_goto_action');
- $this->drupalPost('admin/settings/actions/manage', $edit, t('Create'));
-
- // Make a POST request to the individual action configuration page.
- $edit = array();
- $action_description = $this->randomName();
- $edit['actions_description'] = $action_description;
- $edit['url'] = 'admin';
- $this->drupalPost('admin/settings/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."));
-
- // 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;
- $edit['url'] = 'admin';
- $this->drupalPost('admin/settings/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."));
-
- // Make sure that deletions work properly.
- $this->clickLink(t('delete'));
- $edit = array();
- $this->drupalPost('admin/settings/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->drupalGet('admin/settings/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."));
- $exists = db_result(db_query("SELECT aid FROM {actions} WHERE callback = 'drupal_goto_action'"));
- $this->assertFalse($exists, t('Make sure the action is gone from the database after being deleted.'));
+ // Must be an instance; must check database.
+ $aid = db_result(db_query("SELECT aid FROM {actions} WHERE MD5(aid) = '%s' AND parameters != ''", $hash));
+ return $aid;
+}
+
+/**
+ * Synchronize actions that are provided by modules.
+ *
+ * They are synchronized with actions that are stored in the actions table.
+ * This is necessary so that actions that do not require configuration can
+ * receive action IDs. This is not necessarily the best approach,
+ * but it is the most straightforward.
+ */
+function actions_synchronize($actions_in_code = array(), $delete_orphans = FALSE) {
+ if (!$actions_in_code) {
+ $actions_in_code = actions_list();
+ }
+ $actions_in_db = array();
+ $result = db_query("SELECT * FROM {actions} WHERE parameters = ''");
+ while ($action = db_fetch_object($result)) {
+ $actions_in_db[$action->callback] = array('aid' => $action->aid, 'description' => $action->description);
+ }
+
+ // Go through all the actions provided by modules.
+ foreach ($actions_in_code as $callback => $array) {
+ // Ignore configurable actions since their instances get put in
+ // when the user adds the action.
+ if (!$array['configurable']) {
+ // If we already have an action ID for this action, no need to assign aid.
+ if (array_key_exists($callback, $actions_in_db)) {
+ unset($actions_in_db[$callback]);
+ }
+ else {
+ // This is a new singleton that we don't have an aid for; assign one.
+ db_query("INSERT INTO {actions} (aid, type, callback, parameters, description) VALUES ('%s', '%s', '%s', '%s', '%s')", $callback, $array['type'], $callback, '', $array['description']);
+ watchdog('actions', "Action '%action' added.", array('%action' => filter_xss_admin($array['description'])));
+ }
+ }
+ }
+
+ // Any actions that we have left in $actions_in_db are orphaned.
+ if ($actions_in_db) {
+ $orphaned = array();
+ $placeholder = array();
+
+ foreach ($actions_in_db as $callback => $array) {
+ $orphaned[] = $callback;
+ $placeholder[] = "'%s'";
+ }
+
+ $orphans = implode(', ', $orphaned);
+
+ if ($delete_orphans) {
+ $placeholders = implode(', ', $placeholder);
+ $results = db_query("SELECT a.aid, a.description FROM {actions} a WHERE callback IN ($placeholders)", $orphaned);
+ while ($action = db_fetch_object($results)) {
+ actions_delete($action->aid);
+ watchdog('actions', "Removed orphaned action '%action' from database.", array('%action' => filter_xss_admin($action->description)));
+ }
+ }
+ else {
+ $link = l(t('Remove orphaned actions'), 'admin/build/actions/orphan');
+ $count = count($actions_in_db);
+ watchdog('actions', format_plural($count, 'One orphaned action (%orphans) exists in the actions table. !link', '@count orphaned actions (%orphans) exist in the actions table. !link', array('@count' => $count, '%orphans' => $orphans, '!link' => $link), 'warning'));
+ }
}
}
+
+/**
+ * Save an action and its associated user-supplied parameter values to the database.
+ *
+ * @param $function
+ * The name of the function to be called when this action is performed.
+ * @param $params
+ * An associative array with parameter names as keys and parameter values
+ * as values.
+ * @param $desc
+ * A user-supplied description of this particular action, e.g., 'Send
+ * e-mail to Jim'.
+ * @param $aid
+ * The ID of this action. If omitted, a new action is created.
+ *
+ * @return
+ * The ID of the action.
+ */
+function actions_save($function, $type, $params, $desc, $aid = NULL) {
+ $serialized = serialize($params);
+ if ($aid) {
+ db_query("UPDATE {actions} SET callback = '%s', type = '%s', parameters = '%s', description = '%s' WHERE aid = %d", $function, $type, $serialized, $desc, $aid);
+ watchdog('actions', 'Action %action saved.', array('%action' => $desc));
+ }
+ else {
+ // aid is the callback for singleton actions so we need to keep a
+ // separate table for numeric aids.
+ db_query('INSERT INTO {actions_aid} VALUES (default)');
+ $aid = db_last_insert_id('actions_aid', 'aid');
+ db_query("INSERT INTO {actions} (aid, callback, type, parameters, description) VALUES (%d, '%s', '%s', '%s', '%s')", $aid, $function, $type, $serialized, $desc);
+ watchdog('actions', 'Action %action created.', array('%action' => $desc));
+ }
+
+ return $aid;
+}
+
+/**
+ * Retrieve a single action from the database.
+ *
+ * @param $aid
+ * integer The ID of the action to retrieve.
+ *
+ * @return
+ * The appropriate action row from the database as an object.
+ */
+function actions_load($aid) {
+ return db_fetch_object(db_query("SELECT * FROM {actions} WHERE aid = %d", $aid));
+}
+
+/**
+ * Delete a single action from the database.
+ *
+ * @param $aid
+ * integer The ID of the action to delete.
+ */
+function actions_delete($aid) {
+ db_query("DELETE FROM {actions} WHERE aid = %d", $aid);
+ module_invoke_all('actions_delete', $aid);
+}