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'), '#element_validate' => array('features_export_form_validate_field'), ); $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, '#element_validate' => array('features_export_form_validate_field'), ); $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, '#element_validate' => array('features_export_form_validate_field'), ); // User-selected feature source components. $components = 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 => $component_info) { // Tack on dependency options as they are not a feature component proper. if ($component == 'dependencies') { // Excluded modules. $excluded = drupal_required_modules(); // Don't allow feature to depend on itself. if (isset($feature->name)) { $excluded[] = $feature->name; } $options = array(); foreach (features_get_modules() as $module_name => $info) { if (!in_array($module_name, $excluded) && $info->status && !empty($info->info)) { $options[$module_name] = $info->info['name']; } } $default_value = !empty($feature->info['dependencies']) ? $feature->info['dependencies'] : array(); $component_info = array('name' => t('Dependencies')); $component = 'dependencies'; } 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] = (isset($component_info['name']) ? $component_info['name'] : $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']) : "
", ); $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; } /** * Validation for project field. */ function features_export_form_validate_field($element, &$form_state) { switch ($element['#name']) { case 'project': if (!preg_match('!^[a-z0-9_]+$!', $element['#value'])) { form_error($element, t('The machine-readable name must contain only lowercase letters, numbers, and underscores.')); } // If user is filling out the feature name for the first time and uses // the name of an existing module throw an error. else if (empty($element['#default_value']) && features_get_info('module', $element['#value'])) { form_error($element, t('A module by the name @name already exists on your site. Please choose a different name.', array('@name' => $element['#value']))); } break; case 'project_status_url': if (!empty($element['#value']) && !valid_url($element['#value'])) { form_error($element, t('The URL %url is invalid. Please enter a fully-qualified URL, such as http://www.example.com/feed.xml.', array('%url' => $element['#value']))); } break; case 'version': preg_match('/^(?P\d+\.x)-(?P\d+)\.(?P\d+)-?(?P\w+)?$/', $element['#value'], $matches); if (!empty($element['#value']) && !isset($matches['core'], $matches['major'])) { form_error($element, t('Please enter a valid version with core and major version number. Example: !example', array('!example' => '6.x-1.0'))); }; break; } } /** * Submit handler for features_export_form_build(). */ function features_export_build_form_submit($form, &$form_state) { module_load_include('inc', 'features', 'features.export'); features_include(); // 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"; // Clear out output buffer to remove any garbage from tar output. if (ob_get_level()) { ob_end_clean(); } 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() { module_load_include('inc', 'features', 'features.export'); features_include(); $form_state = array(); $submitted = $_POST; 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))); exit; } drupal_json(array('status' => FALSE, 'data' => '')); exit; } /** * admin/build/features page callback. */ function features_admin_form($form_state) { // Load export functions to use in comparison. module_load_include('inc', 'features', 'features.export'); // Clear & rebuild key caches module_rebuild_cache(); features_rebuild(); $features = features_get_features('', TRUE); $modules = features_get_modules('', TRUE); $conflicts = features_get_conflicts(); $form = array( '#features' => $features, '#theme' => 'features_form', ); // Generate features form. foreach ($features as $name => $module) { $package_title = !empty($module->info['package']) ? $module->info['package'] : t('Other'); $package = strtolower(preg_replace('/[^a-zA-Z0-9-]+/', '-', $package_title)); // Set up package elements if (!isset($form[$package])) { $form[$package] = array( '#tree' => FALSE, '#title' => t($package_title), '#theme' => 'features_form_package' ); $form[$package]['links'] = $form[$package]['version'] = $form[$package]['weight'] = $form[$package]['status'] = $form[$package]['action'] = array('#tree' => TRUE); } $disabled = FALSE; $description = $module->info['description']; // Detect unmet dependencies if (!empty($module->info['dependencies'])) { $unmet_dependencies = array(); $dependencies = _features_export_maximize_dependencies($module->info['dependencies']); foreach ($dependencies as $dependency) { if (empty($modules[$dependency])) { $unmet_dependencies[] = theme('features_module_status', FEATURES_MODULE_MISSING, $dependency); } } if (!empty($unmet_dependencies)) { $description .= "
". t('Unmet dependencies: !dependencies', array('!dependencies' => implode(', ', $unmet_dependencies))) ."
"; $disabled = TRUE; } } // Detect potential conflicts if (!empty($conflicts[$name])) { $module_conflicts = array(); foreach ($conflicts[$name] as $conflict) { $module_conflicts[] = theme('features_module_status', FEATURES_MODULE_MISSING, $conflict); // Only disable modules with conflicts if they are not already enabled. // If they are already enabled, somehow the user got themselves into a // bad situation and they need to be able to disable a conflicted module. if (module_exists($conflict) && !module_exists($name)) { $disabled = TRUE; } } $description .= "
". t('Incompatible with: !conflicts', array('!conflicts' => implode(', ', $module_conflicts))) ."
"; } $form[$package]['status'][$name] = array( '#type' => 'checkbox', '#title' => $module->info['name'], '#description' => $description, '#default_value' => $module->status, '#disabled' => $disabled, ); if (!empty($module->info['project status url'])) { $uri = l(truncate_utf8($module->info['project status url'], 35, TRUE, TRUE), $module->info['project status url']); } else if (isset($module->info['project'], $module->info['version'], $module->info['datestamp'])) { $uri = l('http://drupal.org', 'http://drupal.org/project/'. $module->info['project']); } else { $uri = t('Unavailable'); } $version = !empty($module->info['version']) ? $module->info['version'] : ''; $version = !empty($version) ? "
$version
" : ''; $form[$package]['sign'][$name] = array('#type' => 'markup', '#value' => "{$uri} {$version}"); if (user_access('administer features')) { // Add status link $href = "admin/build/features/{$name}"; if ($module->status) { $state = '' . t('Checking...') . ''; $state .= l(t('Check'), "admin/build/features/{$name}/status", array('attributes' => array('class' => 'admin-check'))); $state .= theme('features_storage_link', FEATURES_REBUILDING, $href); $state .= theme('features_storage_link', FEATURES_NEEDS_REVIEW, $href); $state .= theme('features_storage_link', FEATURES_OVERRIDDEN, $href); $state .= theme('features_storage_link', FEATURES_DEFAULT, $href); } else { $state = ''. t('Disabled') .''; } $form[$package]['state'][$name] = array( '#type' => 'markup', '#value' => !empty($state) ? $state : '', ); // Add in recreate link $form[$package]['actions'][$name] = array( '#type' => 'markup', '#value' => l(t('Recreate'), "admin/build/features/{$name}/recreate", array('attributes' => array('class' => 'admin-update'))), ); } } $form['buttons'] = array( '#theme' => 'features_form_buttons', ); $form['buttons']['submit'] = array( '#type' => 'submit', '#value' => t('Save settings'), '#submit' => array('features_form_submit'), '#validate' => array('features_form_validate'), ); return $form; } /** * Display the components of a feature. */ function features_admin_components($form_state, $feature) { module_load_include('inc', 'features', 'features.export'); $form = array(); // Store feature info for theme layer. $form['module'] = array('#type' => 'value', '#value' => $feature->name); $form['#info'] = $feature->info; $form['#dependencies'] = array(); if (!empty($feature->info['dependencies'])) { foreach ($feature->info['dependencies'] as $dependency) { $status = features_get_module_status($dependency); $form['#dependencies'][$dependency] = $status; } } $review = $revert = FALSE; // Iterate over components and retrieve status for display $states = features_get_component_states(array($feature->name), FALSE); $form['revert']['#tree'] = TRUE; foreach ($feature->info['features'] as $component => $items) { if (user_access('administer features') && in_array($states[$feature->name][$component], array(FEATURES_OVERRIDDEN, FEATURES_NEEDS_REVIEW))) { switch ($states[$feature->name][$component]) { case FEATURES_OVERRIDDEN: $revert = TRUE; break; case FEATURES_NEEDS_REVIEW: $review = TRUE; break; } $form['revert'][$component] = array( '#type' => 'checkbox', '#default_value' => FALSE, ); } if (module_exists('diff')) { $item = menu_get_item("admin/build/features/{$feature->name}/diff/{$component}"); $path = ($item && $item['access']) ? $item['href'] : NULL; } else { $path = NULL; } $form['components'][$component] = array( '#type' => 'markup', '#value' => theme('features_storage_link', $states[$feature->name][$component], $path), ); } if ($review || $revert) { $form['buttons'] = array('#theme' => 'features_form_buttons', '#tree' => TRUE); if ($revert || $review) { $form['buttons']['revert'] = array( '#type' => 'submit', '#value' => t('Revert components'), '#submit' => array('features_admin_components_revert'), ); } if ($review) { $form['buttons']['review'] = array( '#type' => 'submit', '#value' => t('Mark as reviewed'), '#submit' => array('features_admin_components_review'), ); } } return $form; } /** * Submit handler for revert form. */ function features_admin_components_revert(&$form, &$form_state) { module_load_include('inc', 'features', 'features.export'); features_include(); $module = $form_state['values']['module']; $revert = array(); foreach (array_filter($form_state['values']['revert']) as $component => $status) { $revert[$module][] = $component; drupal_set_message(t('Reverted all !component components for !module.', array('!component' => $component, '!module' => $module))); } features_revert($revert); $form_state['redirect'] = 'admin/build/features/'. $module; } /** * Submit handler for revert form. */ function features_admin_components_review(&$form, &$form_state) { module_load_include('inc', 'features', 'features.export'); features_include(); $module = $form_state['values']['module']; $revert = array(); foreach (array_filter($form_state['values']['revert']) as $component => $status) { features_set_signature($module, $component); drupal_set_message(t('All !component components for !module reviewed.', array('!component' => $component, '!module' => $module))); } $form_state['redirect'] = 'admin/build/features/'. $module; } /** * Validate handler for the 'manage features' form. */ function features_form_validate(&$form, &$form_state) { $conflicts = features_get_conflicts(); foreach ($form_state['values']['status'] as $module => $status) { if ($status && !empty($conflicts[$module])) { foreach ($conflicts[$module] as $conflict) { if (!empty($form_state['values']['status'][$conflict])) { form_set_error('status', t('The feature !module cannot be enabled because it conflicts with !conflict.', array('!module' => $module, '!conflict' => $conflict))); } } } } } /** * Submit handler for the 'manage features' form */ function features_form_submit(&$form, &$form_state) { // Clear drupal caches after enabling a feature. We do this in a separate // page callback rather than as part of the submit handler as some modules // have includes/other directives of importance in hooks that have already // been called in this page load. $form_state['redirect'] = 'admin/build/features/cleanup/clear'; $features = $form['#features']; if (!empty($features)) { $status = $form_state['values']['status']; $install = array_keys(array_filter($status)); $disable = array_diff(array_keys($status), $install); features_install_modules($install); module_disable($disable); } } /** * Form for disabling orphaned dependencies. */ function features_cleanup_form($form_state, $cache_clear = FALSE) { $form = array(); // Clear caches if we're getting a post-submit redirect that requests it. if ($cache_clear) { drupal_flush_all_caches(); // The following functions need to be run because drupal_flush_all_caches() // runs rebuilds in the wrong order. The node type cache is rebuilt *after* // the menu is rebuilt, meaning that the menu tree is stale in certain // circumstances after drupal_flush_all_caches(). We rebuild again. menu_rebuild(); } // Retrieve orphaned modules and provide them as optional modules to be disabled. // Exclude any modules that have been added to the 'ignored' list. $options = array(); $orphans = features_get_orphans(); $ignored = variable_get('features_ignored_orphans', array()); if (!empty($orphans)) { foreach ($orphans as $module) { if (!in_array($module->name, $ignored, TRUE)) { $options[$module->name] = check_plain($module->info['name']); } } } if (!empty($options)) { $form['orphans'] = array( '#title' => t('Orphaned dependencies'), '#description' => t('These modules are dependencies of features that have been disabled. They may be disabled without affecting other components of your website.'), '#type' => 'checkboxes', '#options' => $options, '#default_value' => array_keys($options), ); $form['buttons'] = array('#tree' => TRUE, '#theme' => 'features_form_buttons'); $form['buttons']['disable'] = array( '#type' => 'submit', '#value' => t('Disable selected modules'), '#submit' => array('features_cleanup_form_disable'), ); $form['buttons']['ignore'] = array( '#type' => 'submit', '#value' => t('Leave enabled'), '#submit' => array('features_cleanup_form_ignore'), ); } else { drupal_goto('admin/build/features'); } return $form; } /** * Submit handler for disable action on features_cleanup_form(). */ function features_cleanup_form_disable(&$form, &$form_state) { if (!empty($form_state['values']['orphans'])) { $disable = array_keys(array_filter($form_state['values']['orphans'])); $ignored = array_diff(array_keys($form_state['values']['orphans']), $disable); // Disable any orphans that have been selected. module_disable($disable); drupal_flush_all_caches(); // Add enabled modules to ignored orphans list. $ignored_orphans = variable_get('features_ignored_orphans', array()); foreach ($ignored as $module) { $ignored_orphans[$module] = $module; } variable_set('features_ignored_orphans', $ignored_orphans); } $form_state['redirect'] = 'admin/build/features/cleanup'; } /** * Submit handler for ignore action on features_cleanup_form(). */ function features_cleanup_form_ignore(&$form, &$form_state) { if (!empty($form_state['values']['orphans'])) { $ignored = array_keys($form_state['values']['orphans']); $ignored_orphans = variable_get('features_ignored_orphans', array()); foreach ($ignored as $module) { $ignored_orphans[$module] = $module; } variable_set('features_ignored_orphans', $ignored_orphans); } $form_state['redirect'] = 'admin/build/features/cleanup'; } /** * Page callback to display the differences between what's in code and * what is in the db. * * @param $feature * A loaded feature object to display differences for. * @param $component * Optional: specific component to display differences for. If excluded, all components are used. * * @return Themed display of what is different. */ function features_feature_diff($feature, $component = NULL) { drupal_add_css(drupal_get_path('module', 'features') .'/features.css'); module_load_include('inc', 'features', 'features.export'); $overrides = features_detect_overrides($feature); if (!empty($overrides)) { // Filter overrides down to specified component. if (isset($component) && isset($overrides[$component])) { $overrides = array($component => $overrides[$component]); } module_load_include('php', 'diff', 'DiffEngine'); $formatter = new DrupalDiffFormatter(); $rows = array(); foreach ($overrides as $component => $items) { $diff = new Diff(explode("\n", $items['default']), explode("\n", $items['normal'])); $rows[] = array(array('data' => $component, 'colspan' => 4, 'header' => TRUE)); $rows = array_merge($rows, $formatter->format($diff)); } $header = array( array('data' => t('Default'), 'colspan' => 2), array('data' => t('Overrides'), 'colspan' => 2), ); $output .= theme('diff_table', $header, $rows, array('class' => 'diff features-diff')); } else { $output = "
". t('No changes have been made to this feature.') ."
"; } $output = "
{$output}
"; return $output; } /** * Javascript call back that returns the status of a feature. */ function features_feature_status($feature) { module_load_include('inc', 'features', 'features.export'); return drupal_json(array('storage' => features_get_storage($feature->name))); }