You are here

configuration.module in Configuration Management 7

Module file for the configuration module, which enables the capture and management of configuration in Drupal.

File

configuration.module
View source
<?php

/**
 * @file
 * Module file for the configuration module, which enables the capture and
 * management of configuration in Drupal.
 */

/**
 * A bit flag used to let us know if a configuration is the same in both the
 * activestore and the datastore.
 */
define('CONFIGURATION_IN_SYNC', 0x0);

/**
 * A bit flag used to let us know if a configuration was overridden as a result
 * of changing the activestore directly. (config changes via the UI)
 */
define('CONFIGURATION_ACTIVESTORE_OVERRIDDEN', 0x1);

/**
 * A bit flag used to let us know if a configuration was overridden as a result
 * of changing the datastore directly.
 *
 * This flag is set when configuration changes are detected via the "Check for
 * new configurations" button on settings screen.
 */
define('CONFIGURATION_DATASTORE_OVERRIDDEN', 0x2);

/**
 * Both activestore and datastore have a changed.  We have a conflict.
 */
define('CONFIGURATION_CONFLICT', CONFIGURATION_ACTIVESTORE_OVERRIDDEN | CONFIGURATION_DATASTORE_OVERRIDDEN);

/**
 * A bit flag used to let us know if a configuration is only in the datastore
 * and is also being tracked in config.export.
 */
define('CONFIGURATION_TRACKED_DATASTORE_ONLY', 0x4);

/**
 * A bit flag used to let us know if a configuration is only in the datastore.
 */
define('CONFIGURATION_DATASTORE_ONLY', 0x8);

/**
 * If a configuration no longer exists in the active store and still exists in config tracking.
 */
define('CONFIGURATION_DELETE', 0x10);

/**
 * If a configuration requires a dependency that isn't installed.
 */
define('CONFIGURATION_DEPENDENCY_REQUIRED', 0x100);

/**
 * A bit flag used to let us know if a configuration was overridden.
 */
define('CONFIGURATION_OVERRIDDEN', CONFIGURATION_ACTIVESTORE_OVERRIDDEN | CONFIGURATION_DATASTORE_OVERRIDDEN);

// Features constants below.
define('CONFIGURATION_MODULE_ENABLED', 1);
define('CONFIGURATION_MODULE_DISABLED', 0);
define('CONFIGURATION_MODULE_MISSING', -1);
define('CONFIGURATION_REBUILDABLE', -1);
define('CONFIGURATION_NEEDS_REVIEW', 2);
define('CONFIGURATION_REBUILDING', 3);
define('CONFIGURATION_DISABLED', 5);

// Duration of rebuild semaphore: 10 minutes.
define('CONFIGURATION_SEMAPHORE_TIMEOUT', 10 * 60);

/**
 * Components with this 'default_file' flag will have exports written to the
 * common defaults file 'MODULENAME.features.inc'. This is the default
 * behavior.
 */
define('CONFIGURATION_DEFAULTS_INCLUDED_COMMON', 0);

/**
 * Components with this 'default_file' flag will have exports written to a
 * defaults based on the component name like 'MODULENAME.features.COMPONENT-NAME.inc'.
 * Any callers to this component's defaults hook must call
 * features_include_defaults('component') in order to include this file.
 */
define('CONFIGURATION_DEFAULTS_INCLUDED', 1);

/**
 * Components with this 'default_file' flag must specify a filename for their
 * exports. Additionally a stub will NOT be written to 'MODULENAME.features.inc'
 * allowing the file to be included directly by the implementing module.
 */
define('CONFIGURATION_DEFAULTS_CUSTOM', 2);

/**
 * Components with this 'duplicates' flag may not have multiple features provide the
 * same component key in their info files. This is the default behavior.
 */
define('CONFIGURATION_DUPLICATES_CONFLICT', 0);

/**
 * Components with this 'duplicates' flag are allowed to have multiple features
 * provide the same component key in their info files.
 */
define('CONFIGURATION_DUPLICATES_ALLOWED', 1);

/**
 * Implements hook_menu().
 */
function configuration_menu() {
  $items['admin/config/system/configuration'] = array(
    'title' => 'Configuration Management',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'configuration_tracking_form',
    ),
    'access arguments' => array(
      'access configuration management',
    ),
    'description' => 'Configuration Management for Drupal 7',
    'type' => MENU_NORMAL_ITEM,
    'file' => 'configuration.admin.inc',
  );
  $items['admin/config/system/configuration/tracking'] = array(
    'title' => 'Tracking',
    'weight' => -1,
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items['admin/config/system/configuration/notracking'] = array(
    'title' => 'Not tracking',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'configuration_notracking_form',
    ),
    'access arguments' => array(
      'access configuration management',
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'configuration.admin.inc',
  );

  // $items['admin/config/system/configuration/activate'] = array(
  //    'title' => 'Activate new configuration'),
  //    'page callback' => 'drupal_get_form',
  //    'page arguments' => array('configuration_activate_form'),
  //    'access arguments' => array('access configuration management'),
  //    'type' => MENU_LOCAL_TASK,
  //    'file' => 'configuration.admin.inc',
  //    'weight' => 5,
  //  );
  $items['admin/config/system/configuration/migrate'] = array(
    'title' => 'Migrate',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'configuration_migrate_form',
    ),
    'access arguments' => array(
      'access configuration management',
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'configuration.admin.inc',
    'weight' => 7,
  );
  $items['admin/config/system/configuration/migrate/export'] = array(
    'title' => 'Export',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => 8,
  );
  $items['admin/config/system/configuration/migrate/import'] = array(
    'title' => 'Import',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'configuration_import_form',
    ),
    'access arguments' => array(
      'access configuration management',
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'configuration.admin.inc',
    'weight' => 9,
  );
  $items['admin/config/system/configuration/settings'] = array(
    'title' => 'Settings',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'configuration_settings_form',
    ),
    'access arguments' => array(
      'access configuration management',
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'configuration.admin.inc',
    'weight' => 10,
  );
  $items['admin/config/system/configuration/config/%/%/delete'] = array(
    'title' => 'Are you sure you want to stop tracking this config?',
    'page callback' => 'configuration_confirm_delete_page',
    'access arguments' => array(
      'access configuration management',
    ),
    'file' => 'configuration.admin.inc',
    'page arguments' => array(
      5,
      6,
    ),
  );
  if (module_exists('diff')) {
    $items['admin/config/system/configuration/%/%/diff'] = array(
      'title' => 'Review overrides',
      'description' => 'Compare activestore and datastore.',
      'page callback' => 'configuration_diff',
      'page arguments' => array(
        4,
        5,
      ),
      'access arguments' => array(
        'access configuration management',
      ),
      'file' => 'configuration.admin.inc',
    );
  }
  return $items;
}

/**
 * Implements hook_permission().
 */
function configuration_permission() {
  return array(
    'access configuration management' => array(
      'title' => t('Administer Configuration Management'),
      'description' => t('Administer the exports and activation of configuration.'),
    ),
  );
}

/**
 * Implements hook_theme().
 */
function configuration_theme($existing, $type, $theme, $path) {
  $base = array(
    'path' => drupal_get_path('module', 'configuration') . '/theme',
    'file' => 'theme.inc',
  );
  $items = array();
  $items['configuration_form_buttons'] = array(
    'render element' => 'element',
  ) + $base;
  return $items;
}

/**
 * Implements hook_stream_wrappers().
 */
function configuration_stream_wrappers() {
  return array(
    'config' => array(
      'name' => t('Configuration files'),
      'class' => 'ConfigurationStreamWrapper',
      'description' => t('Configuration files export directory.'),
      'type' => STREAM_WRAPPERS_LOCAL_HIDDEN,
    ),
  );
}

/**
 * Implements hook_init().
 */
function configuration_init() {

  // Don't create messages on intermediate submit handler pages.
  if (arg(0) == 'admin' && variable_get('configuration_display_messages', TRUE) && empty($_POST)) {
    $overridden = configuration_get_configuration('overridden');
    if (user_access('access configuration management') && $overridden !== 0) {
      if ($overridden | (CONFIGURATION_ACTIVESTORE_OVERRIDDEN | CONFIGURATION_DATASTORE_OVERRIDDEN)) {
        drupal_set_message(t('Configurations are out of sync and need to be either !write.', array(
          '!write' => l(t('activated or written to file'), 'admin/config/system/configuration/tracking'),
        )), 'warning', FALSE);
      }
      elseif ($overridden & CONFIGURATION_DATASTORE_ONLY) {
        drupal_set_message(t('You have new Configurations that need to be activated.  !click_here to activate your configurations.', array(
          '!click_here' => l(t('Click here'), 'admin/config/system/configuration/activate'),
        )), 'warning', FALSE);
      }
      elseif ($overridden == CONFIGURATION_CONFLICT) {
        drupal_set_message(t('You have conflicts with your Configurations. Activating configurations will overwrite what is in the activestore, or you can write what is in the activestore back to datastore.  !click_here to resolve.', array(
          '!click_here' => l(t('Click here'), 'admin/config/system/configuration/activate'),
        )), 'error', FALSE);
      }
    }
  }
}

/**
 * Implements hook_modules_enabled().
 */
function hook_modules_enabled($modules) {

  // When diff module is enabled after this module was enabled
  // it is necessary to flush the menu cache in order to enable
  // the /diff tab.
  if (in_array('diff', $modules)) {
    drupal_flush_all_caches();
  }
}

/**
 * Returns the array of supported components.
 *
 * @param $configuration_source
 *   If set to to TRUE return configuration sources only.
 * @return An array of component labels keyed by the component names.
 */
function configuration_get_components($configuration_source = FALSE, $reset = FALSE) {
  configuration_include();
  static $components_all;
  static $components_source;
  if (!isset($components_all) || $reset) {
    $components_all = $components_source = array();
    foreach (module_implements('configuration_api') as $module) {
      $info = module_invoke($module, 'configuration_api');
      foreach ($info as $k => $v) {
        $components_all[$k] = $v;
        if (!empty($v['configuration_source'])) {
          $components_source[$k] = $v;
        }
      }
    }
  }
  return $configuration_source ? $components_source : $components_all;
}

/**
 * Load includes for any modules that implement the configuration API and
 * load includes for those provided by configuration.
 */
function configuration_include($reset = FALSE) {
  static $once;
  if (!isset($once) || $reset) {
    $once = TRUE;

    // Check for implementing modules and make necessary inclusions.
    foreach (module_implements('configuration_api') as $module) {
      $info = module_invoke($module, 'configuration_api');
      foreach ($info as $component) {
        if (isset($component['file'])) {
          require_once DRUPAL_ROOT . '/' . $component['file'];
        }
      }
    }

    // configuration provides integration on behalf of these modules.
    // The configuration include provides handling for the configuration dependencies.
    // Note that ctools is placed last because it implements hooks "dynamically" for other modules.
    $modules = array(
      'configuration',
      'block',
      'context',
      'field',
      'filter',
      'image',
      'menu',
      'node',
      'taxonomy',
      'user',
      'views',
      'ctools',
    );
    foreach (array_filter($modules, 'module_exists') as $module) {
      if (!module_hook($module, 'configuration_api')) {
        module_load_include('inc', 'configuration', "includes/configuration.{$module}");
      }
    }

    // Clear static cache, since we've now included new implementers.
    module_implements('configuration_api', FALSE, TRUE);
  }
}

/**
 * Load features includes for all components that require includes before
 * collecting defaults.
 */
function configuration_include_defaults($components = NULL, $reset = FALSE) {
  static $included = array();
  static $include_components;

  // Build an array of components that require inclusion:
  // Views, CTools components and those using FEATURES_DEFAULTS_INCLUDED.
  if (!isset($include_components) || $reset) {
    $include_components = configuration_get_components();
    foreach ($include_components as $component => $info) {
      if ($component !== 'views' && !isset($info['api']) && (!isset($info['default_file']) || $info['default_file'] !== CONFIGURATION_DEFAULTS_INCLUDED)) {
        unset($include_components[$component]);
      }
    }
  }

  // If components are specified, only include for the specified components.
  if (isset($components)) {
    $components = is_array($components) ? $components : array(
      $components => $components,
    );
  }
  else {
    $components = $include_components;
  }
  foreach (array_keys($components) as $component) {
    if (isset($include_components[$component]) && (!isset($included[$component]) || $reset)) {
      $info = $include_components[$component];

      // Inclusion of defaults for Views.
      if ($component === 'views') {
        views_include('view');
        views_include_handlers();
        views_module_include('views_default.inc');
      }
      elseif (isset($info['api'], $info['module'], $info['current_version'])) {
        ctools_include('plugins');
        ctools_plugin_api_include($info['module'], $info['api'], $info['current_version'], $info['current_version']);
        $file = 'config://configuration.' . $component . '.inc';
        if (is_file($file)) {
          include_once drupal_realpath($file);
        }
      }
      else {
        $file = "config://configuration.{$component}.inc";
        if (is_file($file)) {
          include_once drupal_realpath($file);
        }
      }
      $included[$component] = TRUE;
    }
  }

  // Inclusion of the catch all config file
  $file = "config://configuration.inc";
  if (is_file($file)) {
    include_once drupal_realpath($file);
  }
}

/**
 * Wrapper around configuration_get_info() that returns an array
 * of module info objects that are configuration.
 */
function configuration_get_configurations($name = NULL, $reset = FALSE) {
  return configuration_get_info('configuration', $name, $reset);
}

/**
 * Helper for retrieving info from system table.
 */
function configuration_get_info($type = 'module', $name = NULL, $reset = FALSE) {
  static $cache;
  if (!isset($cache)) {
    $cache = cache_get('configuration_module_info');
  }
  if (empty($cache) || $reset) {
    $data = array();
    $ignored = variable_get('configuration_ignored_orphans', array());
    $files = system_rebuild_module_data();
    foreach ($files as $row) {

      // 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]);
      }
      if (!empty($row->info['configuration'])) {

        // Fix css/js paths
        if (!empty($row->info['stylesheets'])) {
          foreach ($row->info['stylesheets'] as $media => $css) {
            $row->info['stylesheets'][$media] = array_keys($css);
          }
        }
        if (!empty($row->info['scripts'])) {
          $row->info['scripts'] = array_keys($row->info['scripts']);
        }
        $data['configuration'][$row->name] = $row;
      }
      $data['module'][$row->name] = $row;
    }

    // Sort configuration according to dependencies.
    // @see install_profile_modules()
    $required = array();
    $non_required = array();
    $sorted = array();
    foreach ($required + $non_required as $module => $weight) {
      $sorted[$module] = $data['configuration'][$module];
    }
    $data['configuration'] = $sorted;
    variable_set('configuration_ignored_orphans', $ignored);
    cache_set("configuration_module_info", $data);
    $cache = new stdClass();
    $cache->data = $data;
  }
  if (!empty($name)) {
    return !empty($cache->data[$type][$name]) ? clone $cache->data[$type][$name] : array();
  }
  return !empty($cache->data[$type]) ? $cache->data[$type] : array();
}

/**
 * Feature object loader.
 */
function configuration_load($name, $reset = FALSE) {
  return configuration_get_configuration($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 configuration_get_modules($name = NULL, $reset = FALSE) {
  return configuration_get_info('module', $name, $reset);
}

/**
 * Invoke a component callback.
 */
function configuration_invoke($component, $callback) {
  $args = func_get_args();
  unset($args[0], $args[1]);

  // Append the component name to the arguments.
  $args[] = $component;
  if ($function = configuration_hook($component, $callback)) {
    return call_user_func_array($function, $args);
  }
}

/**
 * Checks whether a component implements the given hook.
 *
 * @return
 *   The function implementing the hook, or FALSE.
 */
function configuration_hook($component, $hook, $reset = FALSE) {
  $info =& drupal_static(__FUNCTION__);
  if (!isset($info) || $reset) {
    $info = module_invoke_all('configuration_api');
  }

  // Determine the function callback base.
  $base = isset($info[$component]['base']) ? $info[$component]['base'] : $component;
  return function_exists($base . '_' . $hook) ? $base . '_' . $hook : FALSE;
}

/**
 * Save changes to track configuration.
 *
 * @param $configs The $config array to be saved. The array is keyed by component with
 * the value being another array keyed by configuration name and value either being a boolean
 * or the configuration name of any parents.
 *   $configs = array(
 *     component-name => array(
 *       config-name => dependency array or boolean
 *     )
 *   );
 */
function configuration_save($configs) {
  configuration_include();
  module_load_include('inc', 'configuration', 'configuration.export');
  $current_config = configuration_get_configuration();

  // Check to see if there is anything in datastore that would be lost
  if (!configuration_check_changed(CONFIGURATION_DATASTORE_ONLY, array_keys($configs), $current_config)) {
    return FALSE;
  }
  try {
    $transaction = db_transaction();
    $saved = array();
    foreach ($configs as $component => $config) {
      foreach ($config as $name => $on) {
        if ($on) {

          // Using the $on state for dual purpose. When configuration_save is
          // called with dependencies from the $pipe, $on will hold the
          // dependencies.  If this is called from the standard management
          // screen, $on is a boolean and we need to set empty dependencies.
          $deps = is_numeric($on) || $on == '' ? array(
            'parent' => '',
            'modules' => '',
          ) : $on;
          $function = "configuration_hash_{$component}";
          $hash = $function($name);
          $record = array(
            'name' => $name,
            'owner' => $component,
            'status' => CONFIGURATION_IN_SYNC,
            'hash' => $hash,
            'parent' => $deps['parent'],
            'dependencies' => $deps['modules'],
          );

          // Track what configurations we have saved
          if (!isset($saved[$component])) {
            $saved[$component] = TRUE;
          }

          // If this is an update, set the primary key
          $pk = isset($current_config[$component][$name]) ? array(
            'name',
          ) : array();
          drupal_write_record('config_export', $record, $pk);
          $configs[$component][$name] = $name;
        }
        else {
          unset($configs[$component][$name]);
        }
      }
      if (isset($saved[$component])) {
        drupal_set_message(t('Tracking configurations for %config have been saved.', array(
          '%config' => $component,
        )));
      }
    }
    cache_clear_all('config_export', 'cache');
  } catch (Exception $e) {
    $transaction
      ->rollback();
    watchdog_exception('configuration', $e);
    throw $e;
  }

  // Write the new configuration to disk.
  configuration_write_exports(array_keys($saved));
}

/**
 * Delete a specific configuration from being tracked.
 */
function configuration_delete($cid) {
  configuration_delete_multiple(array(
    $cid,
  ));
}

/**
 * Delete a specific configuration from being tracked.
 */
function configuration_delete_multiple($cids) {
  module_load_include('inc', 'configuration', 'configuration.export');
  $transaction = db_transaction();
  try {

    // Build an array of configs to exclude when writing to file.  Even though
    // we deleted the tracking row in the config_export table, configuration
    // module looks through all the config files and will build an $export
    // array which is used to generate the configs again in rebuilt files.  By
    // passing in an exclude array we can ensure these configs do not get
    // exported again.
    $names = db_query("SELECT name, owner FROM {config_export} WHERE cid IN (" . join(',', $cids) . ")")
      ->fetchAll();
    foreach ($names as $name) {
      $exclude[$name->name] = $name->owner;
      $config[$name->owner] = array(
        $name->name => array(),
      );
    }

    // Pass the configuration I want to disable into the populate functions to
    // see what dependencies this component has.  Then check status of
    // dependencies to make sure they are in sync before rewriting the files.
    $config_populate = configuration_populate_sanitize($config);
    $export = configuration_populate($config_populate, array());

    // Track dependencies on config_export table
    _configuration_track_dependencies($export);
    $config = array_key_exists('configuration_dependency', $export) ? array_merge($config, $export['configuration_dependency']['configuration']) : $config;
    if (array_key_exists('configuration_dependency', $export) && !configuration_check_changed(CONFIGURATION_ACTIVESTORE_OVERRIDDEN, array_keys($export['configuration_dependency']['configuration']))) {
      drupal_get_messages();
      drupal_set_message(t('Unable to stop tracking. Dependent configurations that are overridden in the activestore would be written to file. Write configurations in %component component(s) first, then try again.', array(
        '%component' => join(', ', array_keys($export['configuration_dependency']['configuration'])),
      )), 'error');
      return FALSE;
    }
    if (!configuration_check_changed(CONFIGURATION_DATASTORE_ONLY | CONFIGURATION_DATASTORE_OVERRIDDEN, array_keys($config))) {
      drupal_get_messages();
      drupal_set_message(t('Unable to stop tracking. Configs would be lost when rewriting %component config file. Activate configurations in %component component(s) first, then try again.', array(
        '%component' => join(', ', array_keys($config)),
      )), 'error');
      return FALSE;
    }
    $files = configuration_export_render($export);
    foreach (array_keys($files) as $filename) {
      unlink("config://" . $filename . '.inc');
      watchdog('configuration', 'Deleted %file for rewrite.', array(
        '%file' => $filename . '.inc',
      ));
    }

    // Queue the configuration up for deletion.
    db_update('config_export')
      ->fields(array(
      'status' => CONFIGURATION_DELETE,
    ))
      ->condition('cid', $cids, 'IN')
      ->execute();
    cache_clear_all('config_export', 'cache');

    // Write the new configuration to disk.
    configuration_write_exports(array_keys($config), $exclude);

    // Delete the configuration.
    db_delete('config_export')
      ->condition('cid', $cids, 'IN')
      ->execute();
    db_update('config_export')
      ->fields(array(
      'parent' => '',
    ))
      ->condition('parent', array_keys($exclude), 'IN')
      ->execute();
    cache_clear_all('config_export', 'cache');
  } catch (Exception $e) {
    $transaction
      ->rollback();
    watchdog_exception('configuration', $e);
    throw $e;
  }
  return TRUE;
}

/**
 * Add a configuration as datastore only
 *
 * @param $component The component of the configuration.
 * @param $name The unique name of the configuration.
 */
function configuration_add_status($component, $name, $hash) {
  $current_config = configuration_get_configuration();
  $record = array(
    'name' => $name,
    'owner' => $component,
    'status' => CONFIGURATION_DATASTORE_ONLY,
    'hash' => $hash,
  );
  drupal_write_record('config_export', $record);
  cache_clear_all('config_export', 'cache');
}

/**
 * Mark a configuration as default, overridden, etc...
 *
 * @param $component The name of the component
 * @param $name The name of the configuration.
 * @param $status The status of the configuration
 */
function configuration_set_status($component, $name, $status) {

  // dsm("Set $name to $status");
  // dsm(debug_backtrace());
  db_update('config_export')
    ->fields(array(
    'status' => $status,
  ))
    ->condition('name', $name)
    ->condition('owner', $component)
    ->execute();
}

/**
 * Update the hash of a configuration
 *
 * @param $component The name of the component
 * @param $name The name of the configuration.
 * @param $hash The hash of the configuration
 */
function configuration_set_hash($component, $name, $hash) {

  // dsm("Set $name to $status");
  // dsm(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS));
  db_update('config_export')
    ->fields(array(
    'hash' => $hash,
  ))
    ->condition('name', $name)
    ->condition('owner', $component)
    ->execute();
}
function configuration_get_status($component, $name) {
  $config = configuration_get_configuration();
  return $config[$component][$name]['status'];
}

/**
 * Conform the $config array to something that configuration_populate() can use.
 */
function configuration_populate_sanitize($config) {
  $sanitized = array();
  foreach ($config as $component => $value) {
    if (is_array($value)) {
      foreach ($value as $name => $settings) {
        $sanitized[$component][$name] = $name;
      }
    }
  }
  return $sanitized;
}

/**
 * Retrieve configurations that are being tracked
 *
 * @param $component
 *   The name of a specific component to return
 */
function configuration_get_configuration($component = NULL) {

  // Get static variable that we can access across this request.
  $from_activestore =& drupal_static('configuration_from_activestore');
  if (!($cache = cache_get('config_export'))) {
    $config = configuration_build_configuration_status();
    if (!$from_activestore) {

      // Check for new configurations before building the cache again.
      configuration_check_configurations();

      // Update the config in case anything was discovered in the chec
      $config = configuration_build_configuration_status();
    }
    return isset($component) && isset($config[$component]) ? $config[$component] : $config;
  }
  return isset($component) && isset($cache->data[$component]) ? $cache->data[$component] : $cache->data;
}
function configuration_build_configuration_status() {
  $result = db_query("SELECT name, owner, status, hash, parent, dependencies FROM {config_export} WHERE status <> :status ORDER BY owner, name", array(
    ':status' => CONFIGURATION_DELETE,
  ))
    ->fetchAll();
  $config = array(
    'overridden' => 0,
  );
  foreach ($result as $comp) {
    $config[$comp->owner][$comp->name] = array(
      'status' => $comp->status,
      'hash' => $comp->hash,
      'parent' => $comp->parent,
      'dependencies' => $comp->dependencies,
    );

    // If anything is overriden, set a flag.
    if ($comp->status & CONFIGURATION_ACTIVESTORE_OVERRIDDEN) {
      $config['overridden'] = $config['overridden'] | $comp->status;
    }
    elseif ($comp->status & CONFIGURATION_DATASTORE_OVERRIDDEN) {
      $config['overridden'] = $config['overridden'] | $comp->status;
    }
    elseif ($comp->status & CONFIGURATION_DATASTORE_ONLY) {
      $config['overridden'] = $config['overridden'] | $comp->status;
    }
  }
  cache_set('config_export', $config);
  return $config;
}

/**
 * Restore the specified modules to the default state.
 */
function _configuration_restore($op, $items = array(), $module_name = 'configuration') {
  module_load_include('inc', 'configuration', 'configuration.export');
  configuration_include();
  switch ($op) {
    case 'revert':

      // $restore_states = array(FEATURES_OVERRIDDEN, FEATURES_REBUILDABLE, FEATURES_NEEDS_REVIEW);
      $restore_hook = 'configuration_revert';
      $log_action = 'Revert';
      break;
    case 'rebuild':

      // $restore_states = array(FEATURES_REBUILDABLE);
      $restore_hook = 'configuration_rebuild';
      $log_action = 'Rebuild';
      break;
    case 'disable':
      $restore_hook = 'configuration_disable_configuration';
      $log_action = 'Disable';
      break;
    case 'enable':
      $restore_hook = 'configuration_enable_configuration';
      $log_action = 'Enable';
      break;
  }
  if (empty($items)) {

    // Drush may execute a whole chain of commands that may trigger configuration
    // rebuilding multiple times during a single request. Make sure we do not
    // rebuild the same cached list of modules over and over again by setting
    // $reset to TRUE.
    // Note: this may happen whenever more than one configuration will be enabled
    // in chain, for example also using configuration_install_modules().
    $states = configuration_get_component_states(array(), $op == 'rebuild', defined('DRUSH_BASE_PATH'));
    foreach ($states as $module_name => $components) {
      foreach ($components as $component => $state) {
        if (in_array($state, $restore_states)) {
          $items[$module_name][] = $component;
        }
      }
    }
  }
  foreach ($items as $component => $checked) {
    if (configuration_hook($component, $restore_hook)) {
      $selected = array_filter($checked);

      // Set a semaphore to prevent other instances of the same script from running concurrently.
      watchdog('configuration', '@actioning @module_name / @component.', array(
        '@action' => $log_action,
        '@component' => $component,
        '@module_name' => $module_name,
      ));
      configuration_semaphore('set', $component);
      configuration_invoke($component, $restore_hook, array_keys($selected), $module_name);

      // If the script completes, remove the semaphore and set the code signature.
      configuration_semaphore('del', $component);
      configuration_set_signature($module_name, $component);
      watchdog('configuration', '@action completed for @module_name / @component.', array(
        '@action' => $log_action,
        '@component' => $component,
        '@module_name' => $module_name,
      ));
    }
  }
}

/**
 * Wrapper around _configuration_restore().
 */
function configuration_revert($revert = array(), $module_name = 'configuration') {
  return _configuration_restore('revert', $revert, $module_name);
}

/**
 * Wrapper around _configuration_restore().
 */
function configuration_rebuild($rebuild = array()) {
  return _configuration_restore('rebuild', $rebuild);
}

/**
 * Wrapper around _configuration_restore().
 */
function configuration_disable_configuration($module) {
  $configuration = configuration_load($module);
  $items[$module] = array_keys($configuration->info['configuration']);
  return _configuration_restore('disable', $items);
}

/**
 * Wrapper around _configuration_restore().
 */
function configuration_enable_configuration($module) {
  $configuration = configuration_load($module);
  $items[$module] = array_keys($configuration->info['configuration']);
  return _configuration_restore('enable', $items);
}

/**
 * Check to see if a configuration is tracked
 *
 * @return Returns the component for the tracked configuration
 */
function configuration_is_tracked($component, $name) {
  $config = configuration_get_configuration();
  if (isset($config[$component][$name])) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Check each configuration that is being tracked and determine if anything
 * has been overridden.  Not able to just run a diff on an entire file because
 * we need to know which specific configurations are out of sync.  Log any
 * results as CONFIGURATION_DATASTORE_OVERRIDDEN
 */
function configuration_check_configurations($reset = FALSE) {
  if ($reset) {
    cache_clear_all('config_export', 'cache');
  }
  module_load_include('inc', 'configuration', 'configuration.export');
  configuration_include();
  $config = configuration_get_configuration();
  foreach ($config as $component => $info) {
    if (is_array($info)) {

      // Process the rest of configurations individually
      foreach ($info as $name => $settings) {
        $function = "configuration_check_{$component}";
        if (function_exists($function)) {
          call_user_func_array($function, array(
            $name,
          ));
        }
        else {
          drupal_set_message(t('Unable to process %component. You may have an unmet dependency. Enable the required modules to track this component.', array(
            '%component' => $component,
          )), 'warning', FALSE);
        }
      }
    }
  }
  if ($reset) {
    _configuration_write_export_table();
    cache_clear_all('config_export', 'cache');
  }
  configuration_find_new();
}

/**
 * Import the config.export file on filesystem into the database
 *
 * This will take the contents of the file on disk and import it into the db.
 */
function _configuration_write_export_table() {
  $config_export = drupal_parse_info_file("config://config.export");
  $cids = array();
  if (isset($config_export['config']) && is_array($config_export['config'])) {
    foreach ($config_export['config'] as $component => $config) {
      foreach ($config as $identifier => $info) {
        $row = db_query("SELECT cid, name, status, hash FROM {config_export} WHERE name = :name AND owner = :owner", array(
          ':name' => $identifier,
          ':owner' => $component,
        ))
          ->fetchObject();

        // Add a new record to the config_export table as TRACKED_DATASTORE_ONLY
        if (!is_object($row)) {
          $record = array(
            'name' => $identifier,
            'owner' => $component,
            'status' => CONFIGURATION_TRACKED_DATASTORE_ONLY,
            'hash' => $info['hash'],
            'parent' => $info['parent'],
            'dependencies' => $info['dependencies'],
          );
          drupal_write_record('config_export', $record);
          $cids[] = $record['cid'];
          continue;
        }

        // This row exists, store the row id so it is not deleted
        $cids[] = $row->cid;

        // If the hash in the config.export file is different than what we have in
        // the database, then the datastore was overridden.
        if ($info['hash'] && $info['hash'] != $row->hash) {
          db_update('config_export')
            ->fields(array(
            'hash' => 'changed-configuration',
            'parent' => $info['parent'],
            'dependencies' => $info['dependencies'],
            'status' => CONFIGURATION_DATASTORE_OVERRIDDEN,
          ))
            ->condition('cid', $row->cid)
            ->execute();
        }
      }
    }
    $old_config = db_select('config_export', 'c')
      ->fields('c');
    $removed = db_delete('config_export');
    if (!empty($cids)) {
      $old_config
        ->condition('cid', $cids, 'NOT IN');
      $removed
        ->condition('cid', $cids, 'NOT IN');
    }
    $old_config = $old_config
      ->execute()
      ->fetchAll();
    $removed = $removed
      ->execute();
    foreach ($old_config as $old) {
      $removed_configs[] = $old->name;
    }
    if ($removed) {
      drupal_set_message(t('Removed %num configuration(s). No Longer tracking %config. Not found in datastore.', array(
        '%num' => $removed,
        '%config' => join(', ', $removed_configs),
      )));
    }
  }
}

/**
 * Look for any configurations that might be new
 */
function configuration_find_new() {
  static $identifiers = array();
  $config = configuration_get_configuration();

  // Load all the files
  configuration_include_defaults();
  $components = configuration_get_components();
  unset($components['ctools']);
  foreach ($components as $component => $info) {
    $function = "configuration_{$info['default_hook']}";
    if (function_exists($function)) {
      $code = call_user_func($function);
      if (is_array($code)) {
        foreach (array_keys($code) as $name) {
          if (!in_array($name, $identifiers) && (!isset($config[$component]) || !in_array($name, array_keys($config[$component])))) {
            $identifiers[] = $name;
            configuration_add_status($component, $name, md5(serialize($code[$name])));
          }
        }
      }
    }
  }
}

/**
 * Log a message, environment agnostic.
 *
 * @param $message
 *   The message to log.
 * @param $severity
 *   The severity of the message: status, warning or error.
 */
function configuration_log($message, $severity = 'status') {
  if (function_exists('drush_verify_cli')) {
    $message = strip_tags($message);
    if ($severity == 'status') {
      $severity = 'ok';
    }
    elseif ($severity == 'error') {
      drush_set_error($message);
      return;
    }
    drush_log($message, $severity);
    return;
  }
  drupal_set_message($message, $severity, FALSE);
}

/**
 * Implements hook_hook_info().
 */
function configuration_hook_info() {
  $hooks['configuration_api'] = array(
    'group' => 'configuration',
  );
  return $hooks;
}

/**
 * Updates the status of a component after it was updated.
 *
 * @param $component
 *   The type of component, i.e node, user_permissions, taxonomy.
 * @param $identifier
 *   The identifier for the component, usually the machine name.
 * @param $items
 *   Items from the activestore.
 * @param $items_code
 *   Items loaded from the datastore.
 * @param $from_activestore
 *   @todo A static variable that make not sense now we have to remove it.
 */
function configuration_update_component_status($component, $identifier, $items, $items_code, $from_activestore) {

  // @todo re-factory component status logic.
  // If this was the previous configuration in activestore don't mark this as changed.
  $config = configuration_get_configuration();
  $status = (int) $config[$component][$identifier]['status'];
  $md5_datastore = is_array($items_code) && array_key_exists($identifier, $items_code) ? md5(serialize($items_code[$identifier])) : '';
  $md5_activestore = is_array($items) && array_key_exists($identifier, $items) ? md5(serialize($items[$identifier])) : '';

  // @todo remove $from_activestore static variable all around since make no sense now.
  $from_activestore = $md5_activestore != $config[$component][$identifier]['hash'];

  // Configs in code are not the same as what was just saved in activestore.
  if ($md5_datastore != $md5_activestore) {
    $status |= $from_activestore ? CONFIGURATION_ACTIVESTORE_OVERRIDDEN : CONFIGURATION_DATASTORE_OVERRIDDEN;
  }
  else {
    $status = CONFIGURATION_IN_SYNC;
    configuration_set_hash($component, $identifier, $md5_activestore);
  }

  // When checking for new configurations, check to see if configurations are
  // the same in datastore as last activestore. Remove the datastore overridden.
  if ($md5_datastore == $config[$component][$identifier]['hash']) {
    $status = $status & ~CONFIGURATION_DATASTORE_OVERRIDDEN;
  }
  configuration_set_status($component, $identifier, $status);

  // Store the config array in cache for easy access
  $configuration[$component][$identifier]['datastore'] = is_array($items_code) && array_key_exists($identifier, $items_code) ? $items_code[$identifier] : '';
  $configuration[$component][$identifier]['activestore'] = is_array($items) && array_key_exists($identifier, $items) ? $items[$identifier] : '';
  cache_set("{$component}:{$identifier}", $configuration, 'cache_configuration');
}

/**
 * Include the observers
 */
foreach (file_scan_directory(drupal_get_path('module', 'configuration') . '/observers', '/.*/') as $filename) {
  require $filename->uri;
}

Functions

Namesort descending Description
configuration_add_status Add a configuration as datastore only
configuration_build_configuration_status
configuration_check_configurations Check each configuration that is being tracked and determine if anything has been overridden. Not able to just run a diff on an entire file because we need to know which specific configurations are out of sync. Log any results as…
configuration_delete Delete a specific configuration from being tracked.
configuration_delete_multiple Delete a specific configuration from being tracked.
configuration_disable_configuration Wrapper around _configuration_restore().
configuration_enable_configuration Wrapper around _configuration_restore().
configuration_find_new Look for any configurations that might be new
configuration_get_components Returns the array of supported components.
configuration_get_configuration Retrieve configurations that are being tracked
configuration_get_configurations Wrapper around configuration_get_info() that returns an array of module info objects that are configuration.
configuration_get_info Helper for retrieving info from system table.
configuration_get_modules Return a module 'object' including .info information.
configuration_get_status
configuration_hook Checks whether a component implements the given hook.
configuration_hook_info Implements hook_hook_info().
configuration_include Load includes for any modules that implement the configuration API and load includes for those provided by configuration.
configuration_include_defaults Load features includes for all components that require includes before collecting defaults.
configuration_init Implements hook_init().
configuration_invoke Invoke a component callback.
configuration_is_tracked Check to see if a configuration is tracked
configuration_load Feature object loader.
configuration_log Log a message, environment agnostic.
configuration_menu Implements hook_menu().
configuration_permission Implements hook_permission().
configuration_populate_sanitize Conform the $config array to something that configuration_populate() can use.
configuration_rebuild Wrapper around _configuration_restore().
configuration_revert Wrapper around _configuration_restore().
configuration_save Save changes to track configuration.
configuration_set_hash Update the hash of a configuration
configuration_set_status Mark a configuration as default, overridden, etc...
configuration_stream_wrappers Implements hook_stream_wrappers().
configuration_theme Implements hook_theme().
configuration_update_component_status Updates the status of a component after it was updated.
hook_modules_enabled Implements hook_modules_enabled().
_configuration_restore Restore the specified modules to the default state.
_configuration_write_export_table Import the config.export file on filesystem into the database

Constants

Namesort descending Description
CONFIGURATION_ACTIVESTORE_OVERRIDDEN A bit flag used to let us know if a configuration was overridden as a result of changing the activestore directly. (config changes via the UI)
CONFIGURATION_CONFLICT Both activestore and datastore have a changed. We have a conflict.
CONFIGURATION_DATASTORE_ONLY A bit flag used to let us know if a configuration is only in the datastore.
CONFIGURATION_DATASTORE_OVERRIDDEN A bit flag used to let us know if a configuration was overridden as a result of changing the datastore directly.
CONFIGURATION_DEFAULTS_CUSTOM Components with this 'default_file' flag must specify a filename for their exports. Additionally a stub will NOT be written to 'MODULENAME.features.inc' allowing the file to be included directly by the implementing module.
CONFIGURATION_DEFAULTS_INCLUDED Components with this 'default_file' flag will have exports written to a defaults based on the component name like 'MODULENAME.features.COMPONENT-NAME.inc'. Any callers to this component's defaults hook must…
CONFIGURATION_DEFAULTS_INCLUDED_COMMON Components with this 'default_file' flag will have exports written to the common defaults file 'MODULENAME.features.inc'. This is the default behavior.
CONFIGURATION_DELETE If a configuration no longer exists in the active store and still exists in config tracking.
CONFIGURATION_DEPENDENCY_REQUIRED If a configuration requires a dependency that isn't installed.
CONFIGURATION_DISABLED
CONFIGURATION_DUPLICATES_ALLOWED Components with this 'duplicates' flag are allowed to have multiple features provide the same component key in their info files.
CONFIGURATION_DUPLICATES_CONFLICT Components with this 'duplicates' flag may not have multiple features provide the same component key in their info files. This is the default behavior.
CONFIGURATION_IN_SYNC A bit flag used to let us know if a configuration is the same in both the activestore and the datastore.
CONFIGURATION_MODULE_DISABLED
CONFIGURATION_MODULE_ENABLED
CONFIGURATION_MODULE_MISSING
CONFIGURATION_NEEDS_REVIEW
CONFIGURATION_OVERRIDDEN A bit flag used to let us know if a configuration was overridden.
CONFIGURATION_REBUILDABLE
CONFIGURATION_REBUILDING
CONFIGURATION_SEMAPHORE_TIMEOUT
CONFIGURATION_TRACKED_DATASTORE_ONLY A bit flag used to let us know if a configuration is only in the datastore and is also being tracked in config.export.