From aa433a093bda05f4b3a03854d7db6ca0db7595bf Mon Sep 17 00:00:00 2001 From: Dries Buytaert Date: Sun, 29 Jun 2008 12:07:15 +0000 Subject: - Restored actions.inc --- includes/actions.inc | 414 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 356 insertions(+), 58 deletions(-) (limited to 'includes/actions.inc') 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 @@ 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); +} -- cgit v1.2.3