diff --git a/features.admin.inc b/features.admin.inc index 4e12576..48235e5 100644 --- a/features.admin.inc +++ b/features.admin.inc @@ -1095,8 +1095,13 @@ function features_admin_form($form, $form_state) { ); // Add in recreate link + $markup = l(t('Recreate'), "admin/structure/features/{$name}/recreate", array('attributes' => array('class' => array('admin-update')))); + if (features_get_module_status($name) == FEATURES_MODULE_ENABLED) { + // If module is not disabled, add in consolidate link + $markup .= ' ' . l(t('Consolidate'), "admin/structure/features/{$name}/consolidate", array('attributes' => array('class' => array('admin-update')))); + } $form[$package]['actions'][$name] = array( - '#markup' => l(t('Recreate'), "admin/structure/features/{$name}/recreate", array('attributes' => array('class' => array('admin-update')))), + '#markup' => $markup, ); } } @@ -1228,7 +1233,11 @@ function features_admin_components_revert(&$form, &$form_state) { features_include(); $module = $form_state['values']['module']; $revert = array($module => array()); - foreach (array_filter($form_state['values']['revert']) as $component => $status) { + $components = array_filter($form_state['values']['revert']); + if ($components == array()) { + $components = $form_state['values']['revert']; + } + foreach ($components as $component => $status) { $revert[$module][] = $component; drupal_set_message(t('Reverted all @component components for @module.', array('@component' => $component, '@module' => $module))); } @@ -1319,6 +1328,22 @@ function features_cleanup_form($form, $form_state, $cache_clear = FALSE) { } /** + * Page callback to consolidate a feature back into the database. + * + * @param $feature + * The loaded feature object to be consolidated. + */ +function features_feature_consolidate($feature) { + $destination = $_SERVER['HTTP_REFERER']; + features_include(); + $module = $feature->name; + foreach ($feature->info['features'] as $component => $items) { + features_invoke($component, 'features_consolidate', $items, $module); + } + drupal_goto($destination); +} + +/** * Page callback to display the differences between what's in code and * what is in the db. * diff --git a/features.api.php b/features.api.php index 10915e1..81f0c5f 100644 --- a/features.api.php +++ b/features.api.php @@ -239,6 +239,18 @@ function hook_features_export_alter(&$export, $module_name) { } /** + * TODO + */ +function hook_features_consolidate($data, &$export, $module_name) { + // The following is the simplest implementation of a straight object export + // with no further export processors called. + foreach ($data as $component) { + $export['mycomponent'][$component] = $component; + } + return array(); +} + +/** * Alter the pipe array for a given component. This hook should be implemented * with the name of the component type in place of `component` in the function * name, e.g. `features_pipe_views_alter()` will alter the pipe for the Views diff --git a/features.drush.inc b/features.drush.inc index 9da547d..af69994 100644 --- a/features.drush.inc +++ b/features.drush.inc @@ -75,6 +75,19 @@ function features_drush_command() { ), 'aliases' => array('fc'), ); + $items['features-consolidate'] = array( + 'description' => "Consolidate a feature to the database.", + 'drupal dependencies' => array('features'), + 'aliases' => array('fco'), + ); + $items['features-consolidate-all'] = array( + 'description' => "Consolidate all features to the database.", + 'drupal dependencies' => array('features'), + 'options' => array( + 'exclude' => "A comma-separated list of features to exclude from consolidate.", + ), + 'aliases' => array('fco-all', 'fcoa'), + ); $items['features-update'] = array( 'description' => "Update a feature module on your site.", 'arguments' => array( @@ -166,6 +179,10 @@ Patterns uses * or % for matching multiple sources/components. Unlike shorthands Lastly, a pattern without a colon is interpreted as having \":%\" appended, for easy listing of all components of a source. "); + case 'drush:features-consolidate': + return dt("Consolidate a feature to the database."); + case 'drush:features-consolidate-all': + return dt("Consolidate all features to the database."); case 'drush:features-update': return dt("Update a feature module on your site. The option '--version-set=foo' may be used to specify a version number for the feature or the option '--version-increment' may also to increment the feature's version number."); case 'drush:features-update-all': @@ -499,6 +516,58 @@ function drush_features_add() { /** + * Consolidate a feature to the database. + */ +function drush_features_consolidate() { + if ($args = func_get_args()) { + foreach ($args as $module) { + if (($feature = feature_load($module, TRUE)) && module_exists($module)) { + drush_log(dt("Consolidating features for module !module", array('!module' => $module)), 'notice'); + _drush_features_consolidate($feature->info['features'], $module); + } + elseif ($feature) { + _features_drush_set_error($module, 'FEATURES_FEATURE_NOT_ENABLED'); + } + else { + _features_drush_set_error($module); + } + } + } + else { + // By default just show contexts that are available. + $rows = array(array(dt('Available features'))); + foreach (features_get_features(NULL, TRUE) as $name => $info) { + $rows[] = array($name); + } + drush_print_table($rows, TRUE); + } +} + +/** + * Consolidate all features to the database. + */ +function drush_features_consolidate_all() { + $features_to_consolidate = array(); + $features_to_exclude = _convert_csv_to_array(drush_get_option('exclude')); + + $features = features_get_features(); + foreach ($features as $module) { + if ($module->status && !in_array($module->name, $features_to_exclude)) { + $features_to_consolidate[] = $module->name; + } + } + drush_print(dt('The following modules will be consolidated: !modules', array('!modules' => implode(', ', $features_to_consolidate)))); + if (drush_confirm(dt('Do you really want to continue?'))) { + foreach ($features_to_consolidate as $module_name) { + drush_invoke_process('@self', 'features-consolidate', array($module_name)); + } + } + else { + return drush_user_abort(); + } +} + +/** * Update an existing feature module. */ function drush_features_update() { @@ -654,6 +723,17 @@ function _drush_features_generate_export(&$info, &$module_name) { } /** + * Consolidate a feature to the database. + */ +function _drush_features_consolidate($features = array(), $module_name = '') { + features_include(); + foreach ($features as $component => $items) { + drush_log(dt("Running consolidate for component !component", array('!component' => $component)), 'notice'); + features_invoke($component, 'features_consolidate', $items, $module_name); + } +} + +/** * Revert a feature to it's code definition. * Optionally accept a list of components to revert. */ diff --git a/features.module b/features.module index ba5b9fe..a591ad5 100644 --- a/features.module +++ b/features.module @@ -154,6 +154,18 @@ function features_menu() { 'file' => "features.admin.inc", 'weight' => 11, ); + $items['admin/structure/features/%feature/consolidate'] = array( + 'title' => 'Consolidate', + 'description' => 'Consolidate a feature to the database.', + 'page callback' => 'features_feature_consolidate', + 'page arguments' => array(3), + 'load arguments' => array(3, TRUE), + 'access callback' => 'features_access_consolidate_feature', + 'access arguments' => array(3), + 'type' => MENU_LOCAL_TASK, + 'file' => "features.admin.inc", + 'weight' => 12, + ); if (module_exists('diff')) { $items['admin/structure/features/%feature/diff'] = array( 'title' => 'Review overrides', @@ -876,6 +888,14 @@ function features_access_override_actions($feature) { } /** + * Menu access callback for whether a user should be able to + * consolidate a given feature. + */ +function features_access_consolidate_feature($feature) { + return (user_access('administer features') && (features_get_module_status($feature->name) == FEATURES_MODULE_ENABLED)); +} + +/** * Implements hook_form_alter() for system_modules form(). */ function features_form_system_modules_alter(&$form) { diff --git a/includes/features.context.inc b/includes/features.context.inc index 2da59a7..dd6b937 100644 --- a/includes/features.context.inc +++ b/includes/features.context.inc @@ -52,3 +52,15 @@ function context_features_revert($module = NULL) { context_invalidate_cache(); return $return; } + + +/** + * Implementation of hook_features_consolidate(). + */ +function context_features_consolidate($items = array(), $module_name = '') { + foreach ($items as $context_name) { + if ($context = context_load($context_name)) { + context_save($context); + } + } +} diff --git a/includes/features.image.inc b/includes/features.image.inc index 2b5eb27..d92197e 100644 --- a/includes/features.image.inc +++ b/includes/features.image.inc @@ -99,3 +99,36 @@ function _image_features_style_sanitize(&$style, $child = FALSE) { } } } + +/** + * Implementation of hook_features_consolidate(). + */ +function image_features_consolidate($items = array(), $module_name = '') { + foreach ($items as $style_name) { + $style = image_style_load($style_name); + // Styles + if (isset($style['isid'])) { + if (!is_numeric($style['isid'])) { + _image_style_and_effects_save($style); + } + } + else { + _image_style_and_effects_save($style); + } + } +} + +/** + * Helper function for image_features_consolidate(). + * Saves a single style together with its effects. + */ +function _image_style_and_effects_save($style) { + $style = image_style_save($style); + // Actions + if (is_numeric($style['isid'])) { + foreach ($style['effects'] as $effect) { + $effect['isid'] = $style['isid']; + image_effect_save($effect); + } + } +} diff --git a/includes/features.node.inc b/includes/features.node.inc index 7beb55f..f9df80c 100644 --- a/includes/features.node.inc +++ b/includes/features.node.inc @@ -115,7 +115,7 @@ function node_features_revert($module = NULL) { } /** - * Implements hook_features_disable(). + * Implements hook_features_disable_feature(). * * When a features module is disabled, modify any node types it provides so * they can be deleted manually through the content types UI. @@ -123,7 +123,7 @@ function node_features_revert($module = NULL) { * @param $module * Name of module that has been disabled. */ -function node_features_disable($module) { +function node_features_disable_feature($module) { if ($default_types = features_get_default('node', $module)) { foreach ($default_types as $type_name => $type_info) { $type_info = node_type_load($type_name); @@ -137,7 +137,7 @@ function node_features_disable($module) { } /** - * Implements hook_features_enable(). + * Implements hook_features_enable_feature(). * * When a features module is enabled, modify any node types it provides so * they can no longer be deleted manually through the content types UI. @@ -145,7 +145,7 @@ function node_features_disable($module) { * @param $module * Name of module that has been enabled. */ -function node_features_enable($module) { +function node_features_enable_feature($module) { if ($default_types = features_get_default('node', $module)) { foreach ($default_types as $type_name => $type_info) { // Ensure the type exists. @@ -159,3 +159,28 @@ function node_features_enable($module) { } } } + +/** + * Implementation of hook_features_consolidate(). + * + * When a features module is consolidated, modify any node types it provides + * so responsibility is delegated to the node module. + * + * @param $items + * Array of names of node types to be consolidated. + * @param $module + * Name of module that has been consolidated. + */ +function node_features_consolidate($items = array(), $module = 'feature') { + module_load_include('inc', 'features', 'features.export'); + // Delegate responsibility to node module + foreach ($items as $type_name) { + // Load the type. + $type_info = node_type_load($type_name); + $type_info->module = 'node'; + $type_info->custom = 1; + $type_info->modified = 1; + $type_info->locked = 0; + node_type_save($type_info); + } +} diff --git a/includes/features.views.inc b/includes/features.views.inc new file mode 100644 index 0000000..231c950 --- /dev/null +++ b/includes/features.views.inc @@ -0,0 +1,277 @@ + $code); +} + +/** + * Implementation of hook_features_api(). + */ +function views_features_api() { + return array( + 'views' => array( + 'name' => t('Views'), + 'feature_source' => TRUE, + 'default_hook' => 'views_default_views', + 'default_file' => FEATURES_DEFAULTS_CUSTOM, + 'default_filename' => 'views_default', + ), + 'views_api' => array( + 'name' => t('Views API'), + 'feature_source' => FALSE, + 'duplicates' => FEATURES_DUPLICATES_ALLOWED, + // Views API integration does not include a default hook declaration as + // it is not a proper default hook. + // 'default_hook' => 'views_api', + ) + ); +} + +/** + * Implementation of hook_features_export_options(). + */ +function views_features_export_options() { + $enabled_views = array(); + $views = views_get_all_views(); + foreach ($views as $view) { + if (!isset($views[$view->name]->disabled) || !$views[$view->name]->disabled) { + $enabled_views[$view->name] = $view->name; + } + } + ksort($enabled_views); + return $enabled_views; +} + +/** + * Implementation of hook_features_export_render(). + */ +function views_features_export_render($module, $data) { + $code = array(); + $code[] = ' $views = array();'; + $code[] = ''; + + // Build views & add to export array + foreach ($data as $view_name) { + // Build the view + $view = views_get_view($view_name, TRUE); + if ($view) { + $code[] = ' // Exported view: '. $view_name; + $code[] = $view->export(' '); + $code[] = ' $views[$view->name] = $view;'; + $code[] = ''; + } + } + $code[] = ' return $views;'; + $code = implode("\n", $code); + return array('views_default_views' => $code); +} + +/** + * Implementation of hook_features_export(). + */ + +function views_features_export($data, &$export, $module_name = '') { + // Build views & add to export array + $map = features_get_default_map('views', 'name'); + $views = array(); + $conflicts = array(); + foreach ($data as $view_name) { + if ($view = views_get_view($view_name, TRUE)) { + // This view is provided by another module. Add it as a dependency or + // display a conflict message if the View is overridden. + if (isset($map[$view_name]) && $map[$view_name] !== $module_name) { + if ($v = views_get_view($view_name)) { + if ($v->type === 'Overridden') { + $conflicts[$map[$view_name]] = $view_name; + } + elseif ($v->type === 'Default') { + $export['dependencies'][$map[$view_name]] = $map[$view_name]; + } + } + } + // Otherwise just add to exports + else { + $export['features']['views'][$view_name] = $view_name; + $views[$view_name] = $view; + } + } + } + if (!empty($conflicts)) { + $message = 'The following overridden view(s) are provided by other modules: !views. To resolve this problem either revert each view or clone each view so that modified versions can be exported with your feature.'; + $tokens = array('!views' => implode(', ', $conflicts)); + features_log(t($message, $tokens), 'warning'); + } + + // Only add Views API hook if there are actually views to export. + if (!empty($export['features']['views'])) { + $export['features']['views_api']['api:'. views_api_version()] = 'api:'. views_api_version(); + $export['dependencies']['views'] = 'views'; + } + + // Discover module dependencies + // We need to find dependencies based on: + // 1. handlers + // 2. plugins (style plugins, display plugins) + // 3. other... (e.g. imagecache display option for CCK imagefields) : ( + + // Handlers + $handlers = array('fields', 'filters', 'arguments', 'sort', 'relationships'); + $handler_dependencies = views_handler_dependencies(); + + // Plugins + // For now we only support dependency detection for a subset of all views plugins + $plugins = array('display', 'style', 'row', 'access'); + $plugin_dependencies = views_plugin_dependencies(); + + foreach ($views as $view) { + foreach ($view->display as $display) { + // Handlers + foreach ($handlers as $handler) { + if (isset($display->display_options[$handler])) { + foreach ($display->display_options[$handler] as $item) { + if ($item['table'] && isset($handler_dependencies[$item['table']])) { + $module = $handler_dependencies[$item['table']]; + $export['dependencies'][$module] = $module; + } + } + } + } + + // Plugins + foreach ($plugins as $plugin_type) { + $plugin_name = ''; + switch ($plugin_type) { + case 'display': + if (isset($display->display_plugin)) { + $plugin_name = $display->display_plugin; + } + break; + case 'access': + if (isset($display->display_options['access'], $display->display_options['access']['type']) && $display->display_options['access']['type'] != 'none') { + $plugin_name = $display->display_options['access']['type']; + } + break; + default: + if (isset($display->display_options["{$plugin_type}_plugin"])) { + $plugin_name = $display->display_options["{$plugin_type}_plugin"]; + } + break; + } + if (!empty($plugin_name) && isset($plugin_dependencies[$plugin_type][$plugin_name])) { + $module = $plugin_dependencies[$plugin_type][$plugin_name]; + $export['dependencies'][$module] = $module; + } + } + } + } +} + +/** + * Provides an array that maps hook_views_data() tables to modules. + */ +function views_handler_dependencies() { + views_include_handlers(); + + static $map; + if (!isset($map)) { + $map = array(); + foreach (module_implements('views_data') as $module) { + if ($tables = module_invoke($module, 'views_data')) { + foreach ($tables as $table => $info) { + if (isset($info['table']) && (!isset($map[$table]) || $info['table']['group'])) { + $map[$table] = $module; + } + else if (!isset($map[$table])) { + $map[$table] = $module; + } + } + } + } + } + return $map; +} + +/** + * Provides an array that maps hook_views_plugins() to modules. + */ +function views_plugin_dependencies() { + views_include_handlers(); + + static $map; + if (!isset($map)) { + $map = array(); + foreach (module_implements('views_plugins') as $module) { + $plugins = module_invoke($module, 'views_plugins'); + if (!empty($plugins)) { + foreach ($plugins as $type => $items) { + if (is_array($items)) { + foreach (array_keys($items) as $plugin_name) { + $map[$type][$plugin_name] = $module; + } + } + } + } + } + } + return $map; +} + +/** + * Implementation of hook_features_revert(). + */ +function views_features_revert($module) { + if ($default_views = features_get_default('views', $module)) { + foreach ($default_views as $default_view) { + if ($current_view = views_get_view($default_view->name)) { + $current_view->delete(FALSE); + } + } + // Flush caches. + cache_clear_all(); + menu_rebuild(); + } +} + +/** + * Implementation of hook_features_consolidate(). + */ +function views_features_consolidate($items = array(), $module_name = '') { + foreach ($items as $view_name) { + $view = views_get_view($view_name); + $view->save(); + } +}