variable_get('actions_max_stack', 35)) { watchdog('actions', t('Stack overflow: too many calls to actions_do(). Aborting to prevent infinite recursion.'), 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; } /** * This dispatch function hands off structured Drupal arrays to type-specific * *_alter implementations. It ensures a consistent interface for all altering * operations. * * @param $type * The data type of the structured array. 'form', 'links', * 'node_content', and so on are several examples. * @param $data * The structured array to be altered. * @param ... * Any additional params will be passed on to the called * hook_$type_alter functions. */ function actions_alter($type, &$data) { // PHP's func_get_args() always returns copies of params, not references, so // actions_alter() can only manipulate data that comes in via the required first // param. For the edge case functions that must pass in an arbitrary number of // alterable parameters (hook_form_alter() being the best example), an array of // those params can be placed in the __actions_alter_by_ref key of the $data // array. This is somewhat ugly, but is an unavoidable consequence of a flexible // actions_alter() function, and the limitations of func_get_args(). // @todo: Remove this in Drupal 7. if (is_array($data) && isset($data['__actions_alter_by_ref'])) { $by_ref_parameters = $data['__actions_alter_by_ref']; unset($data['__actions_alter_by_ref']); } // Hang onto a reference to the data array so that it isn't blown away later. // Also, merge in any parameters that need to be passed by reference. $args = array(&$data); if (isset($by_ref_parameters)) { $args = array_merge($args, $by_ref_parameters); } // Now, use func_get_args() to pull in any additional parameters passed into // the actions_alter() call. $additional_args = func_get_args(); array_shift($additional_args); array_shift($additional_args); $args = array_merge($args, $additional_args); foreach (module_implements($type .'_alter') as $module) { $function = $module .'_'. $type .'_alter'; call_user_func_array($function, $args); } } /** * 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'); actions_alter('action_info', $actions); } // See module_implements for explanations of this cast. return (array)$actions; } /** * Explode a string of given tags into an array. */ function actions_explode_tags($tags) { // This regexp allows the following types of user input: // this, "somecompany, llc", "and ""this"" w,o.rks", foo bar $regexp = '%(?:^|,\ *)("(?>[^"]*)(?>""[^"]* )*"|(?: [^",]*))%x'; preg_match_all($regexp, $tags, $matches); $typed_tags = array_unique($matches[1]); $tags = array(); foreach ($typed_tags as $tag) { // If a user has escaped a term (to demonstrate that it is a group, // or includes a comma or quote character), we remove the escape // formatting so to save the term into the database as the user intends. $tag = trim(str_replace('""', '"', preg_replace('/^"(.*)"$/', '\1', $tag))); if ($tag != "") { $tags[] = $tag; } } return $tags; } /** * Implode an array of tags into a string. */ function actions_implode_tags($tags) { $encoded_tags = array(); foreach ($tags as $tag) { // Commas and quotes in tag names are special cases, so encode them. if (strpos($tag, ',') !== FALSE || strpos($tag, '"') !== FALSE) { $tag = '"'. str_replace('"', '""', $tag) .'"'; } $encoded_tags[] = $tag; } return implode(', ', $encoded_tags); } /** * 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; } } // Must be an instance; must check database. $aid = db_result(db_query("SELECT callback 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', t('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', t('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', '%message (%orphans). !link', array('%message' => format_plural($count, t('There is an orphaned action in your actions table'), t('There are orphaned actions in your actions table')), '%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', t('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_next_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', t('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); }