$module_name));
$form_state['storage']['info'] = array(
'name' => $module->info['name'],
'filename' => $module_name,
'description' => $module->info['description'],
);
$form_state['storage']['export_final'] = features_populate($module->info['feature'], $module_name);
}
// we are coming in without a step, so default to step 1
else {
$step =
$form_state['storage']['step'] = empty($form_state['storage']['step']) ? 0 : $form_state['storage']['step'];
}
$form = array();
$form['step'] = array(
'#type' => 'item',
'#value' => "{$steps[$step]}",
);
switch ($step) {
// Provide additional information =================================
case 0:
$form['info'] = array(
'#tree' => TRUE,
);
$form['info']['name'] = array(
'#title' => t('Name'),
'#description' => t('Provide a name for your feature.'),
'#type' => 'textfield',
'#required' => TRUE,
'#default_value' => '',
);
$form['info']['filename'] = array(
'#title' => t('Name (machine-readable)'),
'#description' => t('Provide a machine-readable name for your feature. This may only contain lowercase letters, numbers and underscores. It should also avoid conflicting with the names of any existing Drupal modules.'),
'#type' => 'textfield',
'#required' => TRUE,
'#default_value' => '',
);
$form['info']['description'] = array(
'#title' => t('Description'),
'#description' => t('Provide a description for your feature.'),
'#type' => 'textfield',
'#required' => TRUE,
'#default_value' => '',
);
break;
// Choose a context ===============================================
case 1:
// Collect enabled contexts into tree'd array.
$options = array();
$contexts = context_enabled_contexts();
foreach ($contexts as $identifier => $context) {
if (!isset($options["{$context->namespace}-{$context->attribute}"])) {
$options["{$context->namespace}-{$context->attribute}"] = array();
}
$options["{$context->namespace}-{$context->attribute}"][$identifier] = $context->value;
}
$form['contexts'] = array(
'#title' => t('Contexts'),
'#description' => t('Choose contexts to build your feature from'),
'#type' => 'select',
'#multiple' => TRUE,
'#size' => 6,
'#options' => $options,
);
break;
// Confirm components =============================================
case 2:
$form['#theme'] = 'features_export_form_confirm';
$form['detected'] =
$form['added'] = array('#tree' => TRUE);
$export = $form_state['storage']['export'];
foreach ($export['conflicts'] as $type => $messages) {
foreach ($messages as $msg) {
drupal_set_message($msg, $type);
}
}
// Display each set of components and options for adding to the components
foreach ($export['items'] as $module_name => $items) {
$module = features_get_modules($module_name);
$form['detected'][$module_name] = array(
'#type' => 'markup',
'#title' => $module->info['name'],
'#value' => theme('item_list', array_keys($items)),
);
$options = features_export_options($module_name);
if (!empty($options)) {
$options = array_diff_key($options, $items);
$form['added'][$module_name] = array(
'#type' => 'checkboxes',
'#options' => $options,
);
}
else {
$form['added'][$module_name] = array(
'#type' => 'markup',
'#value' => "". t('This module does not support any options.') ."",
);
}
}
// Dependencies
$dependencies = $export['dependencies'];
$form['detected']['dependencies'] = array(
'#type' => 'markup',
'#title' => t('Module dependencies'),
'#value' => theme('item_list', array_keys($dependencies)),
);
$options = array();
foreach (features_get_modules() as $module_name => $info) {
if ($info->status && !empty($info->info)) {
$options[$module_name] = $info->info['name'];
}
}
$options = array_diff_key($options, $export['dependencies']);
$form['added']['dependencies'] = array(
'#tree' => TRUE,
'#type' => 'checkboxes',
'#options' => $options,
);
break;
// Download/export ================================================
case 3:
$info = $form_state['storage']['info'];
$export = $form_state['storage']['export_final'];
$form['#theme'] = 'features_export_form_final';
if ($code = features_export_render($export, $info)) {
$form['info'] = array(
'#description' => t('Copy and paste the following code into !module/!filename in your modules directory.', array('!module' => $info['filename'], '!filename' => "{$info['filename']}.info")),
'#title' => "{$info['filename']}.info",
'#type' => 'textarea',
'#rows' => 20,
'#default_value' => $code[0],
'#resizable' => FALSE,
);
$form['module'] = array(
'#description' => t('Copy and paste the following code into !module/!filename in your modules directory.', array('!module' => $info['filename'], '!filename' => "{$info['filename']}.module")),
'#title' => "{$info['filename']}.module",
'#type' => 'textarea',
'#rows' => 20,
'#default_value' => $code[1],
'#resizable' => FALSE,
);
}
break;
}
// Add Next/Prev step buttons
$form['buttons'] = array('#tree' => FALSE, '#theme' => 'features_form_buttons');
if ($step > 0) {
$form['buttons']['prev'] = array('#value' => t('Previous'), '#type' => 'submit');
}
if ($step < count($steps) - 1) {
$form['buttons']['next'] = array('#value' => t('Next'), '#type' => 'submit');
if (drupal_get_messages('error', FALSE)) {
$form['buttons']['message'] = array(
'#type' => 'markup',
'#value' => "
". t('You should resolve all errors with your feature before continuing.') ."
",
);
}
}
return $form;
}
/**
* Export form submit handler.
*/
function features_export_form_submit($form, &$form_state) {
// tell Drupal we are redrawing the same form
$form_state['rebuild'] = TRUE;
switch ($form_state['storage']['step']) {
// Step 0: Store info
case 0:
$form_state['storage']['info'] = $form_state['values']['info'];
break;
// Step 1: Convert sources into export object
case 1:
// Module name
$module_name = $form_state['storage']['info']['filename'];
// Retrieve export
$feature = array();
if (!empty($form_state['values']['contexts'])) {
foreach ($form_state['values']['contexts'] as $identifier) {
$feature['context'][] = $identifier;
}
}
$export = features_populate($feature, $module_name);
$form_state['storage']['export'] = $export;
break;
// Step 2: Update export object based on user input
case 2:
$export = $form_state['storage']['export'];
// Update export array based on what's been selected
foreach ($export['items'] as $module_name => $items) {
if (!empty($form_state['values']['added'][$module_name])) {
foreach ($form_state['values']['added'][$module_name] as $item => $value) {
if ($value) {
$export['items'][$module_name][$item] = $item;
}
else if (!empty($export['items'][$module_name][$item])) {
unset($export['items'][$module_name][$item]);
}
}
}
}
// Update dependencies
if (!empty($form_state['values']['added']['dependencies'])) {
foreach ($form_state['values']['added']['dependencies'] as $item => $value) {
if ($value) {
$export['dependencies'][$item] = $item;
}
else if (!empty($export['dependencies'][$item])) {
unset($export['dependencies'][$item]);
}
}
}
// Build final export array
$module_name = $form_state['storage']['info']['filename'];
$final = $export;
$final = array_merge($final, features_populate($export['items'], $module_name));
$final['dependencies'] = _features_export_minimize_dependencies($final['dependencies']);
$form_state['storage']['export_final'] = $final;
break;
}
// check the button that was clicked and action the step chagne
if ($form_state['clicked_button']['#id'] == 'edit-prev') {
$form_state['storage']['step']--;
}
elseif ($form_state['clicked_button']['#id'] == 'edit-next') {
$form_state['storage']['step']++;
}
}
/**
* @param $items
* @param $module_name
* @return
*/
function features_populate($items, $module_name) {
$stub = array('items' => array(), 'dependencies' => array(), 'conflicts' => array());
$export = _features_populate($items, $stub, $module_name);
$export['dependencies'] = _features_export_minimize_dependencies($export['dependencies'], $module_name);
return $export;
}
/**
* Iterate and descend into a feature definition to extract module
* dependencies and feature definition. Calls hook_features_export for modules
* that implement it.
*
* @param $pipe
* Associative of array of module => info-for-module
* @param $export
* Associative array of items, and module dependencies which define a feature.
* Passed by reference.
*
* @return fully populated $export array.
*/
function _features_populate($pipe, &$export, $module_name = '') {
foreach ($pipe as $module => $data) {
// Attempt to load inc file for the module, will fail silently if the file
// doesn't exist.
module_load_include('inc', 'features', "includes/features.$module");
if (module_hook($module, 'features_export')) {
$function = "{$module}_features_export";
// Pass module-specific data and export array (should be done by reference)
$more = $function($data, $export, $module_name);
// Allow for export functions to request additional exports.
if (!empty($more)) {
_features_populate($more, $export, $module_name);
}
}
}
return $export;
}
/**
* Iterates over a list of dependencies and kills modules that are
* captured by other modules 'higher up'.
*/
function _features_export_minimize_dependencies($dependencies, $module_name = '') {
// Ensure that the module doesn't depend upon itself
if (!empty($module_name) && !empty($dependencies[$module_name])) {
unset($dependencies[$module_name]);
}
foreach ($dependencies as $k => $v) {
if (empty($v)) {
unset($dependencies[$k]);
}
else {
$module = features_get_modules($v);
if ($module && !empty($module->info['dependencies'])) {
foreach ($module->info['dependencies'] as $dependency) {
if (!empty($dependencies[$dependency])) {
unset($dependencies[$dependency]);
}
}
}
}
}
return $dependencies;
}
/**
* Render feature export into a .info and .module file.
*
* @param $export
* An exported feature definition.
* @param $module_info
* Meta information about the module.
*
* @return array of info file and module file contents.
*/
function features_export_render($export, $module_info = array()) {
global $base_url;
// Standard dot-info file metadata first.
$info = array(
'name' => $module_info['name'],
'description' => $module_info['description'],
'core' => '6.x',
'package' => 'Features',
'#comment_dependencies' => '; Dependencies',
'dependencies' => array_keys($export['dependencies']),
'#comment_features' => '; Feature components',
'feature' => array(),
'#comment_version' => '; Feature information',
'feature_uri' => $base_url,
'feature_timestamp' => time(),
);
foreach ($export['items'] as $key => $item) {
$info['feature'][$key] = $item;
}
$info = features_export_info($info);
// Now the dot-module.
$code = array();
foreach ($export['items'] as $module => $data) {
if (!empty($data)) {
// Attempt to load inc file for the module, will fail silently if the file
// doesn't exist.
module_load_include('inc', 'features', "includes/features.$module");
if (module_hook($module, 'features_export_render')) {
$function = "{$module}_features_export_render";
$code[$module] = $function($module_info['filename'], $data);
}
}
}
$code = implode("\n\n", $code);
$code = "name])) {
// Make necessary inclusions
if (module_exists('views')) {
views_include('view');
}
// Rebuild feature from .info file description.
$export = features_populate($module->info['feature'], '');
// Render and run an export of the current state.
$module->info['filename'] = '_features_comparison_' . $module->name;
list($i, $m) = features_export_render($export, $module->info);
$m = substr_replace($m, '', strpos($m, "info['feature'] as $i => $data) {
if (isset($export_functions[$i])) {
// Call the eval'd function and collect results
$fname = $module->info['filename'] .'_'. $export_functions[$i];
if (function_exists($fname)) {
$current[$i] = call_user_func($fname);
}
// Call the existing in-code function and collect results
$fname = $module->name .'_'. $export_functions[$i];
if (function_exists($fname)) {
$default[$i] = call_user_func($fname);
}
// Compare, and push differences into the overrides array
if (isset($current[$i])) {
foreach ($current[$i] as $j => $k) {
// We serialize objects to eliminate different instantiations of the "same" object
if (is_object($current[$i][$j])) {
$different = serialize($current[$i][$j]) !== serialize($default[$i][$j]);
}
else {
$different = $current[$i][$j] !== $default[$i][$j];
}
if ($different) {
$overridden[$i] = array(
'default' => $default[$i][$j],
'current' => $current[$i][$j],
);
}
}
}
}
}
$cache[$module->name] = $overridden;
}
return $cache[$module->name];
}
/**
* Return an array of default functions
*/
function features_get_default_hooks() {
// Fake a registry of default functions for now.
return array(
'views' => 'views_default_views',
'imagecache' => 'imagecache_default_presets',
'node' => 'node_info',
'context' => 'context_default_contexts',
// The following are hooks provided by features.module as a temporary stopgap
// modules without exportables.
'content' => 'content_default_fields',
'menu' => 'menu_default_items',
);
}
/**
* Theme functions ====================================================
*/
/**
* Theme function for features_export_form (step 2)
*/
function theme_features_export_form_confirm($form) {
drupal_add_css(drupal_get_path('module', 'features') .'/features.css');
$output = drupal_render($form['step']);
$rows = array();
foreach (element_children($form['detected']) as $element) {
$row = array();
$row[] = "{$form['detected'][$element]['#title']}";
unset($form['detected'][$element]['#title']);
$row[] = drupal_render($form['detected'][$element]);
$row[] = drupal_render($form['added'][$element]);
$rows[] = $row;
}
$output .= theme('table', array('', t('Auto-detected components'), t('Select additional components')), $rows, array('class' => 'features-export'));
$output .= drupal_render($form);
return $output;
}
/**
* Theme function for features_export_form (step 3)
*/
function theme_features_export_form_final($form) {
drupal_add_css(drupal_get_path('module', 'features') .'/features.css');
$output = drupal_render($form['step']);
$header = array();
$header[] = $form['info']['#title'];
$header[] = $form['module']['#title'];
unset($form['info']['#title']);
unset($form['module']['#title']);
$rows = array();
$row = array();
$row[] = drupal_render($form['info']);
$row[] = drupal_render($form['module']);
$rows[] = $row;
$output .= theme('table', $header, $rows, array('class' => 'features-export features-export-final'));
$output .= drupal_render($form);
return $output;
}
/**
* Export var function -- from Views.
*/
function features_var_export($var, $prefix = '', $multiple = TRUE) {
if (is_array($var)) {
if (empty($var)) {
$output = 'array()';
}
else {
$output = "array(\n";
foreach ($var as $key => $value) {
$output .= " '$key' => ". context_var_export($value, $prefix . ($multiple? '' : ' ')) .",\n";
}
$output .= ')';
}
}
else if (is_bool($var)) {
$output = $var ? 'TRUE' : 'FALSE';
}
else {
$output = var_export($var, TRUE);
}
if ($prefix) {
$output = str_replace("\n", "\n$prefix", $output);
}
return $output;
}
/**
* Generate code friendly to the Drupal .info format from a structured array.
*
* @param $info
* An array of parameters to put in a module's .info file.
*
* @return
* A code string ready to be written to a module's .info file.
*/
function features_export_info($info) {
// Render the info array into a string
// @TODO: we should probably break this out.
$code = array();
foreach ($info as $k => $v) {
if (strpos($k, '#') === 0) {
$code[] = '';
$code[] = $v;
}
else {
if (is_array($v)) {
foreach ($v as $l => $m) {
if (is_array($m)) {
foreach ($m as $n => $o) {
$code[] = "{$k}[$l][] = \"{$o}\"";
}
}
else {
$code[] = "{$k}[] = \"{$m}\"";
}
}
}
else {
$code[] = "{$k} = \"{$v}\"";
}
}
}
$code = implode("\n", $code);
return $code;
}