Simpletest Coverage - includes/actions.inc

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

Legend

Missed
lines code that were not excersized during program execution.
Covered
lines code were excersized during program execution.
Comment/non executable
Comment or non-executable line of code.
Dead
lines of code that according to xdebug could not be executed. This is counted as coverage code because in almost all cases it is code that runnable.