actions.inc 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. <?php
  2. /**
  3. * @file
  4. * This is the actions engine for executing stored actions.
  5. */
  6. /**
  7. * @defgroup actions Actions
  8. * @{
  9. * Functions that perform an action on a certain system object.
  10. *
  11. * Action functions are declared by modules by implementing hook_action_info().
  12. * Modules can cause action functions to run by calling actions_do(), and
  13. * trigger.module provides a user interface that lets administrators define
  14. * events that cause action functions to run.
  15. *
  16. * Each action function takes two to four arguments:
  17. * - $entity: The object that the action acts on, such as a node, comment, or
  18. * user.
  19. * - $context: Array of additional information about what triggered the action.
  20. * - $a1, $a2: Optional additional information, which can be passed into
  21. * actions_do() and will be passed along to the action function.
  22. *
  23. * @}
  24. */
  25. /**
  26. * Performs a given list of actions by executing their callback functions.
  27. *
  28. * Given the IDs of actions to perform, this function finds out what the
  29. * callback functions for the actions are by querying the database. Then
  30. * it calls each callback using the function call $function($object, $context,
  31. * $a1, $a2), passing the input arguments of this function (see below) to the
  32. * action function.
  33. *
  34. * @param $action_ids
  35. * The IDs of the actions to perform. Can be a single action ID or an array
  36. * of IDs. IDs of configurable actions must be given as numeric action IDs;
  37. * IDs of non-configurable actions may be given as action function names.
  38. * @param $object
  39. * The object that the action will act on: a node, user, or comment object.
  40. * @param $context
  41. * Associative array containing extra information about what triggered
  42. * the action call, with $context['hook'] giving the name of the hook
  43. * that resulted in this call to actions_do().
  44. * @param $a1
  45. * Passed along to the callback.
  46. * @param $a2
  47. * Passed along to the callback.
  48. *
  49. * @return
  50. * An associative array containing the results of the functions that
  51. * perform the actions, keyed on action ID.
  52. *
  53. * @ingroup actions
  54. */
  55. function actions_do($action_ids, $object = NULL, $context = NULL, $a1 = NULL, $a2 = NULL) {
  56. // $stack tracks the number of recursive calls.
  57. static $stack;
  58. $stack++;
  59. if ($stack > variable_get('actions_max_stack', 35)) {
  60. watchdog('actions', 'Stack overflow: too many calls to actions_do(). Aborting to prevent infinite recursion.', array(), WATCHDOG_ERROR);
  61. return;
  62. }
  63. $actions = array();
  64. $available_actions = actions_list();
  65. $actions_result = array();
  66. if (is_array($action_ids)) {
  67. $conditions = array();
  68. foreach ($action_ids as $action_id) {
  69. if (is_numeric($action_id)) {
  70. $conditions[] = $action_id;
  71. }
  72. elseif (isset($available_actions[$action_id])) {
  73. $actions[$action_id] = $available_actions[$action_id];
  74. }
  75. }
  76. // When we have action instances we must go to the database to retrieve
  77. // instance data.
  78. if (!empty($conditions)) {
  79. $query = db_select('actions');
  80. $query->addField('actions', 'aid');
  81. $query->addField('actions', 'type');
  82. $query->addField('actions', 'callback');
  83. $query->addField('actions', 'parameters');
  84. $query->condition('aid', $conditions, 'IN');
  85. $result = $query->execute();
  86. foreach ($result as $action) {
  87. $actions[$action->aid] = $action->parameters ? unserialize($action->parameters) : array();
  88. $actions[$action->aid]['callback'] = $action->callback;
  89. $actions[$action->aid]['type'] = $action->type;
  90. }
  91. }
  92. // Fire actions, in no particular order.
  93. foreach ($actions as $action_id => $params) {
  94. // Configurable actions need parameters.
  95. if (is_numeric($action_id)) {
  96. $function = $params['callback'];
  97. if (function_exists($function)) {
  98. $context = array_merge($context, $params);
  99. $actions_result[$action_id] = $function($object, $context, $a1, $a2);
  100. }
  101. else {
  102. $actions_result[$action_id] = FALSE;
  103. }
  104. }
  105. // Singleton action; $action_id is the function name.
  106. else {
  107. $actions_result[$action_id] = $action_id($object, $context, $a1, $a2);
  108. }
  109. }
  110. }
  111. // Optimized execution of a single action.
  112. else {
  113. // If it's a configurable action, retrieve stored parameters.
  114. if (is_numeric($action_ids)) {
  115. $action = db_query("SELECT callback, parameters FROM {actions} WHERE aid = :aid", array(':aid' => $action_ids))->fetchObject();
  116. $function = $action->callback;
  117. if (function_exists($function)) {
  118. $context = array_merge($context, unserialize($action->parameters));
  119. $actions_result[$action_ids] = $function($object, $context, $a1, $a2);
  120. }
  121. else {
  122. $actions_result[$action_ids] = FALSE;
  123. }
  124. }
  125. // Singleton action; $action_ids is the function name.
  126. else {
  127. if (function_exists($action_ids)) {
  128. $actions_result[$action_ids] = $action_ids($object, $context, $a1, $a2);
  129. }
  130. else {
  131. // Set to avoid undefined index error messages later.
  132. $actions_result[$action_ids] = FALSE;
  133. }
  134. }
  135. }
  136. $stack--;
  137. return $actions_result;
  138. }
  139. /**
  140. * Discovers all available actions by invoking hook_action_info().
  141. *
  142. * This function contrasts with actions_get_all_actions(); see the
  143. * documentation of actions_get_all_actions() for an explanation.
  144. *
  145. * @param $reset
  146. * Reset the action info static cache.
  147. *
  148. * @return
  149. * An associative array keyed on action function name, with the same format
  150. * as the return value of hook_action_info(), containing all
  151. * modules' hook_action_info() return values as modified by any
  152. * hook_action_info_alter() implementations.
  153. *
  154. * @see hook_action_info()
  155. */
  156. function actions_list($reset = FALSE) {
  157. $actions = &drupal_static(__FUNCTION__);
  158. if (!isset($actions) || $reset) {
  159. $actions = module_invoke_all('action_info');
  160. drupal_alter('action_info', $actions);
  161. }
  162. // See module_implements() for an explanation of this cast.
  163. return (array) $actions;
  164. }
  165. /**
  166. * Retrieves all action instances from the database.
  167. *
  168. * This function differs from the actions_list() function, which gathers
  169. * actions by invoking hook_action_info(). The actions returned by this
  170. * function and the actions returned by actions_list() are partially
  171. * synchronized. Non-configurable actions from hook_action_info()
  172. * implementations are put into the database when actions_synchronize() is
  173. * called, which happens when admin/config/system/actions is visited.
  174. * Configurable actions are not added to the database until they are configured
  175. * in the user interface, in which case a database row is created for each
  176. * configuration of each action.
  177. *
  178. * @return
  179. * Associative array keyed by numeric action ID. Each value is an associative
  180. * array with keys 'callback', 'label', 'type' and 'configurable'.
  181. */
  182. function actions_get_all_actions() {
  183. $actions = db_query("SELECT aid, type, callback, parameters, label FROM {actions}")->fetchAllAssoc('aid', PDO::FETCH_ASSOC);
  184. foreach ($actions as &$action) {
  185. $action['configurable'] = (bool) $action['parameters'];
  186. unset($action['parameters']);
  187. unset($action['aid']);
  188. }
  189. return $actions;
  190. }
  191. /**
  192. * Creates an associative array keyed by hashes of function names or IDs.
  193. *
  194. * Hashes are used to prevent actual function names from going out into HTML
  195. * forms and coming back.
  196. *
  197. * @param $actions
  198. * An associative array with function names or action IDs as keys
  199. * and associative arrays with keys 'label', 'type', etc. as values.
  200. * This is usually the output of actions_list() or actions_get_all_actions().
  201. *
  202. * @return
  203. * An associative array whose keys are hashes of the input array keys, and
  204. * whose corresponding values are associative arrays with components
  205. * 'callback', 'label', 'type', and 'configurable' from the input array.
  206. */
  207. function actions_actions_map($actions) {
  208. $actions_map = array();
  209. foreach ($actions as $callback => $array) {
  210. $key = drupal_hash_base64($callback);
  211. $actions_map[$key]['callback'] = isset($array['callback']) ? $array['callback'] : $callback;
  212. $actions_map[$key]['label'] = $array['label'];
  213. $actions_map[$key]['type'] = $array['type'];
  214. $actions_map[$key]['configurable'] = $array['configurable'];
  215. }
  216. return $actions_map;
  217. }
  218. /**
  219. * Returns an action array key (function or ID), given its hash.
  220. *
  221. * Faster than actions_actions_map() when you only need the function name or ID.
  222. *
  223. * @param $hash
  224. * Hash of a function name or action ID array key. The array key
  225. * is a key into the return value of actions_list() (array key is the action
  226. * function name) or actions_get_all_actions() (array key is the action ID).
  227. *
  228. * @return
  229. * The corresponding array key, or FALSE if no match is found.
  230. */
  231. function actions_function_lookup($hash) {
  232. // Check for a function name match.
  233. $actions_list = actions_list();
  234. foreach ($actions_list as $function => $array) {
  235. if (drupal_hash_base64($function) == $hash) {
  236. return $function;
  237. }
  238. }
  239. $aid = FALSE;
  240. // Must be a configurable action; check database.
  241. $result = db_query("SELECT aid FROM {actions} WHERE parameters <> ''")->fetchAll(PDO::FETCH_ASSOC);
  242. foreach ($result as $row) {
  243. if (drupal_hash_base64($row['aid']) == $hash) {
  244. $aid = $row['aid'];
  245. break;
  246. }
  247. }
  248. return $aid;
  249. }
  250. /**
  251. * Synchronizes actions that are provided by modules in hook_action_info().
  252. *
  253. * Actions provided by modules in hook_action_info() implementations are
  254. * synchronized with actions that are stored in the actions database table.
  255. * This is necessary so that actions that do not require configuration can
  256. * receive action IDs.
  257. *
  258. * @param $delete_orphans
  259. * If TRUE, any actions that exist in the database but are no longer
  260. * found in the code (for example, because the module that provides them has
  261. * been disabled) will be deleted.
  262. */
  263. function actions_synchronize($delete_orphans = FALSE) {
  264. $actions_in_code = actions_list(TRUE);
  265. $actions_in_db = db_query("SELECT aid, callback, label FROM {actions} WHERE parameters = ''")->fetchAllAssoc('callback', PDO::FETCH_ASSOC);
  266. // Go through all the actions provided by modules.
  267. foreach ($actions_in_code as $callback => $array) {
  268. // Ignore configurable actions since their instances get put in when the
  269. // user adds the action.
  270. if (!$array['configurable']) {
  271. // If we already have an action ID for this action, no need to assign aid.
  272. if (isset($actions_in_db[$callback])) {
  273. unset($actions_in_db[$callback]);
  274. }
  275. else {
  276. // This is a new singleton that we don't have an aid for; assign one.
  277. db_insert('actions')
  278. ->fields(array(
  279. 'aid' => $callback,
  280. 'type' => $array['type'],
  281. 'callback' => $callback,
  282. 'parameters' => '',
  283. 'label' => $array['label'],
  284. ))
  285. ->execute();
  286. watchdog('actions', "Action '%action' added.", array('%action' => $array['label']));
  287. }
  288. }
  289. }
  290. // Any actions that we have left in $actions_in_db are orphaned.
  291. if ($actions_in_db) {
  292. $orphaned = array_keys($actions_in_db);
  293. if ($delete_orphans) {
  294. $actions = db_query('SELECT aid, label FROM {actions} WHERE callback IN (:orphaned)', array(':orphaned' => $orphaned))->fetchAll();
  295. foreach ($actions as $action) {
  296. actions_delete($action->aid);
  297. watchdog('actions', "Removed orphaned action '%action' from database.", array('%action' => $action->label));
  298. }
  299. }
  300. else {
  301. $link = l(t('Remove orphaned actions'), 'admin/config/system/actions/orphan');
  302. $count = count($actions_in_db);
  303. $orphans = implode(', ', $orphaned);
  304. watchdog('actions', '@count orphaned actions (%orphans) exist in the actions table. !link', array('@count' => $count, '%orphans' => $orphans, '!link' => $link), WATCHDOG_INFO);
  305. }
  306. }
  307. }
  308. /**
  309. * Saves an action and its user-supplied parameter values to the database.
  310. *
  311. * @param $function
  312. * The name of the function to be called when this action is performed.
  313. * @param $type
  314. * The type of action, to describe grouping and/or context, e.g., 'node',
  315. * 'user', 'comment', or 'system'.
  316. * @param $params
  317. * An associative array with parameter names as keys and parameter values as
  318. * values.
  319. * @param $label
  320. * A user-supplied label of this particular action, e.g., 'Send e-mail
  321. * to Jim'.
  322. * @param $aid
  323. * The ID of this action. If omitted, a new action is created.
  324. *
  325. * @return
  326. * The ID of the action.
  327. */
  328. function actions_save($function, $type, $params, $label, $aid = NULL) {
  329. // aid is the callback for singleton actions so we need to keep a separate
  330. // table for numeric aids.
  331. if (!$aid) {
  332. $aid = db_next_id();
  333. }
  334. db_merge('actions')
  335. ->key(array('aid' => $aid))
  336. ->fields(array(
  337. 'callback' => $function,
  338. 'type' => $type,
  339. 'parameters' => serialize($params),
  340. 'label' => $label,
  341. ))
  342. ->execute();
  343. watchdog('actions', 'Action %action saved.', array('%action' => $label));
  344. return $aid;
  345. }
  346. /**
  347. * Retrieves a single action from the database.
  348. *
  349. * @param $aid
  350. * The ID of the action to retrieve.
  351. *
  352. * @return
  353. * The appropriate action row from the database as an object.
  354. */
  355. function actions_load($aid) {
  356. return db_query("SELECT aid, type, callback, parameters, label FROM {actions} WHERE aid = :aid", array(':aid' => $aid))->fetchObject();
  357. }
  358. /**
  359. * Deletes a single action from the database.
  360. *
  361. * @param $aid
  362. * The ID of the action to delete.
  363. */
  364. function actions_delete($aid) {
  365. db_delete('actions')
  366. ->condition('aid', $aid)
  367. ->execute();
  368. module_invoke_all('actions_delete', $aid);
  369. }