| webmaster@1 | 1 <?php | 
| webmaster@1 | 2 // $Id: actions.inc,v 1.8 2007/12/31 14:51:04 goba Exp $ | 
| webmaster@1 | 3 | 
| webmaster@1 | 4 /** | 
| webmaster@1 | 5  * @file | 
| webmaster@1 | 6  * This is the actions engine for executing stored actions. | 
| webmaster@1 | 7  */ | 
| webmaster@1 | 8 | 
| webmaster@1 | 9 /** | 
| webmaster@1 | 10  * Perform a given list of actions by executing their callback functions. | 
| webmaster@1 | 11  * | 
| webmaster@1 | 12  * Given the IDs of actions to perform, find out what the callbacks | 
| webmaster@1 | 13  * for the actions are by querying the database. Then call each callback | 
| webmaster@1 | 14  * using the function call $function($object, $context, $a1, $2) | 
| webmaster@1 | 15  * where $function is the name of a function written in compliance with | 
| webmaster@1 | 16  * the action specification; that is, foo($object, $context). | 
| webmaster@1 | 17  * | 
| webmaster@1 | 18  * @param $action_ids | 
| webmaster@1 | 19  *   The ID of the action to perform. Can be a single action ID or an array | 
| webmaster@1 | 20  *   of IDs. IDs of instances will be numeric; IDs of singletons will be | 
| webmaster@1 | 21  *   function names. | 
| webmaster@1 | 22  * @param $object | 
| webmaster@1 | 23  *   Parameter that will be passed along to the callback. Typically the | 
| webmaster@1 | 24  *   object that the action will act on; a node, user or comment object. | 
| webmaster@1 | 25  *   If the action does not act on an object, pass a dummy object. This | 
| webmaster@1 | 26  *   is necessary to support PHP 4 object referencing. | 
| webmaster@1 | 27  * @param $context | 
| webmaster@1 | 28  *   Parameter that will be passed along to the callback. $context is a | 
| webmaster@1 | 29  *   keyed array containing extra information about what is currently | 
| webmaster@1 | 30  *   happening at the time of the call. Typically $context['hook'] and | 
| webmaster@1 | 31  *   $context['op'] will tell which hook-op combination resulted in this | 
| webmaster@1 | 32  *   call to actions_do(). | 
| webmaster@1 | 33  * @param $a1 | 
| webmaster@1 | 34  *   Parameter that will be passed along to the callback. | 
| webmaster@1 | 35  * @param $a2 | 
| webmaster@1 | 36  *   Parameter that will be passed along to the callback. | 
| webmaster@1 | 37  * | 
| webmaster@1 | 38  * @return | 
| webmaster@1 | 39  *   An associative array containing the result of the function that | 
| webmaster@1 | 40  *   performs the action, keyed on action ID. | 
| webmaster@1 | 41  */ | 
| webmaster@1 | 42 function actions_do($action_ids, &$object, $context = NULL, $a1 = NULL, $a2 = NULL) { | 
| webmaster@1 | 43   static $stack; | 
| webmaster@1 | 44   $stack++; | 
| webmaster@1 | 45   if ($stack > variable_get('actions_max_stack', 35)) { | 
| webmaster@1 | 46     watchdog('actions', 'Stack overflow: too many calls to actions_do(). Aborting to prevent infinite recursion.', WATCHDOG_ERROR); | 
| webmaster@1 | 47     return; | 
| webmaster@1 | 48   } | 
| webmaster@1 | 49   $actions = array(); | 
| webmaster@1 | 50   $available_actions = actions_list(); | 
| webmaster@1 | 51   $result = array(); | 
| webmaster@1 | 52   if (is_array($action_ids)) { | 
| webmaster@1 | 53     $where = array(); | 
| webmaster@1 | 54     $where_values = array(); | 
| webmaster@1 | 55     foreach ($action_ids as $action_id) { | 
| webmaster@1 | 56       if (is_numeric($action_id)) { | 
| webmaster@1 | 57         $where[] = 'OR aid = %d'; | 
| webmaster@1 | 58         $where_values[] = $action_id; | 
| webmaster@1 | 59       } | 
| webmaster@1 | 60       elseif (isset($available_actions[$action_id])) { | 
| webmaster@1 | 61         $actions[$action_id] = $available_actions[$action_id]; | 
| webmaster@1 | 62       } | 
| webmaster@1 | 63     } | 
| webmaster@1 | 64 | 
| webmaster@1 | 65     // When we have action instances we must go to the database to | 
| webmaster@1 | 66     // retrieve instance data. | 
| webmaster@1 | 67     if ($where) { | 
| webmaster@1 | 68       $where_clause = implode(' ', $where); | 
| webmaster@1 | 69       // Strip off leading 'OR '. | 
| webmaster@1 | 70       $where_clause = '('. strstr($where_clause, " ") .')'; | 
| webmaster@1 | 71       $result_db = db_query('SELECT * FROM {actions} WHERE '. $where_clause, $where_values); | 
| webmaster@1 | 72       while ($action = db_fetch_object($result_db)) { | 
| webmaster@1 | 73         $actions[$action->aid] = $action->parameters ? unserialize($action->parameters) : array(); | 
| webmaster@1 | 74         $actions[$action->aid]['callback'] = $action->callback; | 
| webmaster@1 | 75         $actions[$action->aid]['type'] = $action->type; | 
| webmaster@1 | 76       } | 
| webmaster@1 | 77     } | 
| webmaster@1 | 78 | 
| webmaster@1 | 79     // Fire actions, in no particular order. | 
| webmaster@1 | 80     foreach ($actions as $action_id => $params) { | 
| webmaster@1 | 81       if (is_numeric($action_id)) { // Configurable actions need parameters. | 
| webmaster@1 | 82         $function = $params['callback']; | 
| webmaster@1 | 83         $context = array_merge($context, $params); | 
| webmaster@1 | 84         $result[$action_id] = $function($object, $context, $a1, $a2); | 
| webmaster@1 | 85       } | 
| webmaster@1 | 86       // Singleton action; $action_id is the function name. | 
| webmaster@1 | 87       else { | 
| webmaster@1 | 88         $result[$action_id] = $action_id($object, $context, $a1, $a2); | 
| webmaster@1 | 89       } | 
| webmaster@1 | 90     } | 
| webmaster@1 | 91   } | 
| webmaster@1 | 92   // Optimized execution of single action. | 
| webmaster@1 | 93   else { | 
| webmaster@1 | 94     // If it's a configurable action, retrieve stored parameters. | 
| webmaster@1 | 95     if (is_numeric($action_ids)) { | 
| webmaster@1 | 96       $action = db_fetch_object(db_query("SELECT * FROM {actions} WHERE aid = %d", $action_ids)); | 
| webmaster@1 | 97       $function = $action->callback; | 
| webmaster@1 | 98       $context = array_merge($context, unserialize($action->parameters)); | 
| webmaster@1 | 99       $result[$action_ids] = $function($object, $context, $a1, $a2); | 
| webmaster@1 | 100     } | 
| webmaster@1 | 101     // Singleton action; $action_ids is the function name. | 
| webmaster@1 | 102     else { | 
| webmaster@1 | 103       $result[$action_ids] = $action_ids($object, $context, $a1, $a2); | 
| webmaster@1 | 104     } | 
| webmaster@1 | 105   } | 
| webmaster@1 | 106   return $result; | 
| webmaster@1 | 107 } | 
| webmaster@1 | 108 | 
| webmaster@1 | 109 | 
| webmaster@1 | 110 /** | 
| webmaster@1 | 111  * Discover all action functions by invoking hook_action_info(). | 
| webmaster@1 | 112  * | 
| webmaster@1 | 113  * mymodule_action_info() { | 
| webmaster@1 | 114  *   return array( | 
| webmaster@1 | 115  *     'mymodule_functiondescription_action' => array( | 
| webmaster@1 | 116  *       'type' => 'node', | 
| webmaster@1 | 117  *       'description' => t('Save node'), | 
| webmaster@1 | 118  *       'configurable' => FALSE, | 
| webmaster@1 | 119  *       'hooks' => array( | 
| webmaster@1 | 120  *         'nodeapi' => array('delete', 'insert', 'update', 'view'), | 
| webmaster@1 | 121  *         'comment' => array('delete', 'insert', 'update', 'view'), | 
| webmaster@1 | 122  *       ) | 
| webmaster@1 | 123  *     ) | 
| webmaster@1 | 124  *   ); | 
| webmaster@1 | 125  * } | 
| webmaster@1 | 126  * | 
| webmaster@1 | 127  * The description is used in presenting possible actions to the user for | 
| webmaster@1 | 128  * configuration. The type is used to present these actions in a logical | 
| webmaster@1 | 129  * grouping and to denote context. Some types are 'node', 'user', 'comment', | 
| webmaster@1 | 130  * and 'system'. If an action is configurable it will provide form, | 
| webmaster@1 | 131  * validation and submission functions. The hooks the action supports | 
| webmaster@1 | 132  * are declared in the 'hooks' array. | 
| webmaster@1 | 133  * | 
| webmaster@1 | 134  * @param $reset | 
| webmaster@1 | 135  *   Reset the action info static cache. | 
| webmaster@1 | 136  * | 
| webmaster@1 | 137  * @return | 
| webmaster@1 | 138  *   An associative array keyed on function name. The value of each key is | 
| webmaster@1 | 139  *   an array containing information about the action, such as type of | 
| webmaster@1 | 140  *   action and description of the action, e.g., | 
| webmaster@1 | 141  * | 
| webmaster@1 | 142  *   @code | 
| webmaster@1 | 143  *   $actions['node_publish_action'] = array( | 
| webmaster@1 | 144  *     'type' => 'node', | 
| webmaster@1 | 145  *     'description' => t('Publish post'), | 
| webmaster@1 | 146  *     'configurable' => FALSE, | 
| webmaster@1 | 147  *     'hooks' => array( | 
| webmaster@1 | 148  *       'nodeapi' => array('presave', 'insert', 'update', 'view'), | 
| webmaster@1 | 149  *       'comment' => array('delete', 'insert', 'update', 'view'), | 
| webmaster@1 | 150  *     ), | 
| webmaster@1 | 151  *   ); | 
| webmaster@1 | 152  *   @endcode | 
| webmaster@1 | 153  */ | 
| webmaster@1 | 154 function actions_list($reset = FALSE) { | 
| webmaster@1 | 155   static $actions; | 
| webmaster@1 | 156   if (!isset($actions) || $reset) { | 
| webmaster@1 | 157     $actions = module_invoke_all('action_info'); | 
| webmaster@1 | 158     drupal_alter('action_info', $actions); | 
| webmaster@1 | 159   } | 
| webmaster@1 | 160 | 
| webmaster@1 | 161   // See module_implements for explanations of this cast. | 
| webmaster@1 | 162   return (array)$actions; | 
| webmaster@1 | 163 } | 
| webmaster@1 | 164 | 
| webmaster@1 | 165 /** | 
| webmaster@1 | 166  * Retrieve all action instances from the database. | 
| webmaster@1 | 167  * | 
| webmaster@1 | 168  * Compare with actions_list() which gathers actions by | 
| webmaster@1 | 169  * invoking hook_action_info(). The two are synchronized | 
| webmaster@1 | 170  * by visiting /admin/build/actions (when actions.module is | 
| webmaster@1 | 171  * enabled) which runs actions_synchronize(). | 
| webmaster@1 | 172  * | 
| webmaster@1 | 173  * @return | 
| webmaster@1 | 174  *   Associative array keyed by action ID. Each value is | 
| webmaster@1 | 175  *   an associative array with keys 'callback', 'description', | 
| webmaster@1 | 176  *   'type' and 'configurable'. | 
| webmaster@1 | 177  */ | 
| webmaster@1 | 178 function actions_get_all_actions() { | 
| webmaster@1 | 179   $actions = array(); | 
| webmaster@1 | 180   $result = db_query("SELECT * FROM {actions}"); | 
| webmaster@1 | 181   while ($action = db_fetch_object($result)) { | 
| webmaster@1 | 182     $actions[$action->aid] = array( | 
| webmaster@1 | 183       'callback' => $action->callback, | 
| webmaster@1 | 184       'description' => $action->description, | 
| webmaster@1 | 185       'type' => $action->type, | 
| webmaster@1 | 186       'configurable' => (bool) $action->parameters, | 
| webmaster@1 | 187     ); | 
| webmaster@1 | 188   } | 
| webmaster@1 | 189   return $actions; | 
| webmaster@1 | 190 } | 
| webmaster@1 | 191 | 
| webmaster@1 | 192 /** | 
| webmaster@1 | 193  * Create an associative array keyed by md5 hashes of function names. | 
| webmaster@1 | 194  * | 
| webmaster@1 | 195  * Hashes are used to prevent actual function names from going out into | 
| webmaster@1 | 196  * HTML forms and coming back. | 
| webmaster@1 | 197  * | 
| webmaster@1 | 198  * @param $actions | 
| webmaster@1 | 199  *   An associative array with function names as keys and associative | 
| webmaster@1 | 200  *   arrays with keys 'description', 'type', etc. as values. Generally | 
| webmaster@1 | 201  *   the output of actions_list() or actions_get_all_actions() is given | 
| webmaster@1 | 202  *   as input to this function. | 
| webmaster@1 | 203  * | 
| webmaster@1 | 204  * @return | 
| webmaster@1 | 205  *   An associative array keyed on md5 hash of function name. The value of | 
| webmaster@1 | 206  *   each key is an associative array of function, description, and type | 
| webmaster@1 | 207  *   for the action. | 
| webmaster@1 | 208  */ | 
| webmaster@1 | 209 function actions_actions_map($actions) { | 
| webmaster@1 | 210   $actions_map = array(); | 
| webmaster@1 | 211   foreach ($actions as $callback => $array) { | 
| webmaster@1 | 212     $key = md5($callback); | 
| webmaster@1 | 213     $actions_map[$key]['callback']     = isset($array['callback']) ? $array['callback'] : $callback; | 
| webmaster@1 | 214     $actions_map[$key]['description']  = $array['description']; | 
| webmaster@1 | 215     $actions_map[$key]['type']         = $array['type']; | 
| webmaster@1 | 216     $actions_map[$key]['configurable'] = $array['configurable']; | 
| webmaster@1 | 217   } | 
| webmaster@1 | 218   return $actions_map; | 
| webmaster@1 | 219 } | 
| webmaster@1 | 220 | 
| webmaster@1 | 221 /** | 
| webmaster@1 | 222  * Given an md5 hash of a function name, return the function name. | 
| webmaster@1 | 223  * | 
| webmaster@1 | 224  * Faster than actions_actions_map() when you only need the function name. | 
| webmaster@1 | 225  * | 
| webmaster@1 | 226  * @param $hash | 
| webmaster@1 | 227  *   MD5 hash of a function name | 
| webmaster@1 | 228  * | 
| webmaster@1 | 229  * @return | 
| webmaster@1 | 230  *   Function name | 
| webmaster@1 | 231  */ | 
| webmaster@1 | 232 function actions_function_lookup($hash) { | 
| webmaster@1 | 233   $actions_list = actions_list(); | 
| webmaster@1 | 234   foreach ($actions_list as $function => $array) { | 
| webmaster@1 | 235     if (md5($function) == $hash) { | 
| webmaster@1 | 236       return $function; | 
| webmaster@1 | 237     } | 
| webmaster@1 | 238   } | 
| webmaster@1 | 239 | 
| webmaster@1 | 240   // Must be an instance; must check database. | 
| webmaster@1 | 241   $aid = db_result(db_query("SELECT aid FROM {actions} WHERE MD5(aid) = '%s' AND parameters != ''", $hash)); | 
| webmaster@1 | 242   return $aid; | 
| webmaster@1 | 243 } | 
| webmaster@1 | 244 | 
| webmaster@1 | 245 /** | 
| webmaster@1 | 246  * Synchronize actions that are provided by modules. | 
| webmaster@1 | 247  * | 
| webmaster@1 | 248  * They are synchronized with actions that are stored in the actions table. | 
| webmaster@1 | 249  * This is necessary so that actions that do not require configuration can | 
| webmaster@1 | 250  * receive action IDs. This is not necessarily the best approach, | 
| webmaster@1 | 251  * but it is the most straightforward. | 
| webmaster@1 | 252  */ | 
| webmaster@1 | 253 function actions_synchronize($actions_in_code = array(), $delete_orphans = FALSE) { | 
| webmaster@1 | 254   if (!$actions_in_code) { | 
| webmaster@1 | 255     $actions_in_code = actions_list(); | 
| webmaster@1 | 256   } | 
| webmaster@1 | 257   $actions_in_db = array(); | 
| webmaster@1 | 258   $result = db_query("SELECT * FROM {actions} WHERE parameters = ''"); | 
| webmaster@1 | 259   while ($action = db_fetch_object($result)) { | 
| webmaster@1 | 260     $actions_in_db[$action->callback] = array('aid' => $action->aid, 'description' => $action->description); | 
| webmaster@1 | 261   } | 
| webmaster@1 | 262 | 
| webmaster@1 | 263   // Go through all the actions provided by modules. | 
| webmaster@1 | 264   foreach ($actions_in_code as $callback => $array) { | 
| webmaster@1 | 265     // Ignore configurable actions since their instances get put in | 
| webmaster@1 | 266     // when the user adds the action. | 
| webmaster@1 | 267     if (!$array['configurable']) { | 
| webmaster@1 | 268       // If we already have an action ID for this action, no need to assign aid. | 
| webmaster@1 | 269       if (array_key_exists($callback, $actions_in_db)) { | 
| webmaster@1 | 270         unset($actions_in_db[$callback]); | 
| webmaster@1 | 271       } | 
| webmaster@1 | 272       else { | 
| webmaster@1 | 273         // This is a new singleton that we don't have an aid for; assign one. | 
| webmaster@1 | 274         db_query("INSERT INTO {actions} (aid, type, callback, parameters, description) VALUES ('%s', '%s', '%s', '%s', '%s')", $callback, $array['type'], $callback, '', $array['description']); | 
| webmaster@1 | 275         watchdog('actions', "Action '%action' added.", array('%action' => filter_xss_admin($array['description']))); | 
| webmaster@1 | 276       } | 
| webmaster@1 | 277     } | 
| webmaster@1 | 278   } | 
| webmaster@1 | 279 | 
| webmaster@1 | 280   // Any actions that we have left in $actions_in_db are orphaned. | 
| webmaster@1 | 281   if ($actions_in_db) { | 
| webmaster@1 | 282     $orphaned = array(); | 
| webmaster@1 | 283     $placeholder = array(); | 
| webmaster@1 | 284 | 
| webmaster@1 | 285     foreach ($actions_in_db as $callback => $array) { | 
| webmaster@1 | 286       $orphaned[] = $callback; | 
| webmaster@1 | 287       $placeholder[] = "'%s'"; | 
| webmaster@1 | 288     } | 
| webmaster@1 | 289 | 
| webmaster@1 | 290     $orphans = implode(', ', $orphaned); | 
| webmaster@1 | 291 | 
| webmaster@1 | 292     if ($delete_orphans) { | 
| webmaster@1 | 293       $placeholders = implode(', ', $placeholder); | 
| webmaster@1 | 294       $results = db_query("SELECT a.aid, a.description FROM {actions} a WHERE callback IN ($placeholders)", $orphaned); | 
| webmaster@1 | 295       while ($action = db_fetch_object($results)) { | 
| webmaster@1 | 296         actions_delete($action->aid); | 
| webmaster@1 | 297         watchdog('actions', "Removed orphaned action '%action' from database.", array('%action' => filter_xss_admin($action->description))); | 
| webmaster@1 | 298       } | 
| webmaster@1 | 299     } | 
| webmaster@1 | 300     else { | 
| webmaster@1 | 301       $link = l(t('Remove orphaned actions'), 'admin/build/actions/orphan'); | 
| webmaster@1 | 302       $count = count($actions_in_db); | 
| webmaster@1 | 303       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')); | 
| webmaster@1 | 304     } | 
| webmaster@1 | 305   } | 
| webmaster@1 | 306 } | 
| webmaster@1 | 307 | 
| webmaster@1 | 308 /** | 
| webmaster@1 | 309  * Save an action and its associated user-supplied parameter values to the database. | 
| webmaster@1 | 310  * | 
| webmaster@1 | 311  * @param $function | 
| webmaster@1 | 312  *   The name of the function to be called when this action is performed. | 
| webmaster@1 | 313  * @param $params | 
| webmaster@1 | 314  *   An associative array with parameter names as keys and parameter values | 
| webmaster@1 | 315  *   as values. | 
| webmaster@1 | 316  * @param $desc | 
| webmaster@1 | 317  *   A user-supplied description of this particular action, e.g., 'Send | 
| webmaster@1 | 318  *   e-mail to Jim'. | 
| webmaster@1 | 319  * @param $aid | 
| webmaster@1 | 320  *   The ID of this action. If omitted, a new action is created. | 
| webmaster@1 | 321  * | 
| webmaster@1 | 322  * @return | 
| webmaster@1 | 323  *   The ID of the action. | 
| webmaster@1 | 324  */ | 
| webmaster@1 | 325 function actions_save($function, $type, $params, $desc, $aid = NULL) { | 
| webmaster@1 | 326   $serialized = serialize($params); | 
| webmaster@1 | 327   if ($aid) { | 
| webmaster@1 | 328     db_query("UPDATE {actions} SET callback = '%s', type = '%s', parameters = '%s', description = '%s' WHERE aid = %d", $function, $type, $serialized, $desc, $aid); | 
| webmaster@1 | 329     watchdog('actions', 'Action %action saved.', array('%action' => $desc)); | 
| webmaster@1 | 330   } | 
| webmaster@1 | 331   else { | 
| webmaster@1 | 332     // aid is the callback for singleton actions so we need to keep a | 
| webmaster@1 | 333     // separate table for numeric aids. | 
| webmaster@1 | 334     db_query('INSERT INTO {actions_aid} VALUES (default)'); | 
| webmaster@1 | 335     $aid = db_last_insert_id('actions_aid', 'aid'); | 
| webmaster@1 | 336     db_query("INSERT INTO {actions} (aid, callback, type, parameters, description) VALUES (%d, '%s', '%s', '%s', '%s')", $aid, $function, $type, $serialized, $desc); | 
| webmaster@1 | 337     watchdog('actions', 'Action %action created.', array('%action' => $desc)); | 
| webmaster@1 | 338   } | 
| webmaster@1 | 339 | 
| webmaster@1 | 340   return $aid; | 
| webmaster@1 | 341 } | 
| webmaster@1 | 342 | 
| webmaster@1 | 343 /** | 
| webmaster@1 | 344  * Retrieve a single action from the database. | 
| webmaster@1 | 345  * | 
| webmaster@1 | 346  * @param $aid | 
| webmaster@1 | 347  *   integer The ID of the action to retrieve. | 
| webmaster@1 | 348  * | 
| webmaster@1 | 349  * @return | 
| webmaster@1 | 350  *   The appropriate action row from the database as an object. | 
| webmaster@1 | 351  */ | 
| webmaster@1 | 352 function actions_load($aid) { | 
| webmaster@1 | 353   return db_fetch_object(db_query("SELECT * FROM {actions} WHERE aid = %d", $aid)); | 
| webmaster@1 | 354 } | 
| webmaster@1 | 355 | 
| webmaster@1 | 356 /** | 
| webmaster@1 | 357  * Delete a single action from the database. | 
| webmaster@1 | 358  * | 
| webmaster@1 | 359  * @param $aid | 
| webmaster@1 | 360  *   integer The ID of the action to delete. | 
| webmaster@1 | 361  */ | 
| webmaster@1 | 362 function actions_delete($aid) { | 
| webmaster@1 | 363   db_query("DELETE FROM {actions} WHERE aid = %d", $aid); | 
| webmaster@1 | 364   module_invoke_all('actions_delete', $aid); | 
| webmaster@1 | 365 } |