'Features', 'description' => 'Manage features.', 'page callback' => 'drupal_get_form', 'page arguments' => array('features_admin_form'), 'type' => MENU_NORMAL_ITEM, 'file' => 'features.admin.inc', ); $items['admin/build/features/cleanup'] = array( 'title' => 'Cleanup', 'description' => 'Detect and disable any orphaned feature dependencies.', 'page callback' => 'drupal_get_form', 'page arguments' => array('features_cleanup_form', 4), 'type' => MENU_CALLBACK, 'file' => 'features.admin.inc', 'weight' => 1, ); $items['admin/build/features/manage'] = array( 'title' => 'Manage', 'description' => 'Enable and disable features.', 'page callback' => 'drupal_get_form', 'page arguments' => array('features_admin_form'), 'type' => MENU_DEFAULT_LOCAL_TASK, 'file' => 'features.admin.inc', ); $items['admin/build/features/create'] = array( 'title' => 'Create feature', 'description' => 'Create a new feature.', 'page callback' => 'drupal_get_form', 'page arguments' => array('features_export_form'), 'access callback' => 'user_access', 'access arguments' => array('administer features'), 'type' => MENU_LOCAL_TASK, 'file' => "features.admin.inc", 'weight' => 10, ); $items['admin/build/features/export/populate'] = array( 'title' => 'Populate feature', 'description' => 'AHAH callback to populate a feature from selected components.', 'page callback' => 'features_export_build_form_populate', 'page arguments' => array(), 'access callback' => 'user_access', 'access arguments' => array('administer features'), 'type' => MENU_CALLBACK, 'file' => "features.admin.inc", ); $items['admin/build/features/%feature'] = array( 'title callback' => 'features_get_feature_title', 'title arguments' => array(3), 'description' => 'Display components of a feature.', 'page callback' => 'drupal_get_form', 'page arguments' => array('features_admin_components', 3), 'load arguments' => array(3, TRUE), 'access callback' => 'user_access', 'access arguments' => array('administer features'), 'type' => MENU_CALLBACK, 'file' => 'features.admin.inc', ); $items['admin/build/features/%feature/view'] = array( 'title' => 'View', 'description' => 'Display components of a feature.', 'access callback' => 'user_access', 'access arguments' => array('administer features'), 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10, ); $items['admin/build/features/%feature/recreate'] = array( 'title' => 'Recreate', 'description' => 'Recreate an existing feature.', 'page callback' => 'drupal_get_form', 'page arguments' => array('features_export_form', 3), 'load arguments' => array(3, TRUE), 'access callback' => 'user_access', 'access arguments' => array('administer features'), 'type' => MENU_LOCAL_TASK, 'file' => "features.admin.inc", 'weight' => 11, ); if (module_exists('diff')) { $items['admin/build/features/%feature/diff'] = array( 'title' => 'Review overrides', 'description' => 'Compare default and current feature.', 'page callback' => 'features_feature_diff', 'page arguments' => array(3, 5), 'load arguments' => array(3, TRUE), 'access callback' => 'features_access_override_actions', 'access arguments' => array(3), 'type' => MENU_LOCAL_TASK, 'file' => 'features.admin.inc', ); } $items['admin/build/features/%feature/status'] = array( 'title' => 'Status', 'description' => 'Javascript status call back.', 'page callback' => 'features_feature_status', 'page arguments' => array(3), 'load arguments' => array(3, TRUE), 'access callback' => 'user_access', 'access arguments' => array('administer features'), 'type' => MENU_CALLBACK, 'file' => 'features.admin.inc', ); foreach ($items as $path => $item) { if (!isset($item['access callback'])) { $items[$path]['access callback'] = 'user_access'; $items[$path]['access arguments'] = array('manage features'); } } return $items; } /** * Implementation of hook_menu_alter(). */ function features_menu_alter(&$cache) { // Collect exported menu items and graft them into the features menu. $items = module_invoke_all('menu_default_items'); drupal_alter('menu_default_items', $items); foreach ($items as $item) { if (!empty($item['path'])) { $path = $item['path']; unset($item['path']); if (!empty($cache[$path])) { $cache[$path] = array_merge($cache[$path], $item); $cache[$path]['type'] = MENU_NORMAL_ITEM; $cache[$path]['menu_name'] = 'features'; } } } } /** * Implementation of hook_theme(). */ function features_theme() { $base = array( 'path' => drupal_get_path('module', 'features') .'/theme', 'file' => 'theme.inc', ); $items = array(); $items['features_components'] = $items['features_component_key'] = $items['features_component_list'] = $items['features_form_buttons'] = $items['features_form_components'] = $items['features_form_export'] = $items['features_form_package'] = $items['features_module_status'] = $items['features_storage_link'] = $base; $items['features_admin_components'] = array( 'arguments' => array('form' => NULL), 'template' => 'features-admin-components', ) + $base; $items['features_form'] = array( 'arguments' => array('form' => NULL), 'template' => 'features-form', ) + $base; return $items; } /** * Implementation of hook_theme_registry_alter(). */ function features_theme_registry_alter(&$theme_registry) { // Ensures that features_preprocess_page() comes immediately after // template_preprocess_page(). if ($position = array_search('features_preprocess_page', $theme_registry['page']['preprocess functions'])) { unset($theme_registry['page']['preprocess functions'][$position]); } $position = array_search('template_preprocess_page', $theme_registry['page']['preprocess functions']); $position = $position ? $position + 1 : 2; array_splice($theme_registry['page']['preprocess functions'], $position, 0, 'features_preprocess_page'); } /** * Implementation of hook_flush_caches(). */ function features_flush_caches() { features_rebuild(); features_get_modules(NULL, TRUE); return array(); } /** * Implementation of hook_form(). */ function features_form($node, $form_state) { return node_content_form($node, $form_state); } /** * Implemenation of hook_access() */ function features_access($op, $node, $account) { return node_content_access($op, $node, $account); } /** * Implementation of hook_perm(). */ function features_perm() { $perms = array('administer features', 'manage features'); // Generate permissions for any default node types provided by features. foreach (node_get_types() as $type) { if ($type->module == 'features') { $name = check_plain($type->type); $perms[] = 'create '. $name .' content'; $perms[] = 'delete own '. $name .' content'; $perms[] = 'delete any '. $name .' content'; $perms[] = 'edit own '. $name .' content'; $perms[] = 'edit any '. $name .' content'; } } return $perms; } /** * Load includes for any modules that implement the features API and * load includes for those provided by features. */ function features_include() { // Check for implementing modules and make necessary inclusions. foreach (module_implements('features_api') as $module) { $info = module_invoke($module, 'features_api'); foreach ($info as $component) { if (isset($component['file'])) { require_once $component['file']; } } } // Always load the menu module include to be able to deal with menu items. module_load_include('inc', 'features', "includes/features.menu"); // Features provides integration on behalf of these modules. // Note that ctools is placed last because it implements hooks "dynamically" for other modules. $modules = array('block', 'content', 'context', 'filter', 'imagecache', 'node', 'user', 'views', 'ctools'); foreach (array_filter($modules, 'module_exists') as $module) { if (!module_hook($module, 'features_api')) { module_load_include('inc', 'features', "includes/features.$module"); } } // Clear static cache, since we've now included new implementers. module_implements('features_api', FALSE, TRUE); } /** * Feature object loader. */ function feature_load($name, $reset = FALSE) { return features_get_features($name, $reset); } /** * Return a module 'object' including .info information. * * @param $name * The name of the module to retrieve information for. If ommitted, * an array of all available modules will be returned. * @param $reset * Whether to reset the cache. * * @return * If a module is request (and exists) a module object is returned. If no * module is requested info for all modules is returned. */ function features_get_modules($name = NULL, $reset = FALSE) { return features_get_info('module', $name, $reset); } /** * Returns the array of supported components. * * @param $feature_source * If set to to TRUE return feature sources only. * @return An array of component labels keyed by the component names. */ function features_get_components($feature_source = FALSE) { features_include(); $components = array(); foreach (module_implements('features_api') as $module) { $info = module_invoke($module, 'features_api'); if ($feature_source) { // Gather only components that are declared as feature sources. foreach ($info as $k => $v) { if ($v['feature_source']) { $components[$k] = $v; } } } else { $components = array_merge($components, $info); } } return $components; } /** * Like module_invoke() but for arbitrary callback base names. */ function features_invoke() { $args = func_get_args(); $base = $args[0]; $hook = $args[1]; unset($args[0], $args[1]); $function = $base .'_'. $hook; if (features_hook($base, $hook)) { return call_user_func_array($function, $args); } } /** * Like module_invoke_all() but for arbitrary callback base names. */ function features_invoke_all() { $args = func_get_args(); $hook = $args[0]; unset($args[0]); $return = array(); foreach (features_get_components() as $component => $info) { $result = features_invoke($component, $hook); if (isset($result) && is_array($result)) { $return = array_merge_recursive($return, $result); } else if (isset($result)) { $return[] = $result; } } return $return; } /** * Like module_hook() but for arbitrary callback base names. */ function features_hook($base, $hook) { return function_exists($base .'_'. $hook); } /** * Enables and installs an array of modules, ignoring those * already enabled & installed. Consider this a helper or * extension to drupal_install_modules(). * * @param $modules * An array of modules to install. * @param $reset * Clear the module info cache. */ function features_install_modules($modules) { module_load_include('inc', 'features', 'features.export'); $files = module_rebuild_cache(); // Build maximal list of dependencies. $install = array(); foreach ($modules as $name) { if ($file = $files[$name]) { $install[] = $name; if (!empty($file->info['dependencies'])) { $install = array_merge($install, _features_export_maximize_dependencies($file->info['dependencies'])); } } } // Filter out enabled modules. $enabled = array_filter($install, 'module_exists'); $install = array_diff($install, $enabled); if (!empty($install)) { // Make sure the install API is available. $install = array_unique($install); include_once './includes/install.inc'; drupal_install_modules($install); } } /** * Wrapper around features_get_info() that returns an array * of module info objects that are features. */ function features_get_features($name = NULL, $reset = FALSE) { return features_get_info('feature', $name, $reset); } /** * Helper for retrieving info from system table. */ function features_get_info($type = 'module', $name = NULL, $reset = FALSE) { static $cache; if (!isset($cache)) { $cache = cache_get('features_module_info'); } if (empty($cache) || $reset) { $data = array(); $ignored = variable_get('features_ignored_orphans', array()); $result = db_query("SELECT filename, name, type, status, throttle, schema_version FROM {system} WHERE type = 'module' ORDER BY name ASC"); while ($row = db_fetch_object($result)) { // If module is no longer enabled, remove it from the ignored orphans list. if (in_array($row->name, $ignored, TRUE) && !$row->status) { $key = array_search($row->name, $ignored, TRUE); unset($ignored[$key]); } // Parse and allow modules to alter the info file if necessary. $row->info = drupal_parse_info_file(dirname($row->filename) .'/'. $row->name .'.info'); if (!empty($row->info)) { drupal_alter('system_info', $row->info, $row); if (!empty($row->info['features'])) { $data['feature'][$row->name] = $row; } $data['module'][$row->name] = $row; } } variable_set('features_ignored_orphans', $ignored); cache_set("features_module_info", $data); $cache = new stdClass(); $cache->data = $data; } if (!empty($name)) { return !empty($cache->data[$type][$name]) ? $cache->data[$type][$name] : array(); } return !empty($cache->data[$type]) ? $cache->data[$type] : array(); } /** * Generate an array of feature dependencies that have been orphaned. */ function features_get_orphans($reset = FALSE) { static $orphans; if (!isset($orphans) || $reset) { module_load_include('inc', 'features', 'features.export'); $orphans = array(); // Build a list of all dependencies for enabled and disabled features. $dependencies = array('enabled' => array(), 'disabled' => array()); $features = features_get_features(); foreach ($features as $feature) { $key = module_exists($feature->name) ? 'enabled' : 'disabled'; $dependencies[$key] = array_merge($dependencies[$key], _features_export_maximize_dependencies($feature->info['dependencies'])); } $dependencies['enabled'] = array_unique($dependencies['enabled']); $dependencies['disabled'] = array_unique($dependencies['disabled']); // Find the list of orphaned modules. $orphaned = array_diff($dependencies['disabled'], $dependencies['enabled']); $orphaned = array_intersect($orphaned, module_list(FALSE, FALSE)); $orphaned = array_diff($orphaned, drupal_required_modules()); $orphaned = array_diff($orphaned, array('features')); // Build final list of modules that can be disabled. $modules = features_get_modules(NULL, TRUE); $enabled = module_list(); _module_build_dependencies($modules); foreach ($orphaned as $module) { if (!empty($modules[$module]->info['dependents'])) { // Determine whether any dependents are actually enabled. $dependents = array_intersect($modules[$module]->info['dependents'], $enabled); if (empty($dependents)) { $info = features_get_modules($module); $orphans[$module] = $info; } } } } return $orphans; } /** * Detect potential conflicts between any features that provide * identical components. */ function features_get_conflicts($reset = FALSE) { $conflicts = array(); $component_info = features_get_components(); $map = features_get_component_map($reset); foreach ($map as $type => $components) { foreach ($components as $component => $modules) { if (isset($component_info[$type]['duplicates']) && $component_info[$type]['duplicates'] == FEATURES_DUPLICATES_ALLOWED) { continue; } else if (count($modules) > 1) { foreach ($modules as $module) { if (!isset($conflicts[$module])) { $conflicts[$module] = array(); } foreach ($modules as $m) { if ($m != $module) { $conflicts[$module][$m] = $m; } } } } } } return $conflicts; } /** * Provide a component to feature map. */ function features_get_component_map($reset = FALSE) { static $map; if (!isset($map) || $reset) { $map = array(); $features = features_get_features('', TRUE); foreach ($features as $feature) { foreach ($feature->info['features'] as $type => $components) { if (!isset($map[$type])) { $map[$type] = array(); } foreach ($components as $component) { $map[$type][$component][] = $feature->name; } } } } return $map; } /** * Simple wrapper returns the status of a module. */ function features_get_module_status($module) { if (module_exists($module)) { return FEATURES_MODULE_ENABLED; } else if (features_get_modules($module)) { return FEATURES_MODULE_DISABLED; } else { return FEATURES_MODULE_MISSING; } } /** * Menu title callback. */ function features_get_feature_title($feature) { return $feature->info['name']; } /** * Menu access callback for whether a user should be able to access * override actions for a given feature. */ function features_access_override_actions($feature) { static $access = array(); if (!isset($access[$feature->name])) { // Set a value first. We may get called again from within features_detect_overrides(). $access[$feature->name] = FALSE; features_include(); module_load_include('inc', 'features', 'features.export'); $access[$feature->name] = in_array(features_get_storage($feature->name), array(FEATURES_OVERRIDDEN, FEATURES_NEEDS_REVIEW)) && user_access('administer features'); } return $access[$feature->name]; } /** * Implementation of hook_form_alter() for system_modules form. */ function features_form_system_modules_alter(&$form) { features_rebuild(); } /** * Restore the specified modules to the default state. */ function _features_restore($op, $items = array()) { // Since we can't ensure that users will run update.php immediately after // updating the features codebase, we must check the schema version explicitly // to ensure that we will not blow away any overrides. module_load_install('features'); if (drupal_get_installed_schema_version('features', TRUE) < 6101) { return; } module_load_include('inc', 'features', 'features.export'); features_include(); switch ($op) { case 'revert': $restore_states = array(FEATURES_OVERRIDDEN, FEATURES_REBUILDABLE, FEATURES_NEEDS_REVIEW); $restore_hook = 'features_revert'; $log_action = 'Revert'; break; case 'rebuild': $restore_states = array(FEATURES_REBUILDABLE); $restore_hook = 'features_rebuild'; $log_action = 'Rebuild'; break; } if (empty($items)) { $states = features_get_component_states(); foreach ($states as $module_name => $components) { foreach ($components as $component => $state) { if (in_array($state, $restore_states)) { $items[$module_name][] = $component; } } } } foreach ($items as $module_name => $components) { foreach ($components as $component) { if (features_hook($component, $restore_hook)) { // Set a semaphore to prevent other instances of the same script from running concurrently. watchdog('features', '@actioning @module_name / @component.', array('@action' => $log_action, '@component' => $component, '@module_name' => $module_name)); features_semaphore('set', $component); features_invoke($component, $restore_hook, $module_name); // If the script completes, remove the semaphore and set the code signature. features_semaphore('del', $component); features_set_signature($module_name, $component); watchdog('features', '@action completed for @module_name / @component.', array('@action' => $log_action, '@component' => $component, '@module_name' => $module_name)); } } } } /** * Wrapper around _features_restore(). */ function features_revert($revert = array()) { return _features_restore('revert', $revert); } /** * Wrapper around _features_restore(). */ function features_rebuild($rebuild = array()) { return _features_restore('rebuild', $rebuild); } /** * Returns a links array in the theme_links() format for the features menu. * * @return * Array of links. */ function features_menu_links($level = 0, $reset = FALSE) { static $links; if (!isset($links[$level]) || $reset) { if (!isset($links)) { $links = array(); } $links[$level] = menu_navigation_links('features', $level); } drupal_alter('features_menu_links', $links[$level]); return $links[$level]; } /** * Theme functions ==================================================== */ /** * Preprocess function for features links. */ function features_preprocess_page(&$vars) { if (variable_get('menu_primary_links_source', 'primary-links') == 'features') { $vars['primary_links'] = features_menu_links(); } if (variable_get('menu_secondary_links_source', 'secondary-links') == 'features') { if (variable_get('menu_secondary_links_source', 'primary-links') == 'features') { $vars['secondary_links'] = features_menu_links(1); } else { $vars['secondary_links'] = features_menu_links(); } } } /** * Utility functions ================================================== */ /** * Represent the current state of permissions as a role name to role/perm array. */ function _features_get_roles() { $roles = array(); $result = db_query("SELECT r.rid, r.name, p.perm FROM {role} r LEFT JOIN {permission} p ON r.rid = p.rid"); while ($row = db_fetch_object($result)) { $roles[$row->name] = array( 'rid' => $row->rid, 'perm' => explode(', ', $row->perm), ); } return $roles; }