/[drupal]/contributions/modules/configuration/configuration.module
ViewVC logotype

Contents of /contributions/modules/configuration/configuration.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.1 - (show annotations) (download) (as text)
Fri Feb 27 10:28:05 2009 UTC (9 months ago) by sarvab
Branch: MAIN
CVS Tags: HEAD
Branch point for: DRUPAL-6--1
File MIME type: text/x-php
Initial commit of the Configuration Framework. This framework will be used as the core of the Patterns module and provide a programming interface to configure sites.
1 <?php
2 // $Id: $
3
4 /**
5 * @file
6 * Provide a unified method for defining site configurations abstracted from their data format.
7 * Various data formats should be supported via a plugin architecture such as XML, YAML, JSON, PHP
8 */
9
10 /**
11 * The first execution phase: Apply the base map to $context
12 */
13 define('CONFIGURATION_INITIALIZE', 1);
14
15 /**
16 * The second execution phase: Apply the base map to $context
17 */
18 define('CONFIGURATION_DEFAULT_MAP', 2);
19
20 /**
21 * The third execution phase: Enable all modules needed for the configuration
22 */
23 define('CONFIGURATION_ENABLE_MODULES', 3);
24
25 /**
26 * The fourth execution phase: Apply all modules configuration maps
27 */
28 define('CONFIGURATION_MODULE_MAPS', 4);
29
30 /**
31 * The fifth execution phase: Execute the built up actions, one action per execution pass.
32 */
33 define('CONFIGURATION_EXECUTE_ACTIONS', 5);
34
35 /**
36 * The six execution phase: Disable any modules marked for disabling
37 */
38 define('CONFIGURATION_DISABLE_MODULES', 6);
39
40 /**
41 * The seventh and last execution phase: Any needed cleanup operations
42 */
43 define('CONFIGURATION_FINALIZE', 7);
44
45 /**
46 * An execution phase to mark the success and completion of the configuration
47 */
48 define('CONFIGURATION_SUCCESS', -1);
49
50 /**
51 * An execution phase to mark an error in the execution somewhere
52 */
53 define('CONFIGURATION_ERROR', 0);
54
55 /**
56 * The key that signals an attribute in the end-result parsed data
57 */
58 define('CONFIGURATION_ATTRIBUTE_KEY', '$');
59
60 /**
61 * Implementation of hook_perm()
62 */
63 function configuration_perm() {
64 return array('execute configurations');
65 }
66
67 /**
68 * Execute a configuration diven a data object of actions. The supplied
69 * data should be a php array in the correct format.
70 *
71 * Here is an example configuration to setup two blocks:
72 *
73 * array(
74 * array(
75 * tag => block
76 * config => array(
77 * description => My Custom Block
78 * region => right
79 * weight => -5
80 * )
81 * )
82 * array(
83 * tag => block
84 * config => array(
85 * id => locale-0
86 * region => left
87 * weight => 5
88 * )
89 * )
90 * )
91 *
92 * @param $data
93 * The dataset containing the configuration to execute
94 * @param $mode
95 * A string that will form a callback function used to dictate
96 * execution of the configuration. Two default modes are:
97 * - batch: Run the configuration on a batch/progress bar page
98 * - xmlrpc: Run the configuration through services without a
99 * break in the pages php execution
100 *
101 * The job of the callback function is to ensure that data
102 * set in the configuration_set_data static cache is maintained
103 * through any possible page re-loads or however the execution
104 * is processed so that each pass of configuration_execute_pass
105 * has the right data available to it.
106 * @return
107 * TRUE or FALSE for successful execution. The exact error in case
108 * of an unsuccessful run can be retrieved with configuration_get_error()
109 */
110 function configuration_execute($data, $mode = 'batch', $current_id = null) {
111 $args = func_get_args();
112 array_splice($args, 0, 3);
113
114 $function = 'configuration_execute_'. $mode;
115
116 if (!function_exists($function)) {
117 return FALSE;
118 }
119
120 // Create and set a unique id for this execution
121 if (!$current_id) {
122 $current_id = md5(mt_rand());
123 }
124
125 configuration_set_current_id($current_id);
126
127 // Set the current data so it can be used with this execution
128 configuration_set_data('data', $data);
129
130 // Set to the first phase to get things started
131 configuration_set_data('phase', CONFIGURATION_INTIALIZE);
132
133 return call_user_func_array($function, $args);
134 }
135
136 /**
137 * Run through a pass of the configuration process
138 *
139 * @return
140 * Return the current phase
141 */
142 function configuration_run_pass() {
143 $phases = array(CONFIGURATION_INTIALIZE, CONFIGURATION_DEFAULT_MAP, CONFIGURATION_ENABLE_MODULES, CONFIGURATION_MODULE_MAPS, CONFIGURATION_EXECUTE_ACTIONS, CONFIGURATION_DISABLE_MODULES, CONFIGURATION_FINALIZE);
144
145 $phase =& configuration_get_data('phase');
146 $context =& configuration_get_data('context');
147 $data =& configuration_get_data('data');
148
149 // The context object is required for every phase except the first
150 // Also, the original data object should always be a reference to the
151 // data in the context object which we can't guarantee unless we do it
152 // ourselves here.
153 if (is_object($context)) {
154 $data = &$context->item;
155 }
156 else if ($phase > CONFIGURATION_INTIALIZE) {
157 configuration_set_error('no_context');
158 $phase = CONFIGURATION_ERROR;
159 }
160
161 // Ensure our phase is valid
162 if (!in_array($phase, $phases)) {
163 configuration_set_error('invalid_phase', array('phase' => $phase));
164 }
165
166 // Make sure necessary include files are loaded
167 configuration_load_includes();
168
169 switch($phase) {
170 // TODO Decide if maybe we should run the first few phases all at once
171 case CONFIGURATION_INTIALIZE:
172 configuration_initiate();
173 $phase++;
174 break;
175 case CONFIGURATION_DEFAULT_MAP:
176 // First apply the base configuration map. This primary purpose
177 // of this map is to set appropriate attributes in the context
178 // object, make it look a bit nicer, and build the module list
179 configuration_apply_map($context, configuration_default_map());
180
181 if (!configuration_get_error()) {
182 $phase++;
183 }
184 break;
185 case CONFIGURATION_ENABLE_MODULES:
186 // Enable all modules set in the configuration
187 $modules =& configuration_get_data('enable modules');
188 if (!empty($modules)) {
189 configuration_enable_modules($modules);
190 }
191
192 if (!configuration_get_error()) {
193 $phase++;
194 }
195 break;
196 case CONFIGURATION_MODULE_MAPS:
197 // Apply all module configuration maps to the object to prepare
198 // the data, apply some basic validations, and setup data needed
199 // for the next configuration phases
200 configuration_apply_maps($context);
201
202 if (!configuration_get_error()) {
203 $phase++;
204 }
205 break;
206 case CONFIGURATION_EXECUTE_ACTIONS:
207 // Go through the built list of actions and build/run them one at a time
208 configuration_process_actions();
209
210 $left = count(configuration_get_data('actions'));
211 if ($phase != CONFIGURATION_ERROR && !$left) {
212 $phase++;
213 }
214 break;
215 case CONFIGURATION_DISABLE_MODULES:
216 // Disable modules
217 $modules =& configuration_get_data('disable modules');
218 if (!empty($modules)) {
219 configuration_disable_modules($modules);
220 }
221
222 if (!configuration_get_error()) {
223 $phase++;
224 }
225 break;
226 case CONFIGURATION_FINALIZE:
227 // The configuration succeed so do any necessary cleanup
228 configuration_finalize();
229 $phases++;
230 break;
231 case CONFIGURATION_SUCCESS:
232 // We are done. Nothing to do here.
233 break;
234 case CONFIGURATION_ERROR:
235 default:
236 // TODO Error handling
237 break;
238 }
239
240 // Any data can be retreived through configuration_get_data, but this can be
241 // a quick way of determining if there was an error
242 return $phase;
243 }
244
245 /**
246 * The "batch" method of executing a configuration
247 *
248 * The job of this method is to save and set the configuration data
249 * upon the start of each batch operation while passing it through
250 * the batch sandbox
251 */
252 function configuration_execute_batch() {
253 $batch = array(
254 'init_message' => t('Running action @current out of @total', array('@current' => 1, '@total' => 7)),
255 'progress_message' => t('Running action @current out of @total.'),
256 'operations' => array(),
257 );
258
259 for($i=1;$i<=7;$i++) {
260 $batch['operations'][] = array('_configuration_execute_batch', array());
261 }
262
263 batch_set($batch);
264
265 // Get the current batch so we can add the current configuration data
266 $batch =& batch_get();
267 $batch['sets'][0]['results']['configuration_data'] = configuration_get_data(null, true);
268
269 batch_process();
270 }
271
272 /**
273 * Batch callback function for batch execution
274 */
275 function _configuration_execute_batch(&$batch_context) {
276 // Set the configuration data
277 configuration_set_data(null, null, $batch_context['results']['configuration_data']);
278
279 // Bail on an error
280 if (configuration_get_error()) {
281 configuration_debug_batch(configuration_set_error());
282 }
283
284 // Start a timer. Since we want each action to be its own http request, we need
285 // to ensure the batch api will decide to do it like that by making each action
286 // take at least a second to execute
287 timer_start('configuration_action');
288
289 // Run the current pass
290 $phase = configuration_run_pass();
291
292 // Deal with custom stuff for the error and action phases
293 if ($phase == CONFIGURATION_EXECUTE_ACTIONS) {
294 $done = configuration_get_data('actions done');
295 $left = configuration_get_data('actions');
296 $done = count($done);
297 $left = count($left);
298
299 if ($done != $left) {
300 // Set the progress and message of the actions
301 $batch_context['finished'] = $done / ($done + $left);
302 $batch_context['message'] = t('@current out of @total', array('@current' => $done, '@total' => $done + $left));
303
304 // Sleep till 1000 (a second) if needed so we know we'll get a new page load
305 if (timer_read('configuration_action') < 1000) {
306 //@usleep(1000 - timer_read('configuration_action'));
307 }
308 }
309 }
310
311 // Make sure batch is updated with the updated configuration data store
312 $batch_context['results']['configuration_data'] = configuration_set_data();
313 }
314
315 /**
316 * Print debug data during a batch operation
317 */
318 function configuration_debug_batch($var = null) {
319 $var = print_r($var, true);
320 $var = str_replace(" ", '&nbsp;', $var);
321 $var = str_replace("\n", "<br />", $var);
322 print drupal_to_js(array('status' => 0, 'data' => $var));
323 exit;
324 }
325
326 /**
327 * Move through the actions list processing one per call
328 */
329 function configuration_process_actions() {
330 $actions =& configuration_get_data('actions');
331 $done =& configuration_get_data('actions done');
332
333 if (empty($actions) || !isset($actions[0])) {
334 return true;
335 }
336
337 // Get the next action
338 $action = $actions[0];
339 array_shift($actions);
340
341 // If the action here is not set, call the callback to set them
342 if (!isset($action[0]) && ($callback = $action[1]->action_callback) && function_exists($callback)) {
343 $value = $callback($action[1]->item, $action[1]);
344
345 if (!configuration_get_error() && !isset($value)) {
346 $done[] = $action;
347 return true;
348 }
349
350 if (is_scalar($value)) {
351 $value = array($value);
352 }
353
354 // Insert the action(s) here and move out so the next calls can handle them
355 $context = &$action[1];
356 for($i=count($value)-1;isset($value[$i]);$i--) {
357 $new = array($value[$i], &$context);
358 array_unshift($actions, $new);
359 }
360
361 $done[] = $action;
362 return true;
363 }
364 else if (!isset($action[0])){
365 configuration_set_error('missing action', array('!context' => $action[1]));
366 }
367
368 // Process this action
369 if (!configuration_get_error() && $action[0]) {
370 configuration_process_action($action);
371 }
372
373 // Mark this action as complete if no errors
374 if (!configuration_get_error()) {
375 $done[] = $action;
376 }
377 else {
378 // Put the action back on if there was an error. Maybe the error
379 // can be corrected and tried again
380 array_unshift($actions, $action);
381 }
382 }
383
384 /**
385 * Execute a single action
386 */
387 function configuration_process_action($action) {
388 // Get the module list of actions and identifiers
389 $actions =& module_invoke_all('configuration_actions');
390 $identifiers =& configuration_get_data('identifiers');
391
392 $action_id = $action[0];
393 $action_info = $actions[$action_id];
394 $action_context = &$action[1];
395
396 if (!isset($action_info)) {
397 configuration_set_error('invalid action id', array('!action' => $action_id));
398 return false;
399 }
400
401 // Replace identifiers and tokens in the action data
402 configuration_replace_tokens($action_context, $identifiers);
403
404 // Unlikely, but if there is an error replacing tokens
405 if (configuration_get_error()) {
406 return false;
407 }
408
409 // With proper data and values replaced, apply the actions build map. This is a great
410 // place to do any validation as well (using a #validate callback property or directly)
411 // TODO Implement #validate property...
412 if ($action_info['#build map']) {
413 configuration_apply_map($action_context, $action_info['#build map']);
414 }
415
416 if (configuration_get_error()) {
417 configuration_set_error('action build map', array('!action' => $action_id));
418 }
419
420 // Build the execute $args array. Starting with the actual form data
421 //if ($action_context->build_data) {
422 if ($action_info['#build data'] && ($matches = configuration_fetch($action_info['#build data'], $action_context))) {
423 $action_data = $matches[0]->item;
424 }
425 else {
426 $action_data = $action_context->item;
427 }
428
429 // Then the form state
430 if ($action_context->form_state) {
431 $action_state = &$action_context->form_state;
432 $action_state['values'] = &$action_data;
433 }
434 else {
435 $action_state = array(
436 'storage' => null,
437 'submitted' => false,
438 'values' => &$action_data
439 );
440 }
441 // Then the params
442 for($i=0;isset($action_info['#parameters'][$i]);$i++) {
443 $matches = configuration_fetch($action_info['#parameters'][$i], $action_context);
444 // Ensure that the parameter is at least set
445 $params[$i] = null;
446 if (isset($matches[0])) {
447 $params[$i] = $matches[0]->item;
448 }
449 }
450
451 // Drupal Execute
452 configuration_drupal_execute($action_id, $action_state, $params);
453
454 // Store any form errors
455 if ($form_errors = form_set_error()) {
456 configuration_set_error('form error', $form_errors);
457 }
458 // Run an on-success map and save any identifiers
459 else {
460 if ($action_info['#success map']) {
461 configuration_apply_map($action_context, $action_info['#success map']);
462 }
463 // TODO Replace identifiers before running action
464 if (!empty($action_info['#identifiers'])) {
465 for($i=0;isset($action_info['#identifiers'][$i]);$i++) {
466 $matches = configuration_fetch($action_info['#identifiers'][$i], $action_context);
467
468 // TODO Should we set an error if count($matches) > 1 ?
469 if (!empty($matches)) {
470 $action_context->identifiers[$i] = $matches[0]->item;
471
472 // Make an id=>identifier map for easier access
473 if ($action_context->id) {
474 $identifiers[$action_context->id] = &$action_context->identifiers[$i];
475 }
476 }
477 }
478 }
479 }
480
481 // Cleanup items
482 unset($action_context->build_data, $action_context->form_state);
483
484 if ($action_info['#cleanup map']) {
485 configuration_apply_map($action_context, $action_info['#cleanup map']);
486 }
487 }
488
489 /**
490 * Apply all module configuration maps to the data while
491 * building the action queue at the same time
492 *
493 * @param $context
494 * The context array needing the config maps applied to
495 * @return
496 * An array of actions referencing points in the $context object for
497 * later use in action execution
498 */
499 function configuration_apply_maps(&$context) {
500 // Get all the module maps and apply each one to the context object
501 foreach(module_implements('configuration_maps') as $module) {
502 $map = module_invoke($module, 'configuration_maps');
503 configuration_apply_map($context, $map);
504 }
505 }
506
507 /**
508 * Apply an individual configuration map to the data set object
509 *
510 * @param $context
511 * The context array for which to apply the map
512 * @param $map
513 * The configuration map to apply to the context array
514 * @return
515 * An array of referenced actions that resulted from this configuration map
516 */
517 function configuration_apply_map(&$context, $map) {
518 foreach($map as $path => &$config) {
519 // TODO Make $path available in sub-functions for error reporting
520 // Get the matches for this path
521 $matches = configuration_fetch($path, $context, TRUE);
522
523 while(!empty($matches)) {
524 // Get and drop off the first match from the matches array
525 $match = &$matches[0];
526 array_shift($matches);
527
528 // If this match is null (was unset somewhere) skip it
529 if (!isset($match)) {
530 continue;
531 }
532
533 // Setup a list of properties to process in the right order
534 $properties = array_keys(array_intersect_key(configuration_map_properties(), $config));
535
536 // Loop through the properties list processing one at a time
537 foreach($properties as $property) {
538 $value = $config[$property];
539
540 // Process this property
541 configuration_context_process_property($property, $value, $match, $config);
542
543 // Unset this property as we are done with it
544 unset($properties[$property]);
545 }
546 }
547 }
548 }
549
550 /**
551 * The default configuration map that is applied to all configuration
552 * context objects. For the time being all it does is make it easier
553 * to visualize the incoming configuration data and set up the module
554 * list.
555 *
556 * The original format of the incoming data is like: array(
557 * array( type => block, config => array( // config data ) )
558 * array( type => block, config => array( // config data ) )
559 * array( type => menu, config => array( // config data ) )
560 * )
561 *
562 * Applying a config path against an object like that would look like:
563 * /*[type=block]/config
564 *
565 * When in realitiy all that should be needed is:
566 * /block
567 *
568 * This is accomplished here by moving the config data a level above it
569 * and getting rid of the tag key from the data object completely. The
570 * tag key is set in the context object and is made to act as a secondary
571 * array key (the original is just an indexed number).
572 *
573 * The new configuration looks like: array(
574 * array( array( // config data ) ) // secondary_key=block
575 * array( array( // config data ) ) // secondary_key=block
576 * array( array( // config data ) ) // secondary_key=menu
577 * )
578 */
579 function configuration_default_map() {
580 return array(
581 '//*' => array('#value callback' => 'configuration_find_attributes'), // Convert tags starting with a # to attributes
582 '/*/tag' => array('#required' => true, '#context key' => true),
583 '/*/config' => array('#move' => '..'),
584 '/module' => array('#array' => true), // TODO This is temporary since the next item can't catch these matches after the #alias yet
585 '/modules' => array('#alias' => array('module'), '#array' => true),
586 '/modules/*' => array('#enable module' => true)
587 );
588 }
589
590 /**
591 * Return a list of supported config properties in the order they
592 * should be processed with their descriptions
593 *
594 * TODO Remove descriptions here as they could just be in documentation somewhere.
595 * TODO Allow modules to specify custom properties as well that would get sent to custom callbacks. Or add a #utility type property that handles a variety of functionality including module custom functionality. Such as a 'split' utitlity or in block modules case a custom function to split a module ID (block-3) into module=block and delta=3.
596 */
597 function configuration_map_properties() {
598 return array(
599 '#include' => t('Include/require a file during the build process.'),
600 '#alias' => t('Allow a tag to have aliases for better readability.'),
601 '#default callback' => t('Set the default value if the tag is missing via a callback'),
602 '#default php' => t('Set the default value if the tag is missing via a php snippet'),
603 '#default path' => t('Set the default value if the tag is missing via a path to another item.'),
604 '#default value' => t('Set the default value if the tag is missing.'),
605 '#default' => t('Same as #default value'),
606 '#create' => t('Create the tag under this tag if it does not exist. Either the name of the tag can be set or both the name and the value like: array( name => value )'),
607 '#key' => t('The name of the tag will change to this when processed.'),
608 '#key callback' => t('The tag will change when processed to the result of the callback'),
609 '#key path' => t('The tag will change when processed to the value of the tag at the specified path in the data object. In case the path does not exist a default value can be specified by setting an array like: array( path => default )'),
610 '#key php' => t('The tag will change when processed to the value of the evaluted php.'),
611 '#array' => t('Mark that this data must be an array and make it so if it is not.'),
612 '#assoc' => t('Mark that this data must be an associative array. We cannot make it one if it is not.'),
613 '#move' => t('Move the tag to a different location specified by a path here. The end of the path will replace the current key.'),
614 '#copy' => t('Copy this tag to another location specified by a path here.'),
615 '#required' => t('Specify that this tag is required and must be specified.'),
616 '#context key' => t('Act as a secondary_key for that data object and will be matched against config paths.'),
617 '#value alias' => t('A key=>list array that determines possible aliases of certain values'),
618 '#value callback' => t('Apply a callback on a value. The returned value will become the new value.'),
619 '#value php' => t('Run a php snippet on the value here. The returned value will become the new value.'),
620 '#value path' => t('Set the value of this item to the item specified with the path.'),
621 '#value' => t('Set the value to what is specified here.'),
622 '#id callback' => t('Run a callback that returns an identifier for the action.'),
623 '#id php' => t('Evaluate php that returns an identifier for the action.'),
624 '#options' => t('An array of possible values for the tag. It must be one of them.'),
625 '#options callback' => t('An array of possible values for the tag as returned from the callback.'),
626 '#build callback' => t('The return of this callback will be used as the input data for the execution and will not replace the data in the $context object.'),
627 '#build php' => t('Use a php snippet to get the build data instead of the callback.'),
628 '#action' => t('Specify an action to be called on the corresponding data. This will be saved and run at the end of the execution process.'),
629 '#action callback' => t('A callback that will specify the action to be run here. This callback is run after previous actions are executed.'),
630 '#attribute' => t('A boolean value specifying wether this tag/value pair is an attribute. Child tags are not allowed with attributes. Attributes stay in the context object, but not in the actual data set.'),
631 '#enable module' => t('Add the module here to the enable modules list.'),
632 '#disable module' => t('Add the module here to the disable modules list.'),
633 '#delete' => t('Remove the key=>value from the object completely.')
634 );
635 }
636
637 /**
638 * Process a single mapping config property. Various properties will mark static data
639 * that will be processed later on in the configuration phases.
640 *
641 * @param $property
642 * The property needing processed
643 * @param $value
644 * The value of the property
645 * @param $context
646 * The context area of the data to process with this property
647 * @param $config
648 * The config this property=>value pair is a part of
649 */
650 function configuration_context_process_property($property, $value, &$context, &$config = array()) {
651 $empty = $context->empty;
652
653 // This is a list of properties that can take an empty match
654 $empty_properties = array('#alias', '#required', '#create', '#default', '#default callback', '#default php', '#default path', '#default value');
655
656 // This is a list of properties that do not modify the context object
657 // which will save any integrity checks later on
658 $no_modify_propertiers = array('#include', '#action', '#action callback', '#required', '#options', '#options callback');
659
660
661 // If this is an empty context and not in the empty property list, return immediately
662 if ($empty && !in_array($property, $empty_properties)) {
663 return;
664 }
665
666 switch ($property) {
667 case '#include':
668 $file = './'. $value;
669 if (!is_file($file)) {
670 configuration_set_error('pre-validate: include', array('!file' => $file));
671 }
672 else {
673 @require_once $file;
674 $context->include = $file;
675 }
676 break;
677 case '#alias':
678 // TODO Figure out how to deal with the $matches array syncing with new matches here
679 if ($empty) {
680 if (is_scalar($value)) {
681 $value = array($value);
682 }
683
684 foreach($value as $alias) {
685 // Find the alias, if it exists rename it
686 if (array_key_exists($alias, $context->parent->item)) {
687 // Find the alias context
688 for($i=0;isset($context->parent->children[$i]);$i++) {
689 $change = &$context->parent->children[$i];
690 if ($change->key == $alias) {
691 configuration_context_process_property('#key', $context->key, $change, $config);
692 break(2);
693 }
694 }
695 }
696 }
697 }
698
699 // Deal with secondary keys here. No need to check if it already exists
700 // since they can exist multiple times
701 for($i=0;isset($context->parent->children[$i]);$i++) {
702 $check=&$context->parent->children[$i];
703 if (($pos = in_array($check->secondary_key, $value)) !== false) {
704 $check->secondary_key = $context->secondary_key;
705 }
706 }
707 break;
708 case '#default callback':
709 if ($empty) {
710 if (is_array($value)) {
711 $args = reset($value);
712 $value = key($value);
713 }
714 if (function_exists($value)) {
715 if (isset($args)) {
716 $context->parent->item[$context->key] = call_user_func_array($value, $args);
717 }
718 else {
719 $context->parent->item[$context->key] = $value($context);
720 }
721 $context->empty = false;
722 }
723 }
724 break;
725 case '#default php':
726 if ($empty) {
727 // Make sure it is set and then get the default and reset it to that
728 $context->parent->item[$context->key] = null;
729 $php = $value;
730 $value = &$context->parent->item[$context->key];
731 $new = eval($php);
732 if (isset($new)) {
733 $context->parent->item[$context->key] = $new;
734 }
735 $context->empty = false;
736 }
737 break;
738 case '#default path':
739 if ($empty && ($matches = configuration_fetch($value, $context)) && count($matches) == 1) {
740 $context->parent->item[$context->key] = $matches[0]->item;
741 $context->empty = false;
742 }
743 break;
744 case '#default':
745 case '#default value':
746 case '#create':
747 if ($empty) {
748 // TODO Think about converting all other context keys to ->_ like ->_parent ->_item
749 if ($context->_attribute) {
750 $context->item = $value;
751 }
752 else {
753 $context->parent->item[$context->key] = $value;
754 $context->item = &$context->parent->item[$context->key];
755 }
756 $context->empty = false;
757 }
758 break;
759 case '#key callback':
760 if (function_exists($value)) {
761 $value = $value($context->item);
762 }
763 else {
764 configuration_set_error(t('An invalid callback was specified: !callback', array('!callback' => $value)));
765 break;
766 }
767 case '#key path':
768 if ($property == '#key path') {
769 if (is_array($value)) {
770 $path = key($value);
771 $default = current($value);
772 }
773 else {
774 $path = $value;
775 }
776 $match = configuration_fetch($path, $context);
777
778 if (count($match) == 1 && is_scalar($match[0]->item)) {
779 $value = $match[0]->item;
780 }
781 else if ($config['#key default']) {
782 $value = $config['#key default'];
783 }
784 else {
785 break;
786 }
787 }
788 case '#key php':
789 if ($property == '#key php') {
790 $value = eval($value);
791 }
792 break;
793 case '#key':
794 if (array_key_exists($value, $context->parent->item)) {
795 // Key exists already
796 // TODO Is it a problem that the key exists already? Quite possibly....
797 break;
798 }
799
800 $key = $context->key;
801 $context->parent->item[$value] = &$context->item;
802
803 // We need to keep our current context object. check_context should fix the trace paths below
804 $context->key = $value;
805
806 // A plain unset is sufficient even with indexed arrays because check_context will pick it up and fix
807 unset($context->parent->item[$key]);
808 break;
809 case '#array':
810 // TODO Figure out how to deal with associative vs indexed arrays
811 if (!is_array($context->item) || !array_key_exists(0, $context->item)) {
812 if (is_null($context->item) || (is_array($context->item && empty($context->item)))) {
813 $context->item = array();
814 }
815 else {
816 $context->item = array($context->item);
817 }
818 }
819 break;
820 case '#assoc':
821 if (!is_array($value) || array_key_exists(0, $context->item)) {
822 configuration_set_error('assoc array');
823 }
824 break;
825 // TODO Test both copy and move
826 case '#copy':
827 $destinations = configuration_fetch($value, $context);
828
829 foreach($destinations as &$dest) {
830 // TODO We are making it impossible to overwrite an existing item. Maybe we should make it possible? This applies to #move too
831 if ($dest->empty) {
832 $dest->item = $context->item;
833 }
834 }
835 break;
836 case '#move':
837 // TODO Moving/Copying to the same array level as the current item can cause problems with the current matches set. Maybe make it impossible to do so or think of a solution.
838 $dest = configuration_fetch($value, $context);
839
840 if (count($dest) == 1 && $dest = &$dest[0]) {
841 $parent = &$context->parent;
842 $key = $context->key;
843
844 // Move the necessary dest info over here
845 foreach(array_diff(array_keys((array) $dest), array('children', 'item')) as $key) {
846 // TODO Is there room to do better memory cleanup here or does it not matter?
847 $context->$key = &$dest->$key;
848 }
849
850 // Make sure the old child pointer points to the new context
851 for($i=0;isset($context->parent->children[$i]);$i++) {
852 $check = &$context->parent->children[$i];
853 if ($check === $dest) {
854 $context->parent->children[$i] = &$context;
855 break;
856 }
857 }
858
859 // Make sure the moved data exists in the right place of the original array
860 $context->parent->item[$context->key] = &$context->item;
861
862 // Remove the item here. check_context will make sure contexts are removed
863 // We can't remove if we moved to this items parent
864 // TODO Make it work so you can move two parents up
865 if ($parent !== $dest) {
866 unset($parent->item[$key]);
867 }
868
869 // Fix all the children trace paths
870 configuration_fix_context_trace($context);
871 }
872 case '#required':
873 if ($context->empty) {
874 $path = implode('/', array_merge($context->trace, array($context->key)));
875 configuration_set_error('pre-validate: required', array('!tag' => $path));
876 }
877 else {
878 $context->required = true;
879 }
880 break;
881 case '#context key':
882 if (is_scalar($context->item)) {
883 $context->parent->secondary_key = $context->item;
884 }
885 break;
886 case '#attribute':
887 if (!is_scalar($context->item)) {
888 $path = implode('/', $context->trace);
889 configuration_set_error('pre-validate: attribute', array('!tag' => $path));
890 }
891 else {
892 if ($context->key[0] == CONFIGURATION_ATTRIBUTE_KEY) {
893 $key = substr($context->key, 1);
894 }
895 else {
896 $key = $context->key;
897 }
898 if (!configuration_reserved_attribute($key)) {
899 $context->parent->{$key} = $context->item;
900 unset($context->parent->item[$context->key]);
901 }
902 }
903 break;
904 case '#options callback':
905 if (function_exists($value)) {
906 $value = $value($context);
907 }
908 case '#options':
909 if (is_scalar($value)) {
910 $value = array($value);
911 }
912
913 if (!in_array($context->item, $value)) {
914 if (!($context->item === '' && !$context->required)) {
915 $path = implode('/', $context->trace);
916 configuration_set_error('pre-validate: options', array('!path' => $path, '!options' => implode(', ', $value)));
917 break;
918 }
919 }
920 case '#value alias':
921 foreach($value as $real => $aliases) {
922 foreach($aliases as $alias) {
923 if ($context->item == $alias) {
924 $context->item = $real;
925 break(2);
926 }
927 }
928 }
929 break;
930 case '#value callback':
931 if (function_exists($value)) {
932 $value($context->item, $context);
933 }
934 else {
935 configuration_set_error('value callback', array('@callback' => $value));
936 }
937 break;
938 case '#value php':
939 $php = $value;
940 $value = &$context->item;
941 $new = eval($php);
942 if (isset($new)) {
943 $context->item = $new;
944 }
945 break;
946 case '#value path':
947 if (($matches = configuration_fetch($value, $context)) && count($matches) == 1) {
948 $context->item = $matches[0]->item;
949 }
950 break;
951 case '#value':
952 $context->item = $value;
953 break;
954 case '#id callback':
955 if (function_exists($value)) {
956 $identifiers =& configuration_get_data('identifiers');
957 }
958 break;
959 case '#id php':
960
961 break;
962 case '#id path':
963
964 break;
965 // Not used/supported.
966 // TODO Remove?
967 case '#build callback':
968 if (function_exists($value)) {
969 $context->build_data = $value($context);
970 }
971 break;
972 case '#build php':
973 $context->build_data = eval($value);
974 break;
975 case '#action':
976 if (!is_array($value)) {
977 $value = array($value);
978 }
979
980 $actions =& configuration_get_data('actions');
981 foreach($value as $action) {
982 $action[] = array($action, null, &$context);
983 }
984 break;
985 case '#action callback':
986 if (function_exists($value)) {
987 $actions =& configuration_get_data('actions');
988 $context->action_callback = $value;
989 $actions[] = array(null, &$context);
990 }
991 else {
992 configuration_set_error('missing action callback', array('@callback' => $value));
993 }
994 break;
995 case '#enable module':
996 if (is_scalar($context->item)) {
997 $enable =& configuration_get_data('enable modules');
998 $enable[] = $context->item;
999 }
1000 break;
1001 case '#disable module':
1002 if (is_scalar($context->item)) {
1003 $disable =& configuration_get_data('disable modules');
1004 $disable[] = $context->item;
1005 }
1006 break;
1007 case '#delete':
1008 unset($context->parent->item[$context->key]);
1009 break;
1010 }
1011
1012 // Update the context object to reflect any possible new changes
1013 if (!in_array($property, $no_modify_propertiers)) {
1014 configuration_check_context($context);
1015 }
1016 }
1017
1018 /**
1019 * Do the actual drupal execute on an action
1020 */
1021 function configuration_drupal_execute($form_id, &$form_state, $params) {
1022 // Make sure we always have a clear cache for everything
1023 $result = db_query('SHOW TABLES LIKE "cache_%"');
1024
1025 while ($table = db_fetch_array($result)) {
1026 $table = current($table);
1027 cache_clear_all(null, $table);
1028 }
1029
1030 // Perform direct and automatic manipulation of the configuration data
1031 // from the default fapi form object
1032 configuration_sync_data_form($form_state['values'], $form_id, $form_state, $params);
1033
1034 $args = array($form_id, &$form_state);
1035
1036 if (is_array($params)) {
1037 $args = array_merge($args, $params);
1038 }
1039
1040 configuration_set_data('executing', true);
1041
1042 // If we are in batch mode, trick the form api to think
1043 // otherwise to avoid potential problems
1044 $batch =& batch_get();
1045 $batch_clone = $batch;
1046 $batch = null;
1047
1048 // drupal_execute fails to keep $form_state in-sync through the
1049 // whole FAPI process. Issue http://drupal.org/node/346244
1050 //$return = call_user_func_array('drupal_execute', $args);
1051
1052 // Copy of drupal_execute until above issue is fixed
1053 $form = call_user_func_array('drupal_retrieve_form', $args);
1054
1055 $form['#post'] = $form_state['values'];
1056 drupal_prepare_form($form_id, $form, $form_state);
1057
1058 // If you call drupal_validate_form() on the same form more
1059 // than once per page request, validation is not performed
1060 // on any but the first call.
1061 // see issue: http://drupal.org/node/260934
1062 // drupal_process_form($form_id, $form, $form_state);
1063
1064 // Until above issue is fixed we use our own implementation
1065 // of drupal_process_form() and drupal_validate_form().
1066 _configuration_process_form($form_id, $form, $form_state);
1067
1068 configuration_set_data('executing', true);
1069 $batch = $batch_clone;
1070 }
1071
1072 /**
1073 * Based on a form api array, make automatic modifications
1074 * to the form submission data.
1075 *
1076 * What this can do:
1077 *
1078 * 1. Check all #options and try and match the data to them. Example of why this is needed:
1079 *
1080 * XML syntax
1081 * <types>page</types>
1082 * OR
1083 * <types>
1084 * <type>page</type>
1085 * <type>story</type>
1086 * </types>
1087 *
1088 * Original Results
1089 * array(
1090 * 'types' => 'page'
1091 * )
1092 * OR
1093 * array(
1094 * 'types' => array(
1095 * 'type' => array('page', 'store')
1096 * )
1097 * )
1098 *
1099 * Desired Results
1100 * array(
1101 * 'types' => array(
1102 * 'page' => 'page'
1103 * )
1104 * )
1105 * OR
1106 * array(
1107 * 'types' => array(
1108 * 'page' => 'page',
1109 * 'story' => 'story'
1110 * )
1111 * )
1112 *
1113 * However in some cases the first example (<types>page</types>) would be in the form <type>page</type> without
1114 * the <types> parent and no need for the associative array. It would just be array('type' => 'page').
1115 *
1116 * Based off of the form api #options, #tree, and #multiple values, it should be possible to convert
1117 * any of the above xml objects to the right form api object.
1118 */
1119 function configuration_sync_data_form(&$data, $form_id, $form_state, $params = null) {
1120 unset($form_state['values']);
1121 $args = array($form_id, &$form_state);
1122 if (is_array($params)) {
1123 $args = array_merge($args, $params);
1124 }
1125
1126 // Get the fully built fapi object
1127 $form = call_user_func_array('drupal_retrieve_form', $args);
1128 drupal_prepare_form($form_id, $form, $form_state);
1129 $form = form_builder($form_id, $form, $form_state);
1130
1131 $context = configuration_build_context($data);
1132 $form = configuration_build_context($form);
1133
1134 // TODO Should we remove build data that does not exist in the form object?
1135 if ($form_matches = configuration_fetch('//#options', $form)) {
1136 for($i=0;isset($form_matches[$i]);$i++) {
1137 $form_match = &$form_matches[$i];
1138
1139 // Check if we have this data
1140 $path = '/'. implode('/', $form_matches[$i]->parent->item['#parents']);
1141 if ($data_matches = configuration_fetch($path, $context)) {
1142 // Possibly manipulate our data to match these #options
1143 for($j=0;isset($data_matches[$j]);$j++) {
1144 $data_match = &$data_matches[$j];
1145
1146 // Determine if this is a singular/scalar or arrayed element
1147 $scalar = false;
1148 if (!$form_match->parent->item['#tree'] || (isset($form_match->parent->item['#multiple']) && !$form_match->parent->item['#multiple'])) {
1149 $scalar = true;
1150 }
1151 // Setup the new value
1152 // TODO Actually implement the update vs overwrite functionality as opposed to the 100% update (true)
1153 if (true && !$scalar && !empty($form_match->parent->item['#default_value'])) {
1154 // Default values are not key=>key arrays like the post values. They are
1155 // indexed arrays. The below should catch an indexed array as well as a singular
1156 // scalar default value and turn it into a key=>key array
1157 $values = (array)$form_match->parent->item['#default_value'];
1158 $values = array_combine($values, $values);
1159 }
1160 else {
1161 $values = array();
1162 }
1163 foreach((array)$data_match->item as $value) {
1164 // If the supplied value matches an options label, use that option
1165 if ($key = array_search($value, $form_match->item)) {
1166 $values[$key] = $key;
1167 }
1168 // Or if the supplied value is an actual options value (not the label) use that
1169 else if (array_key_exists($value, $form_match->item)) {
1170 $values[$value] = $value;
1171 }
1172 }
1173
1174 // If we are not on a multiple/tree form, the value should be singular/not an array
1175 if ($scalar) {
1176 $data_match->item = reset($values);
1177 }
1178 else {
1179 $data_match->item = $values;
1180 }
1181 }
1182 }
1183 }
1184 }
1185 }
1186
1187 /**
1188 * Set the unique execution identifier for this configuration
1189 *
1190 * @param $id
1191 * The unique identifier for this execution run
1192 * @return
1193 * The currently set identifier
1194 */
1195 function configuration_set_current_id($id = null) {
1196 static $current_id;
1197
1198 if ($id) {
1199 $current_id = $id;
1200 }
1201
1202 return $current_id;
1203 }
1204
1205 /**
1206 * Get the current configuration execution identifier
1207 */
1208 function configuration_get_current_id() {
1209 return configuration_set_current_id();
1210 }
1211
1212 /**
1213 * Set an error during configuration setup
1214 *
1215 * @param $type
1216 * The type of error being set.
1217 * @param $vars
1218 * Any variables that are associated with the particular error
1219 * @param $cache
1220 * This will set the static $errors cache
1221 * @return
1222 * If $type is set, the error(s) for that type are returned. If not the entire
1223 * static cache is returned.
1224 * //TODO Add a param to mark critical vs warning errors?
1225 */
1226 function configuration_set_error($type = null, $vars = array()) {
1227 // Use the configuration data store instead of our own static cache here
1228 $errors =& configuration_set_data('errors');
1229
1230 if ($type) {
1231 $errors[$type][] = $vars;
1232 }
1233
1234 if ($type) {
1235 return $errors[$type];
1236 }
1237 else {
1238 return $errors;
1239 }
1240 }
1241
1242 /**
1243 * Get any errors that have happened during configuration
1244 *
1245 * @param $type
1246 * The type of error being checked
1247 * @return
1248 * The variable data stored for that particular error, if there is an error
1249 */
1250 function configuration_get_error($type = null, &$vars = null) {
1251 $vars = configuration_set_error($type);
1252
1253 // TODO Problem here
1254 if (isset($vars)) {
1255 return true;
1256 }
1257 else {
1258 return false;
1259 }
1260 }
1261
1262 /**
1263 * Set static data to be retrieved during later phases
1264 *
1265 * @param $type
1266 * The type of data being stored, such as 'phase', 'build', 'action', 'cleanup' etc...
1267 * @param $data
1268 * The actual data needing stored
1269 * @param $cache
1270 * Set the full static $store of data with this $cache