array('class' => 'features-export-form'), '#feature' => isset($feature) ? $feature : NULL, ); $form['info'] = array( '#type' => 'fieldset', '#tree' => FALSE, ); $form['info']['name'] = array( '#title' => t('Name'), '#description' => t('Example: Image gallery'), '#type' => 'textfield', '#required' => TRUE, '#default_value' => !empty($feature->info['name']) ? $feature->info['name'] : '', '#attributes' => array('class' => 'feature-name'), ); $form['info']['project'] = array( '#type' => 'textfield', '#title' => t('Machine-readable name'), '#description' => t('Example: image_gallery'). '
' .t('May only contain lowercase letters, numbers and underscores. Try to avoid conflicts with the names of existing Drupal projects.'), '#required' => TRUE, '#default_value' => !empty($feature->name) ? $feature->name : '', '#attributes' => array('class' => 'feature-project'), ); $form['info']['description'] = array( '#title' => t('Description'), '#description' => t('Provide a short description of what users should expect when they enable your feature.'), '#type' => 'textfield', '#required' => TRUE, '#default_value' => !empty($feature->info['description']) ? $feature->info['description'] : '', ); $form['info']['version'] = array( '#title' => t('Version'), '#description' => t('Examples: 6.x-1.0, 6.x-1.0-beta1'), '#type' => 'textfield', '#required' => FALSE, '#default_value' => !empty($feature->info['version']) ? $feature->info['version'] : '', '#size' => 30, ); $form['info']['project_status_url'] = array( '#title' => t('URL of update XML'), '#description' => t('Example: http://mywebsite.com/fserver'), '#type' => 'textfield', '#required' => FALSE, '#default_value' => !empty($feature->info['project status url']) ? $feature->info['project status url'] : '', '#size' => 30, ); // User-selected feature source components. $components = array_keys(features_get_components()); $components[] = 'dependencies'; $form['export'] = array( '#type' => 'fieldset', '#tree' => FALSE, '#theme' => 'features_form_export', ); $form['export']['components'] = array( '#title' => t('Add components'), '#type' => 'select', '#options' => array('------'), '#attributes' => array('class' => 'features-select-components'), ); $form['export']['sources'] = array( '#tree' => TRUE, '#theme' => 'features_form_components', ); foreach ($components as $component) { // Tack on dependency options as they are not a feature component proper. if ($component == 'dependencies') { $required = drupal_required_modules(); $options = array(); foreach (features_get_modules() as $module_name => $info) { if (!in_array($module_name, $required) && $info->status && !empty($info->info)) { $options[$module_name] = $info->info['name']; } } $default_value = !empty($feature->info['dependencies']) ? $feature->info['dependencies'] : array(); } else { $options = features_invoke($component, 'features_export_options'); $default_value = !empty($feature->info['features'][$component]) ? $feature->info['features'][$component] : array(); } if ($options) { $form['export']['components']['#options'][$component] = $component; $form['export']['sources'][$component] = array( '#type' => 'checkboxes', '#options' => $options, '#title' => $component, '#default_value' => $default_value, '#ahah' => array( 'path' => 'admin/build/features/export/populate', 'wrapper' => 'features-export-populated', ), ); } } $form['export']['features'] = array( '#tree' => TRUE, '#type' => 'markup', '#prefix' => "
", '#suffix' => "
", '#value' => !empty($feature->info) ? theme('features_components', $feature->info, $feature->info['features'], array(), TRUE) : "
", ); $form['buttons'] = array('#theme' => 'features_form_buttons', '#tree' => FALSE); $form['buttons']['submit'] = array( '#type' => 'submit', '#value' => t('Download feature'), '#weight' => 10, '#submit' => array('features_export_build_form_submit'), ); return $form; } /** * Submit handler for features_export_form_build(). */ function features_export_build_form_submit($form, &$form_state) { // Assemble the combined component list $stub = array(); $components = array_keys(features_get_components()); foreach ($components as $component) { // User-selected components take precedence. if (!empty($form_state['values']['sources'][$component])) { $stub[$component] = array_filter($form_state['values']['sources'][$component]); } // Only fallback to an existing feature's values if there are no export options for the component. else if (!empty($form['#feature']->info['features'][$component])) { $stub[$component] = $form['#feature']->info['features'][$component]; } } // Generate populated feature $export = features_populate($stub, $form_state['values']['project']); // Directly copy the following attributes $attr = array('name', 'description', 'project', 'version'); foreach ($attr as $key) { $export[$key] = isset($form_state['values'][$key]) ? $form_state['values'][$key] : NULL; } if (!empty($form_state['values']['project_status_url'])) { $export['project status url'] = $form_state['values']['project_status_url']; } // Minimize detected dependencies and then merge in existing dependencies if (!empty($form_state['values']['sources']['dependencies'])) { $dependencies = array_filter($form_state['values']['sources']['dependencies']); $export['dependencies'] = _features_export_preserve_dependencies($export['dependencies'], NULL, $dependencies); } // Generate download if ($files = features_export_render($export, $export['project'], TRUE)) { $module_name = $export['project']; $filename = !empty($export['version']) ? "{$module_name}-{$export['version']}" : $module_name; $tar = array(); foreach ($files as $extension => $file_contents) { if (!in_array($extension, array('module', 'info'))) { $extension .= '.inc'; } $tar["{$module_name}/{$module_name}.$extension"] = $file_contents; } $tar = features_tar_create($tar); $header = function_exists('gzencode') ? 'Content-type: application/x-gzip' : 'Content-type: application/x-tar'; $filename = !empty($filename) ? $filename : $module_name; $filename = function_exists('gzencode') ? "{$filename}.tgz" : "{$filename}.tar"; drupal_set_header($header); drupal_set_header('Content-Disposition: attachment; filename="'. $filename .'"'); print $tar; exit; } } /** * AHAH handler for features_export_form_build(). */ function features_export_build_form_populate() { $form_state = array(); $submitted = $_POST; // Only uncomment this for debugging AJAX action $submitted = !empty($_POST) ? $_POST : $_GET; if ($form = form_get_cache($submitted['form_build_id'], $form_state)) { $stub = array(); // Assemble the combined component list $components = array_keys(features_get_components()); foreach ($components as $component) { // User-selected components take precedence. if (!empty($submitted['sources'][$component])) { $stub[$component] = $submitted['sources'][$component]; } // Only fallback to an existing feature's values if there are no export options for the component. else if (!isset($form['export']['sources'][$component]) && !empty($form['#feature']->info['features'][$component])) { $stub[$component] = $form['#feature']->info['features'][$component]; } } // Ensure source dependencies are populated $stub['dependencies'] = isset($submitted['sources']['dependencies']) ? $submitted['sources']['dependencies'] : array(); // Generate populated feature $export = features_populate($stub, $submitted['project']); $export['dependencies'] = _features_export_preserve_dependencies($export['dependencies'], NULL, $stub['dependencies']); drupal_json(array('status' => TRUE, 'data' => theme('features_components', $export, $stub, array(), TRUE))); exit; } drupal_json(array('status' => FALSE, 'data' => '')); exit; } /** * @param $items * @param $module_name * @return */ function features_populate($items, $module_name) { // Sanitize items. $items = array_filter($items); // Populate stub $stub = array('features' => array(), 'dependencies' => array(), 'conflicts' => array()); $export = _features_populate($items, $stub, $module_name); $export['dependencies'] = _features_export_minimize_dependencies($export['dependencies'], $module_name); // Clean up and standardize order foreach (array_keys($export['features']) as $k) { ksort($export['features'][$k]); } ksort($export['features']); ksort($export['dependencies']); 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 = '') { features_include(); foreach ($pipe as $component => $data) { if (features_hook($component, 'features_export')) { // Pass module-specific data and export array. // We don't use features_invoke() here since we need to pass $export by reference. $function = "{$component}_features_export"; $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]); } // Do some cleanup: // - Remove modules required by Drupal core. // - Protect against direct circular dependencies. // - Remove "intermediate" dependencies. $required = drupal_required_modules(); foreach ($dependencies as $k => $v) { if (empty($v) || in_array($v, $required)) { unset($dependencies[$k]); } else { $module = features_get_modules($v); if ($module && !empty($module->info['dependencies'])) { // If this dependency depends on the module itself, we have a circular dependency. // Don't let it happen. Only you can prevent forest fires. if (in_array($module_name, $module->info['dependencies'])) { unset($dependencies[$k]); } // Iterate through the dependency's dependencies and remove any dependencies // that are captured by it. else { foreach ($module->info['dependencies'] as $j => $dependency) { if (array_search($dependency, $dependencies) !== FALSE) { $position = array_search($dependency, $dependencies); unset($dependencies[$position]); } } } } } } return drupal_map_assoc(array_unique($dependencies)); } /** * Iterates over a list of dependencies and maximize the list of modules. */ function _features_export_maximize_dependencies($dependencies, $module_name = '', $first = TRUE) { $maximized = $dependencies; foreach ($dependencies as $k => $v) { $module = features_get_modules($v); if ($module && !empty($module->info['dependencies'])) { $maximized = array_merge($maximized, _features_export_maximize_dependencies($module->info['dependencies'], $module_name, FALSE)); } } return array_unique($maximized); } /** * Preserve existing dependencies. */ function _features_export_preserve_dependencies($dependencies, $module_name = NULL, $existing = array()) { if (!empty($module_name)) { $feature = feature_load($module_name); $existing = !empty($feature->info['dependencies']) ? $feature->info['dependencies'] : array(); } $merged = array_merge($existing, $dependencies); $merged = array_unique($merged); ksort($merged); return $merged; } /** * Prepare a feature export array into a finalized info array. */ function features_export_prepare($export, $module_name, $reset = FALSE) { $existing = features_get_modules($module_name, $reset); // Prepare info string -- if module exists, merge into its existing info file $defaults = $existing ? $existing->info : array('core' => '6.x', 'package' => 'Features', 'project' => $module_name); $export = array_merge($defaults, $export); // Cleanup info array foreach ($export['features'] as $component => $data) { $export['features'][$component] = array_keys($data); } if (isset($export['dependencies'])) { $export['dependencies'] = array_values($export['dependencies']); } if (isset($export['conflicts'])) { unset($export['conflicts']); } ksort($export); return $export; } /** * Generate an array of hooks and their raw code. */ function features_export_render_hooks($export, $module_name, $reset = FALSE) { features_include(); $code = array(); // Sort components to keep exported code consistent ksort($export['features']); foreach ($export['features'] as $component => $data) { if (!empty($data)) { // Sort the items so that we don't generate different exports based on order asort($data); if (features_hook($component, 'features_export_render')) { $hooks = features_invoke($component, 'features_export_render', $module_name, $data, $export); $code[$component] = $hooks; } } } return $code; } /** * Render feature export into an array representing its files. * * @param $export * An exported feature definition. * @param $module_name * The name of the module to be exported. * @param $reset * Boolean flag for resetting the module cache. Only set to true when * doing a final export for delivery. * * @return array of info file and module file contents. */ function features_export_render($export, $module_name, $reset = FALSE) { $code = array(); // Generate hook code $component_hooks = features_export_render_hooks($export, $module_name, $reset); $components = features_get_components(); // Group component code into their respective files foreach ($component_hooks as $component => $hooks) { $file = array('name' => 'defaults', 'stub' => true); if (isset($components[$component]['default_file'])) { switch ($components[$component]['default_file']) { case FEATURES_DEFAULTS_INCLUDED: $file['name'] = "features.$component"; break; case FEATURES_DEFAULTS_CUSTOM: $file['name'] = $components[$component]['default_filename']; $file['stub'] = false; break; } } if (!isset($code[$file['name']])) { $code[$file['name']] = array(); } foreach ($hooks as $hook_name => $hook_code) { if ($file['stub']) { if (!isset($code['features'])) { $code['features'] = array(); } $code['features'][$hook_name] = features_export_render_features($module_name, $hook_name, $file['name']); } $code[$file['name']][$hook_name] = features_export_render_defaults($module_name, $hook_name, $hook_code); } } // Finalize strings to be written to files foreach ($code as $filename => $contents) { $code[$filename] = "filename); } // Add a stub module to include the defaults else if (!empty($code['features'])) { $code['module'] = "name])) { // Make necessary inclusions if (module_exists('views')) { views_include('view'); } // Retrieve default hooks $default_hooks = features_get_default_hooks(); // Rebuild feature from .info file description and prepare an export from current DB state. $export = features_populate($module->info['features'], $module->name); $export['dependencies'] = _features_export_preserve_dependencies($export['dependencies'], $module->name); $export = features_export_prepare($export, $module->name); $code = features_export_render_hooks($export, "_features_comparison_{$module->name}"); $overridden = array(); // Compare feature info ksort($module->info); ksort($export); $compare = array( 'current' => features_export_info($export), 'default' => features_export_info($module->info) ); if ($compare['current'] !== $compare['default']) { $overridden['info'] = $compare; } // Merge items from both for comparison $merged = array_merge($export['features'], $module->info['features']); foreach (array_keys($merged) as $component) { if (isset($default_hooks[$component]) && ($default_hook = $default_hooks[$component])) { $compare = array('current' => array(), 'default' => array()); // Eval the database version of the export if (isset($code[$component][$default_hook])) { $compare['current'] = eval($code[$component][$default_hook]); $compare['current'] = is_array($compare['current']) ? $compare['current'] : array(); } // Call the existing in-code function and collect results if (module_hook($module->name, $default_hook)) { $compare['default'] = module_invoke($module->name, $default_hook); $compare['default'] = is_array($compare['default']) ? $compare['default'] : array(); } // Export both versions foreach ($compare as $storage => $items) { foreach ($items as $k => $v) { // Special case for views which provides its own export method if (is_object($v) && get_class($v) == 'view') { $compare[$storage][$k] = $v->export(); } else { $compare[$storage][$k] = features_var_export($v); } } } // Collect differences between the two arrays foreach ($compare['current'] as $k => $v) { if (isset($compare['current'][$k], $compare['default'][$k])) { if (_features_linetrim($compare['current'][$k]) !== _features_linetrim($compare['default'][$k])) { $overridden[$component][$k] = array('current' => $compare['current'][$k], 'default' => $compare['default'][$k]); } } else { $overridden[$component][$k] = array('current' => $compare['current'][$k], 'default' => ''); } } foreach ($compare['default'] as $k => $v) { if (!isset($compare['current'][$k])) { $overridden[$component][$k] = array('current' => '', 'default' => $compare['default'][$k]); } } } } $cache[$module->name] = $overridden; } return $cache[$module->name]; } /** * Gets the available default hooks keyed by components. */ function features_get_default_hooks() { static $hooks; if (!isset($hooks)) { $hooks = array(); features_include(); foreach (module_implements('features_api') as $module) { $info = module_invoke($module, 'features_api'); foreach ($info as $k => $v) { if (isset($v['default_hook'])) { $hooks[$k] = $v['default_hook']; } } } } return $hooks; } /** * Return a code string representing an implementation of a module hook. * Includes the module's defaults .inc and calls the private helper function. * * @param $module * The name of the module being generated * @param $hook * The name of the hook, without the "hook_" prefix. "hook_node_info" should * be "node_info" * @param $filename * Name of the include file. For a module 'foo', and filename'defaults' * becomes 'foo.defaults.inc'. */ function features_export_render_features($module, $hook, $filename = 'defaults') { $output = array(); $output[] = "/**"; $output[] = " * Implementation of hook_{$hook}()."; $output[] = " */"; $output[] = "function {$module}_{$hook}() {"; $output[] = " module_load_include('inc', '{$module}', '{$module}.{$filename}');"; $output[] = ' $args = func_get_args();'; $output[] = " return call_user_func_array('_{$module}_{$hook}', ".'$args'.");"; $output[] = "}"; return implode("\n", $output); } /** * Return a code string representing an implementation of a defaults module hook. */ function features_export_render_defaults($module, $hook, $code) { $output = array(); $output[] = "/**"; $output[] = " * Helper to implementation of hook_{$hook}()."; $output[] = " */"; $output[] = "function _{$module}_{$hook}() {"; $output[] = $code; $output[] = "}"; return implode("\n", $output); } /** * Generate code friendly to the Drupal .info format from a structured array. * * @param $info * An array or single value to put in a module's .info file. * @param $parents * Array of parent keys (internal use only). * * @return * A code string ready to be written to a module's .info file. */ function features_export_info($info, $parents = array()) { $output = ''; if (is_array($info)) { foreach ($info as $k => $v) { $child = $parents; $child[] = $k; $output .= features_export_info($v, $child); } } else if (!empty($info) && count($parents)) { $line = array_shift($parents); foreach ($parents as $key) { $line .= is_numeric($key) ? "[]" : "[{$key}]"; } $line .= " = \"{$info}\"\n"; return $line; } return $output; } /** * Tar creation function. Written by dmitrig01. * * @param $files * A keyed array where the key is the filepath and the value is the * string contents of the file. * * @return * A string of the tar file contents. */ function features_tar_create($files) { $tar = ''; foreach ($files as $name => $contents) { $binary_data_first = pack("a100a8a8a8a12A12", $name, '100644 ', // File permissions ' 765 ', // UID, ' 765 ', // GID, sprintf("%11s ", decoct(strlen($contents))), // Filesize, sprintf("%11s", decoct(time())) // Creation time ); $binary_data_last = pack("a1a100a6a2a32a32a8a8a155a12", '', '', '', '', '', '', '', '', '', ''); $checksum = 0; for ($i = 0; $i < 148; $i++) { $checksum += ord(substr($binary_data_first, $i, 1)); } for ($i = 148; $i < 156; $i++) { $checksum += ord(' '); } for ($i = 156, $j = 0; $i < 512; $i++, $j++) { $checksum += ord(substr($binary_data_last, $j, 1)); } $tar .= $binary_data_first; $tar .= pack("a8", sprintf("%6s ", decoct($checksum))); $tar .= $binary_data_last; $buffer = str_split($contents, 512); foreach ($buffer as $item) { $tar .= pack("a512", $item); } } if (function_exists('gzencode')) { $tar = gzencode($tar); } return $tar; } /** * Export var function -- from Views. */ function features_var_export($var, $prefix = '', $init = TRUE) { if (is_array($var)) { if (empty($var)) { $output = 'array()'; } else { $output = "array(\n"; foreach ($var as $key => $value) { $output .= " '$key' => " . features_var_export($value, ' ', FALSE) . ",\n"; } $output .= ')'; } } else if (is_bool($var)) { $output = $var ? 'TRUE' : 'FALSE'; } else if (is_string($var) && strpos($var, "\n") !== FALSE) { // Replace line breaks in strings with a token for replacement // at the very end. This protects whitespace in strings from // unintentional indentation. $var = str_replace("\n", "***BREAK***", $var); $output = var_export($var, TRUE); } else { $output = var_export($var, TRUE); } if ($prefix) { $output = str_replace("\n", "\n$prefix", $output); } if ($init) { $output = str_replace("***BREAK***", "\n", $output); } return $output; } /** * Helper function to return an array of t()'d translatables strings. * Useful for providing a separate array of translatables with your * export so that string extractors like potx can detect them. */ function features_translatables_export($translatables, $prefix = '') { sort($translatables); $translatables = array_unique($translatables); $output = $prefix . "// Translatables\n"; $output .= $prefix . "array(\n"; foreach ($translatables as $string) { $output .= $prefix . " t('" . strtr($string, array("'" => "\'")) . "'),\n"; } $output .= $prefix . ");\n"; return $output; } /** * Helper function to eliminate whitespace differences in code. */ function _features_linetrim($code) { $code = explode("\n", $code); foreach ($code as $k => $line) { $code[$k] = trim($line); } return implode("\n", $code); }