- Patch #320451 by chx, Damien Tournoud: improved Drupal's module dependency system. This helps with fields in core. Comes with tests\!
parent
dba81743a6
commit
a10731cedf
|
@ -3866,7 +3866,7 @@ function drupal_write_record($table, &$object, $primary_keys = array()) {
|
|||
* Information stored in the module.info file:
|
||||
* - name: The real name of the module for display purposes.
|
||||
* - description: A brief description of the module.
|
||||
* - dependencies: An array of shortnames of other modules this module depends on.
|
||||
* - dependencies: An array of shortnames of other modules this module requires.
|
||||
* - package: The name of the package of modules this module belongs to.
|
||||
*
|
||||
* Example of .info file:
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
<?php
|
||||
// $Id$
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Directed acyclic graph functions.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Perform a depth first sort on a directed acyclic graph.
|
||||
*
|
||||
* @param $graph
|
||||
* A three dimensional associated array, with the first keys being the names
|
||||
* of the vertices, these can be strings or numbers. The second key is
|
||||
* 'edges' and the third one are again vertices, each such key representing
|
||||
* an edge. Values of array elements do not matter.
|
||||
*
|
||||
* Example:
|
||||
* @code
|
||||
* $graph[1]['edges'][2] = 1;
|
||||
* $graph[2]['edges'][3] = 1;
|
||||
* $graph[2]['edges'][4] = 1;
|
||||
* $graph[3]['edges'][4] = 1;
|
||||
* @endcode
|
||||
*
|
||||
* On return you will also have:
|
||||
* @code
|
||||
* $graph[1]['paths'][2] = 1;
|
||||
* $graph[1]['paths'][3] = 2;
|
||||
* $graph[2]['reverse_paths'][1] = 1;
|
||||
* $graph[3]['reverse_paths'][1] = 1;
|
||||
* @endcode
|
||||
*
|
||||
* @return
|
||||
* The passed in $graph with more secondary keys filled in:
|
||||
* - 'paths': Contains a list of vertices than can be reached on a path from
|
||||
* this vertex.
|
||||
* - 'reverse_paths': Contains a list of vertices that has a path from them
|
||||
* to this vertex.
|
||||
* - 'weight': If there is a path from a vertex to another then the weight of
|
||||
* the latter is higher.
|
||||
* - 'component': Vertices in the same component have the same component
|
||||
* identifier.
|
||||
*
|
||||
* @see _drupal_depth_first_search()
|
||||
*/
|
||||
function drupal_depth_first_search(&$graph) {
|
||||
$state = array(
|
||||
// The order of last visit of the depth first search. This is the reverse
|
||||
// of the topological order if the graph is acyclic.
|
||||
'last_visit_order' => array(),
|
||||
// The components of the graph.
|
||||
'components' => array(),
|
||||
);
|
||||
// Perform the actual sort.
|
||||
foreach ($graph as $start => $data) {
|
||||
_drupal_depth_first_search($graph, $state, $start);
|
||||
}
|
||||
|
||||
// We do such a numbering that every component starts with 0. This is useful
|
||||
// for module installs as we can install every 0 weighted module in one
|
||||
// request, and then every 1 weighted etc.
|
||||
$component_weights = array();
|
||||
|
||||
foreach ($state['last_visit_order'] as $vertex) {
|
||||
$component = $graph[$vertex]['component'];
|
||||
if (!isset($component_weights[$component])) {
|
||||
$component_weights[$component] = 0;
|
||||
}
|
||||
$graph[$vertex]['weight'] = $component_weights[$component]--;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to perform a depth first sort.
|
||||
*
|
||||
* @param &$graph
|
||||
* A three dimensional associated graph array.
|
||||
* @param &$state
|
||||
* An associative array. The key 'last_visit_order' stores a list of the
|
||||
* vertices visited. The key components stores list of vertices belonging
|
||||
* to the same the component.
|
||||
* @param $start
|
||||
* An arbitrary vertex where we started traversing the graph.
|
||||
* @param &$component
|
||||
* The component of the last vertex.
|
||||
*
|
||||
* @see drupal_depth_first_search()
|
||||
*/
|
||||
function _drupal_depth_first_search(&$graph, &$state, $start, &$component = NULL) {
|
||||
// Assign new component for each new vertex, i.e. when not called recursively.
|
||||
if (!isset($component)) {
|
||||
$component = $start;
|
||||
}
|
||||
// Nothing to do, if we already visited this vertex.
|
||||
if (isset($graph[$start]['paths'])) {
|
||||
return;
|
||||
}
|
||||
// Mark $start as visited.
|
||||
$graph[$start]['paths'] = array();
|
||||
|
||||
// Assign $start to the current component.
|
||||
$graph[$start]['component'] = $component;
|
||||
$state['components'][$component][] = $start;
|
||||
|
||||
// Visit edges of $start.
|
||||
if (isset($graph[$start]['edges'])) {
|
||||
foreach ($graph[$start]['edges'] as $end => $v) {
|
||||
// Mark that $start can reach $end.
|
||||
$graph[$start]['paths'][$end] = TRUE;
|
||||
|
||||
if (isset($graph[$end]['component']) && $component != $graph[$end]['component']) {
|
||||
// This vertex already has a component, use that from now on and
|
||||
// reassign all the previously explored vertices.
|
||||
$new_component = $graph[$end]['component'];
|
||||
foreach ($state['components'][$component] as $vertex) {
|
||||
$graph[$vertex]['component'] = $new_component;
|
||||
$state['components'][$new_component][] = $vertex;
|
||||
}
|
||||
unset($state['components'][$component]);
|
||||
$component = $new_component;
|
||||
}
|
||||
|
||||
// Visit the connected vertex.
|
||||
_drupal_depth_first_search($graph, $state, $end, $component);
|
||||
|
||||
// All vertices reachable by $end are also reachable by $start.
|
||||
$graph[$start]['paths'] += $graph[$end]['paths'];
|
||||
}
|
||||
}
|
||||
|
||||
// Now that any other subgraph has been explored, add $start to all reverse
|
||||
// paths.
|
||||
foreach ($graph[$start]['paths'] as $end => $v) {
|
||||
$graph[$end]['reverse_paths'][$start] = TRUE;
|
||||
}
|
||||
|
||||
// Record the order of the last visit. This is the reverse of the
|
||||
// topological order if the graph is acyclic.
|
||||
$state['last_visit_order'][] = $start;
|
||||
}
|
||||
|
|
@ -145,69 +145,35 @@ function module_rebuild_cache() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Find dependencies any level deep and fill in dependents information too.
|
||||
*
|
||||
* If module A depends on B which in turn depends on C then this function will
|
||||
* add C to the list of modules A depends on. This will be repeated until
|
||||
* module A has a list of all modules it depends on. If it depends on itself,
|
||||
* called a circular dependency, that's marked by adding a nonexistent module,
|
||||
* called -circular- to this list of modules. Because this does not exist,
|
||||
* it'll be impossible to switch module A on.
|
||||
*
|
||||
* Also we fill in a dependents array in $file->info. Using the names above,
|
||||
* the dependents array of module B lists A.
|
||||
* Find dependencies any level deep and fill in required by information too.
|
||||
*
|
||||
* @param $files
|
||||
* The array of filesystem objects used to rebuild the cache.
|
||||
* @return
|
||||
* The same array with dependencies and dependents added where applicable.
|
||||
* The same array with the new keys for each module:
|
||||
* - requires: An array with the keys being the modules that this module
|
||||
* requires.
|
||||
* - required_by: An array with the keys being the modules that will not work
|
||||
* without this module.
|
||||
*/
|
||||
function _module_build_dependencies($files) {
|
||||
do {
|
||||
$new_dependency = FALSE;
|
||||
foreach ($files as $filename => $file) {
|
||||
// We will modify this object (module A, see doxygen for module A, B, C).
|
||||
$file = &$files[$filename];
|
||||
if (isset($file->info['dependencies']) && is_array($file->info['dependencies'])) {
|
||||
foreach ($file->info['dependencies'] as $dependency_name) {
|
||||
// This is a nonexistent module.
|
||||
if ($dependency_name == '-circular-' || !isset($files[$dependency_name])) {
|
||||
continue;
|
||||
}
|
||||
// $dependency_name is module B (again, see doxygen).
|
||||
$files[$dependency_name]->info['dependents'][$filename] = $filename;
|
||||
$dependency = $files[$dependency_name];
|
||||
if (isset($dependency->info['dependencies']) && is_array($dependency->info['dependencies'])) {
|
||||
// Let's find possible C modules.
|
||||
foreach ($dependency->info['dependencies'] as $candidate) {
|
||||
if (array_search($candidate, $file->info['dependencies']) === FALSE) {
|
||||
// Is this a circular dependency?
|
||||
if ($candidate == $filename) {
|
||||
// As a module name can not contain dashes, this makes
|
||||
// impossible to switch on the module.
|
||||
$candidate = '-circular-';
|
||||
// Do not display the message or add -circular- more than once.
|
||||
if (array_search($candidate, $file->info['dependencies']) !== FALSE) {
|
||||
continue;
|
||||
}
|
||||
drupal_set_message(t('%module is part of a circular dependency. This is not supported and you will not be able to switch it on.', array('%module' => $file->info['name'])), 'error');
|
||||
}
|
||||
else {
|
||||
// We added a new dependency to module A. The next loop will
|
||||
// be able to use this as "B module" thus finding even
|
||||
// deeper dependencies.
|
||||
$new_dependency = TRUE;
|
||||
}
|
||||
$file->info['dependencies'][] = $candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
require_once DRUPAL_ROOT .'/includes/graph.inc';
|
||||
$roots = $files;
|
||||
foreach ($files as $filename => $file) {
|
||||
$graph[$file->name]['edges'] = array();
|
||||
if (isset($file->info['dependencies']) && is_array($file->info['dependencies'])) {
|
||||
foreach ($file->info['dependencies'] as $dependency_name) {
|
||||
$graph[$file->name]['edges'][$dependency_name] = 1;
|
||||
unset($roots[$dependency_name]);
|
||||
}
|
||||
// Don't forget to break the reference.
|
||||
unset($file);
|
||||
}
|
||||
} while ($new_dependency);
|
||||
}
|
||||
drupal_depth_first_search($graph, array_keys($roots));
|
||||
foreach ($graph as $module => $data) {
|
||||
$files[$module]->required_by= isset($data['reverse_paths']) ? $data['reverse_paths'] : array();
|
||||
$files[$module]->requires = isset($data['paths']) ? $data['paths'] : array();
|
||||
$files[$module]->sort = $data['weight'];
|
||||
}
|
||||
return $files;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,190 @@
|
|||
<?php
|
||||
// $Id$
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Provides unit tests for graph.inc.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Unit tests for the graph handling features.
|
||||
*/
|
||||
class GraphUnitTest extends DrupalWebTestCase {
|
||||
function getInfo() {
|
||||
return array(
|
||||
'name' => t('Graph'),
|
||||
'description' => t('Graph handling unit tests.'),
|
||||
'group' => t('System'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test depth-first-search features.
|
||||
*/
|
||||
function testDepthFirstSearch() {
|
||||
// Provoke the inclusion of graph.inc.
|
||||
drupal_function_exists('drupal_depth_first_search');
|
||||
|
||||
// The sample graph used is:
|
||||
// 1 --> 2 --> 3 5 ---> 6
|
||||
// | ^ ^
|
||||
// | | |
|
||||
// | | |
|
||||
// +---> 4 <-- 7 8 ---> 9
|
||||
$graph = $this->normalizeGraph(array(
|
||||
1 => array(2),
|
||||
2 => array(3, 4),
|
||||
3 => array(),
|
||||
4 => array(3),
|
||||
5 => array(6),
|
||||
7 => array(4, 5),
|
||||
8 => array(9),
|
||||
));
|
||||
drupal_depth_first_search($graph);
|
||||
|
||||
$expected_paths = array(
|
||||
1 => array(2, 3, 4),
|
||||
2 => array(3, 4),
|
||||
3 => array(),
|
||||
4 => array(3),
|
||||
5 => array(6),
|
||||
6 => array(),
|
||||
7 => array(4, 3, 5, 6),
|
||||
8 => array(9),
|
||||
9 => array(),
|
||||
);
|
||||
$this->assertPaths($graph, $expected_paths);
|
||||
|
||||
$expected_reverse_paths = array(
|
||||
1 => array(),
|
||||
2 => array(1),
|
||||
3 => array(2, 1, 4, 7),
|
||||
4 => array(2, 1, 7),
|
||||
5 => array(7),
|
||||
6 => array(5, 7),
|
||||
7 => array(),
|
||||
8 => array(),
|
||||
9 => array(8),
|
||||
);
|
||||
$this->assertReversePaths($graph, $expected_reverse_paths);
|
||||
|
||||
$expected_components = array(
|
||||
array(1, 2, 3, 4, 5, 6, 7),
|
||||
array(8, 9),
|
||||
);
|
||||
$this->assertComponents($graph, $expected_components);
|
||||
|
||||
$expected_weights = array(
|
||||
array(1, 2, 3),
|
||||
array(2, 4, 3),
|
||||
array(7, 4, 3),
|
||||
array(7, 5, 6),
|
||||
array(8, 9),
|
||||
);
|
||||
$this->assertWeights($graph, $expected_weights);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a normalized version of a graph.
|
||||
*/
|
||||
function normalizeGraph($graph) {
|
||||
$normalized_graph = array();
|
||||
foreach ($graph as $vertex => $edges) {
|
||||
foreach ($edges as $edge) {
|
||||
$normalized_graph[$vertex]['edges'][$edge] = TRUE;
|
||||
}
|
||||
}
|
||||
return $normalized_graph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify expected paths in a graph.
|
||||
*
|
||||
* @param $graph
|
||||
* A graph array processed by drupal_depth_first_search().
|
||||
* @param $expected_paths
|
||||
* An associative array containing vertices with their expected paths.
|
||||
*/
|
||||
function assertPaths($graph, $expected_paths) {
|
||||
foreach ($expected_paths as $vertex => $paths) {
|
||||
// Build an array with keys = $paths and values = TRUE.
|
||||
$expected = array_fill_keys($paths, TRUE);
|
||||
$result = isset($graph[$vertex]['paths']) ? $graph[$vertex]['paths'] : array();
|
||||
$this->assertEqual($expected, $result, t('Expected paths for vertex @vertex: @expected-paths, got @paths', array('@vertex' => $vertex, '@expected-paths' => $this->displayArray($expected, TRUE), '@paths' => $this->displayArray($result, TRUE))));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify expected reverse paths in a graph.
|
||||
*
|
||||
* @param $graph
|
||||
* A graph array processed by drupal_depth_first_search().
|
||||
* @param $expected_reverse_paths
|
||||
* An associative array containing vertices with their expected reverse
|
||||
* paths.
|
||||
*/
|
||||
function assertReversePaths($graph, $expected_reverse_paths) {
|
||||
foreach ($expected_reverse_paths as $vertex => $paths) {
|
||||
// Build an array with keys = $paths and values = TRUE.
|
||||
$expected = array_fill_keys($paths, TRUE);
|
||||
$result = isset($graph[$vertex]['reverse_paths']) ? $graph[$vertex]['reverse_paths'] : array();
|
||||
$this->assertEqual($expected, $result, t('Expected reverse paths for vertex @vertex: @expected-paths, got @paths', array('@vertex' => $vertex, '@expected-paths' => $this->displayArray($expected, TRUE), '@paths' => $this->displayArray($result, TRUE))));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify expected components in a graph.
|
||||
*
|
||||
* @param $graph
|
||||
* A graph array processed by drupal_depth_first_search().
|
||||
* @param $expected_components
|
||||
* An array containing of components defined as a list of their vertices.
|
||||
*/
|
||||
function assertComponents($graph, $expected_components) {
|
||||
$unassigned_vertices = array_fill_keys(array_keys($graph), TRUE);
|
||||
foreach ($expected_components as $component) {
|
||||
$result_components = array();
|
||||
foreach ($component as $vertex) {
|
||||
$result_components[] = $graph[$vertex]['component'];
|
||||
unset($unassigned_vertices[$vertex]);
|
||||
}
|
||||
$this->assertEqual(1, count(array_unique($result_components)), t('Expected one unique component for vertices @vertices, got @components', array('@vertices' => $this->displayArray($component), '@components' => $this->displayArray($result_components))));
|
||||
}
|
||||
$this->assertEqual(array(), $unassigned_vertices, t('Vertices not assigned to a component: @vertices', array('@vertices' => $this->displayArray($unassigned_vertices, TRUE))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify expected order in a graph.
|
||||
*
|
||||
* @param $graph
|
||||
* A graph array processed by drupal_depth_first_search().
|
||||
* @param $expected_orders
|
||||
* An array containing lists of vertices in their expected order.
|
||||
*/
|
||||
function assertWeights($graph, $expected_orders) {
|
||||
foreach ($expected_orders as $order) {
|
||||
$previous_vertex = array_shift($order);
|
||||
foreach ($order as $vertex) {
|
||||
$this->assertTrue($graph[$previous_vertex]['weight'] < $graph[$vertex]['weight'], t('Weights of @previous-vertex and @vertex are correct relative to each other', array('@previous-vertex' => $previous_vertex, '@vertex' => $vertex)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to output vertices as comma-separated list.
|
||||
*
|
||||
* @param $paths
|
||||
* An array containing a list of vertices.
|
||||
* @param $keys
|
||||
* (optional) Whether to output the keys of $paths instead of the values.
|
||||
*/
|
||||
function displayArray($paths, $keys = FALSE) {
|
||||
if (!empty($paths)) {
|
||||
return implode(', ', $keys ? array_keys($paths) : $paths);
|
||||
}
|
||||
else {
|
||||
return '(empty)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -47,7 +47,7 @@ table.package .description {
|
|||
table.package .version {
|
||||
direction: ltr;
|
||||
}
|
||||
div.admin-dependencies, div.admin-required {
|
||||
div.admin-requirements, div.admin-required {
|
||||
font-size: 0.9em;
|
||||
color: #444;
|
||||
}
|
||||
|
|
|
@ -527,35 +527,23 @@ function system_theme_settings_submit($form, &$form_state) {
|
|||
* Recursively check compatibility.
|
||||
*
|
||||
* @param $incompatible
|
||||
* An associative array which at the end of the check contains all incompatible files as the keys, their values being TRUE.
|
||||
* An associative array which at the end of the check contains all
|
||||
* incompatible files as the keys, their values being TRUE.
|
||||
* @param $files
|
||||
* The set of files that will be tested.
|
||||
* @param $file
|
||||
* The file at which the check starts.
|
||||
* @return
|
||||
* Returns TRUE if an incompatible file is found, NULL (no return value) otherwise.
|
||||
* Returns TRUE if an incompatible file is found, NULL (no return value)
|
||||
* otherwise.
|
||||
*/
|
||||
function _system_is_incompatible(&$incompatible, $files, $file) {
|
||||
static $seen;
|
||||
// We need to protect ourselves in case of a circular dependency.
|
||||
if (isset($seen[$file->name])) {
|
||||
return isset($incompatible[$file->name]);
|
||||
}
|
||||
$seen[$file->name] = TRUE;
|
||||
if (isset($incompatible[$file->name])) {
|
||||
return TRUE;
|
||||
}
|
||||
// The 'dependencies' key in .info files was a string in Drupal 5, but changed
|
||||
// to an array in Drupal 6. If it is not an array, the module is not
|
||||
// compatible and we can skip the check below which requires an array.
|
||||
if (!is_array($file->info['dependencies'])) {
|
||||
$file->info['dependencies'] = array();
|
||||
$incompatible[$file->name] = TRUE;
|
||||
return TRUE;
|
||||
}
|
||||
// Recursively traverse the dependencies, looking for incompatible modules
|
||||
foreach ($file->info['dependencies'] as $dependency) {
|
||||
if (isset($files[$dependency]) && _system_is_incompatible($incompatible, $files, $files[$dependency])) {
|
||||
// Recursively traverse required modules, looking for incompatible modules.
|
||||
foreach ($file->requires as $requires) {
|
||||
if (isset($files[$requires]) && _system_is_incompatible($incompatible, $files, $files[$requires])) {
|
||||
$incompatible[$file->name] = TRUE;
|
||||
return TRUE;
|
||||
}
|
||||
|
@ -566,12 +554,12 @@ function _system_is_incompatible(&$incompatible, $files, $file) {
|
|||
* Menu callback; provides module enable/disable interface.
|
||||
*
|
||||
* The list of modules gets populated by module.info files, which contain each module's name,
|
||||
* description and dependencies.
|
||||
* description and information about which modules it requires.
|
||||
* @see drupal_parse_info_file for information on module.info descriptors.
|
||||
*
|
||||
* Dependency checking is performed to ensure that a module cannot be enabled if the module has
|
||||
* disabled dependencies and also to ensure that the module cannot be disabled if the module has
|
||||
* enabled dependents.
|
||||
* Dependency checking is performed to ensure that a module:
|
||||
* - can not be enabled if there are disabled modules it requires.
|
||||
* - can not be disabled if there are enabled modules which depend on it.
|
||||
*
|
||||
* @param $form_state
|
||||
* An associative array containing the current state of the form.
|
||||
|
@ -600,14 +588,14 @@ function system_modules($form_state = array()) {
|
|||
|
||||
uasort($files, 'system_sort_modules_by_info_name');
|
||||
|
||||
// If the modules form was submitted, then system_modules_submit() runs first
|
||||
// and if there are unfilled required modules, then form_state['storage'] is
|
||||
// filled, triggering a rebuild. In this case we need to display a
|
||||
// confirmation form.
|
||||
if (!empty($form_state['storage'])) {
|
||||
// If the modules form was submitted, then first system_modules_submit runs
|
||||
// and if there are unfilled dependencies, then form_state['storage'] is
|
||||
// filled, triggering a rebuild. In this case we need to show a confirm
|
||||
// form.
|
||||
return system_modules_confirm_form($files, $form_state['storage']);
|
||||
}
|
||||
$dependencies = array();
|
||||
|
||||
$modules = array();
|
||||
$form['modules'] = array('#tree' => TRUE);
|
||||
|
||||
|
@ -618,19 +606,17 @@ function system_modules($form_state = array()) {
|
|||
foreach ($files as $filename => $module) {
|
||||
$extra = array();
|
||||
$extra['enabled'] = (bool) $module->status;
|
||||
// If this module has dependencies, add them to the array.
|
||||
if (is_array($module->info['dependencies'])) {
|
||||
foreach ($module->info['dependencies'] as $dependency) {
|
||||
if (!isset($files[$dependency])) {
|
||||
$extra['dependencies'][$dependency] = t('@module (<span class="admin-missing">missing</span>)', array('@module' => drupal_ucfirst($dependency)));
|
||||
// If this module requires other modules, add them to the array.
|
||||
foreach ($module->requires as $requires => $v) {
|
||||
if (!isset($files[$requires])) {
|
||||
$extra['requires'][$requires] = t('@module (<span class="admin-missing">missing</span>)', array('@module' => drupal_ucfirst($requires)));
|
||||
$extra['disabled'] = TRUE;
|
||||
}
|
||||
elseif (!$files[$dependency]->status) {
|
||||
$extra['dependencies'][$dependency] = t('@module (<span class="admin-disabled">disabled</span>)', array('@module' => $files[$dependency]->info['name']));
|
||||
elseif (!$files[$requires]->status) {
|
||||
$extra['requires'][$requires] = t('@module (<span class="admin-disabled">disabled</span>)', array('@module' => $files[$requires]->info['name']));
|
||||
}
|
||||
else {
|
||||
$extra['dependencies'][$dependency] = t('@module (<span class="admin-enabled">enabled</span>)', array('@module' => $files[$dependency]->info['name']));
|
||||
}
|
||||
$extra['requires'][$requires] = t('@module (<span class="admin-enabled">enabled</span>)', array('@module' => $files[$requires]->info['name']));
|
||||
}
|
||||
}
|
||||
// Generate link for module's help page, if there is one.
|
||||
|
@ -640,17 +626,19 @@ function system_modules($form_state = array()) {
|
|||
$extra['help'] = theme('more_help_link', url("admin/help/$filename"));
|
||||
}
|
||||
}
|
||||
// Mark dependents disabled so user can not remove modules being depended on.
|
||||
// Mark dependents disabled so the user cannot remove required modules.
|
||||
$dependents = array();
|
||||
foreach ($module->info['dependents'] as $dependent) {
|
||||
// If this module is required by other modules, list those, and then make it
|
||||
// impossible to disable this one.
|
||||
foreach ($module->required_by as $required_by => $v) {
|
||||
// Hidden modules are unset already.
|
||||
if (isset($files[$dependent])) {
|
||||
if ($files[$dependent]->status == 1) {
|
||||
$extra['dependents'][] = t('@module (<span class="admin-enabled">enabled</span>)', array('@module' => $files[$dependent]->info['name']));
|
||||
if (isset($files[$required_by])) {
|
||||
if ($files[$required_by]->status == 1) {
|
||||
$extra['required_by'][] = t('@module (<span class="admin-enabled">enabled</span>)', array('@module' => $files[$required_by]->info['name']));
|
||||
$extra['disabled'] = TRUE;
|
||||
}
|
||||
else {
|
||||
$extra['dependents'][] = t('@module (<span class="admin-disabled">disabled</span>)', array('@module' => $files[$dependent]->info['name']));
|
||||
$extra['required_by'][] = t('@module (<span class="admin-disabled">disabled</span>)', array('@module' => $files[$required_by]->info['name']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -694,8 +682,8 @@ function system_sort_modules_by_info_name($a, $b) {
|
|||
function _system_modules_build_row($info, $extra) {
|
||||
// Add in the defaults.
|
||||
$extra += array(
|
||||
'dependencies' => array(),
|
||||
'dependents' => array(),
|
||||
'requires' => array(),
|
||||
'required_by' => array(),
|
||||
'disabled' => FALSE,
|
||||
'enabled' => FALSE,
|
||||
'help' => '',
|
||||
|
@ -713,8 +701,8 @@ function _system_modules_build_row($info, $extra) {
|
|||
$form['version'] = array(
|
||||
'#markup' => $info['version'],
|
||||
);
|
||||
$form['#dependencies'] = $extra['dependencies'];
|
||||
$form['#dependents'] = $extra['dependents'];
|
||||
$form['#requires'] = $extra['requires'];
|
||||
$form['#required_by'] = $extra['required_by'];
|
||||
|
||||
// Check the compatibilities.
|
||||
$compatible = TRUE;
|
||||
|
@ -767,13 +755,13 @@ function _system_modules_build_row($info, $extra) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Display confirmation form for dependencies.
|
||||
* Display confirmation form for required modules.
|
||||
*
|
||||
* @param $modules
|
||||
* Array of module file objects as returned from module_rebuild_cache().
|
||||
* @param $storage
|
||||
* The contents of $form_state['storage']; an array with two
|
||||
* elements: the list of dependencies and the list of status
|
||||
* elements: the list of required modules and the list of status
|
||||
* form field values from the previous screen.
|
||||
* @ingroup forms
|
||||
*/
|
||||
|
@ -784,12 +772,12 @@ function system_modules_confirm_form($modules, $storage) {
|
|||
$form['validation_modules'] = array('#type' => 'value', '#value' => $modules);
|
||||
$form['status']['#tree'] = TRUE;
|
||||
|
||||
foreach ($storage['dependencies'] as $info) {
|
||||
foreach ($storage['more_modules'] as $info) {
|
||||
$t_argument = array(
|
||||
'@module' => $info['name'],
|
||||
'@dependencies' => implode(', ', $info['dependencies']),
|
||||
'@required' => implode(', ', $info['requires']),
|
||||
);
|
||||
$items[] = format_plural(count($info['dependencies']), 'You must enable the @dependencies module to install @module.', 'You must enable the @dependencies modules to install @module.', $t_argument);
|
||||
$items[] = format_plural(count($info['requires']), 'You must enable the @required module to install @module.', 'You must enable the @required modules to install @module.', $t_argument);
|
||||
}
|
||||
$form['text'] = array('#markup' => theme('item_list', $items));
|
||||
|
||||
|
@ -826,7 +814,8 @@ function system_modules_submit($form, &$form_state) {
|
|||
$modules = $form_state['storage']['modules'];
|
||||
}
|
||||
|
||||
// Get a list of all modules, for building dependencies with.
|
||||
// Get a list of all modules, it will be used to find which module requires
|
||||
// which.
|
||||
$files = module_rebuild_cache();
|
||||
|
||||
// The modules to be enabled.
|
||||
|
@ -835,11 +824,11 @@ function system_modules_submit($form, &$form_state) {
|
|||
$disable_modules = array();
|
||||
// The modules to be installed.
|
||||
$new_modules = array();
|
||||
// The un-met dependencies.
|
||||
$dependencies = array();
|
||||
// Go through each module, finding out
|
||||
// if we should enable, install, or disable it,
|
||||
// and if it has any un-met dependencies.
|
||||
// Modules that need to be switched on because other modules require them.
|
||||
$more_modules = array();
|
||||
// Go through each module, finding out if we should enable, install, or
|
||||
// disable it. Also, we find out if there are modules it requires that are
|
||||
// not enabled.
|
||||
foreach ($modules as $name => $module) {
|
||||
// If it's enabled, find out whether to just
|
||||
// enable it, or install it.
|
||||
|
@ -850,18 +839,18 @@ function system_modules_submit($form, &$form_state) {
|
|||
elseif (!module_exists($name)) {
|
||||
$modules_to_be_enabled[$name] = $name;
|
||||
}
|
||||
// If we're not coming from a confirmation form,
|
||||
// search dependencies. Otherwise, the user will have already
|
||||
// approved of the dependent modules being enabled.
|
||||
// If we're not coming from a confirmation form, search for modules the
|
||||
// new ones require and see whether there are any that additionally
|
||||
// need to be switched on.
|
||||
if (empty($form_state['storage'])) {
|
||||
foreach ($form['modules'][$module['group']][$name]['#dependencies'] as $dependency => $string) {
|
||||
if (!$modules[$dependency]['enabled']) {
|
||||
if (!isset($dependencies[$name])) {
|
||||
$dependencies[$name]['name'] = $files[$name]->info['name'];
|
||||
foreach ($form['modules'][$module['group']][$name]['#requires'] as $requires => $v) {
|
||||
if (!$modules[$requires]['enabled']) {
|
||||
if (!isset($more_modules[$name])) {
|
||||
$more_modules[$name]['name'] = $files[$name]->info['name'];
|
||||
}
|
||||
$dependencies[$name]['dependencies'][$dependency] = $files[$dependency]->info['name'];
|
||||
$more_modules[$name]['requires'][$requires] = $files[$requires]->info['name'];
|
||||
}
|
||||
$modules[$dependency] = array('group' => $files[$dependency]->info['package'], 'enabled' => TRUE);
|
||||
$modules[$requires] = array('group' => $files[$requires]->info['package'], 'enabled' => TRUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -873,18 +862,18 @@ function system_modules_submit($form, &$form_state) {
|
|||
$disable_modules[$name] = $name;
|
||||
}
|
||||
}
|
||||
if ($dependencies) {
|
||||
// If there where un-met dependencies and they haven't confirmed don't process
|
||||
// the submission yet. Store the form submission data needed later.
|
||||
if ($more_modules) {
|
||||
// If we need to switch on more modules because other modules require
|
||||
// them and they haven't confirmed, don't process the submission yet. Store
|
||||
// the form submission data needed later.
|
||||
if (!isset($form_state['values']['confirm'])) {
|
||||
$form_state['storage'] = array('dependencies' => $dependencies, 'modules' => $modules);
|
||||
$form_state['storage'] = array('more_modules' => $more_modules, 'modules' => $modules);
|
||||
return;
|
||||
}
|
||||
// Otherwise, install or enable the modules.
|
||||
else {
|
||||
$dependencies = $form_storage['dependencies'];
|
||||
foreach ($dependencies as $info) {
|
||||
foreach ($info['dependencies'] as $dependency => $name) {
|
||||
foreach ($form_state['storage']['more_modules'] as $info) {
|
||||
foreach ($info['requires'] as $requires => $name) {
|
||||
if (drupal_get_installed_schema_version($name) == SCHEMA_UNINSTALLED) {
|
||||
$new_modules[$name] = $name;
|
||||
}
|
||||
|
@ -895,28 +884,42 @@ function system_modules_submit($form, &$form_state) {
|
|||
}
|
||||
}
|
||||
}
|
||||
// If we have no dependencies, or the dependencies are confirmed
|
||||
// to be installed, we don't need the temporary storage anymore.
|
||||
// Now we have installed every module as required (either by the user or
|
||||
// because other modules require them) so we don't need the temporary
|
||||
// storage anymore.
|
||||
unset($form_state['storage']);
|
||||
|
||||
$old_module_list = module_list();
|
||||
|
||||
// Enable the modules needing enabling.
|
||||
if (!empty($modules_to_be_enabled)) {
|
||||
$sort = array();
|
||||
foreach ($modules_to_be_enabled as $module) {
|
||||
$sort[$module] = $files[$module]->sort;
|
||||
}
|
||||
array_multisort($sort, $modules_to_be_enabled);
|
||||
module_enable($modules_to_be_enabled);
|
||||
}
|
||||
// Disable the modules that need disabling.
|
||||
if (!empty($disable_modules)) {
|
||||
$sort = array();
|
||||
foreach ($disable_modules as $module) {
|
||||
$sort[$module] = $files[$module]->sort;
|
||||
}
|
||||
array_multisort($sort, $disable_modules);
|
||||
module_disable($disable_modules);
|
||||
}
|
||||
|
||||
// Install new modules.
|
||||
if (!empty($new_modules)) {
|
||||
$sort = array();
|
||||
foreach ($new_modules as $key => $module) {
|
||||
if (!drupal_check_module($module)) {
|
||||
unset($new_modules[$key]);
|
||||
}
|
||||
$sort[$module] = $files[$module]->sort;
|
||||
}
|
||||
array_multisort($sort, $new_modules);
|
||||
drupal_install_modules($new_modules);
|
||||
}
|
||||
|
||||
|
@ -2102,13 +2105,13 @@ function theme_system_modules_fieldset($form) {
|
|||
if (isset($module['help'])) {
|
||||
$description = '<div class="module-help">'. drupal_render($module['help']) .'</div>';
|
||||
}
|
||||
// Add the description, along with any dependencies.
|
||||
// Add the description, along with any modules it requires.
|
||||
$description .= drupal_render($module['description']);
|
||||
if ($module['#dependencies']) {
|
||||
$description .= '<div class="admin-dependencies">' . t('Depends on: ') . implode(', ', $module['#dependencies']) . '</div>';
|
||||
if ($module['#requires']) {
|
||||
$description .= '<div class="admin-requirements">' . t('Requires: @module-list', array('@module-list' => implode(', ', $module['#requires']))) . '</div>';
|
||||
}
|
||||
if ($module['#dependents']) {
|
||||
$description .= '<div class="admin-dependencies">' . t('Required by: ') . implode(', ', $module['#dependents']) . '</div>';
|
||||
if ($module['#required_by']) {
|
||||
$description .= '<div class="admin-requirements">' . t('Required by: @module-list', array('@module-list' => implode(', ', $module['#required_by']))) . '</div>';
|
||||
}
|
||||
$row[] = array('data' => $description, 'class' => 'description');
|
||||
$rows[] = $row;
|
||||
|
|
|
@ -1,23 +1,12 @@
|
|||
<?php
|
||||
// $Id$
|
||||
|
||||
class EnableDisableCoreTestCase extends DrupalWebTestCase {
|
||||
/**
|
||||
* Helper class for module test cases.
|
||||
*/
|
||||
class ModuleTestCase extends DrupalWebTestCase {
|
||||
protected $admin_user;
|
||||
|
||||
/**
|
||||
* Implementation of getInfo().
|
||||
*/
|
||||
function getInfo() {
|
||||
return array(
|
||||
'name' => t('Module list functionality'),
|
||||
'description' => t('Enable/disable core module and confirm table creation/deletion. Enable module without dependency enabled. Attempt disabling of required modules.'),
|
||||
'group' => t('System')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of setUp().
|
||||
*/
|
||||
function setUp() {
|
||||
parent::setUp('system_test');
|
||||
|
||||
|
@ -25,6 +14,58 @@ class EnableDisableCoreTestCase extends DrupalWebTestCase {
|
|||
$this->drupalLogin($this->admin_user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert there are tables that begin with the specified base table name.
|
||||
*
|
||||
* @param $base_table
|
||||
* Beginning of table name to look for.
|
||||
* @param $count
|
||||
* (optional) Whether or not to assert that there are tables that match the
|
||||
* specified base table. Defaults to TRUE.
|
||||
*/
|
||||
function assertTableCount($base_table, $count = TRUE) {
|
||||
$tables = db_find_tables(Database::getActiveConnection()->prefixTables('{' . $base_table . '}') . '%');
|
||||
|
||||
if ($count) {
|
||||
return $this->assertTrue($tables, t('Tables matching "@base_table" found.', array('@base_table' => $base_table)));
|
||||
}
|
||||
return $this->assertFalse($tables, t('Tables matching "@base_table" not found.', array('@base_table' => $base_table)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the list of modules are enabled or disabled.
|
||||
*
|
||||
* @param $modules
|
||||
* Module list to check.
|
||||
* @param $enabled
|
||||
* Expected module state.
|
||||
*/
|
||||
function assertModules(array $modules, $enabled) {
|
||||
module_list(TRUE);
|
||||
foreach ($modules as $module) {
|
||||
if ($enabled) {
|
||||
$message = 'Module "@module" is enabled.';
|
||||
}
|
||||
else {
|
||||
$message = 'Module "@module" is not enabled.';
|
||||
}
|
||||
$this->assertEqual(module_exists($module), $enabled, t($message, array('@module' => $module)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test module enabling/disabling functionality.
|
||||
*/
|
||||
class EnableDisableTestCase extends ModuleTestCase {
|
||||
function getInfo() {
|
||||
return array(
|
||||
'name' => t('Enable/disable modules'),
|
||||
'description' => t('Enable/disable core module and confirm table creation/deletion.'),
|
||||
'group' => t('Module'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable a module, check the database for related tables, disable module,
|
||||
* check for related tables, uninstall module, check for related tables.
|
||||
|
@ -71,6 +112,19 @@ class EnableDisableCoreTestCase extends DrupalWebTestCase {
|
|||
$this->assertModules(array('aggregator'), FALSE);
|
||||
$this->assertTableCount('aggregator', FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test module dependency functionality.
|
||||
*/
|
||||
class ModuleDependencyTestCase extends ModuleTestCase {
|
||||
function getInfo() {
|
||||
return array(
|
||||
'name' => t('Module dependencies'),
|
||||
'description' => t('Enable module without dependency enabled.'),
|
||||
'group' => t('Module'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to enable translation module without locale enabled.
|
||||
|
@ -97,6 +151,19 @@ class EnableDisableCoreTestCase extends DrupalWebTestCase {
|
|||
$this->assertTableCount('languages', TRUE);
|
||||
$this->assertTableCount('locale', TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test required modules functionality.
|
||||
*/
|
||||
class ModuleRequiredTestCase extends ModuleTestCase {
|
||||
function getInfo() {
|
||||
return array(
|
||||
'name' => t('Required modules'),
|
||||
'description' => t('Attempt disabling of required modules.'),
|
||||
'group' => t('Module'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that core required modules cannot be disabled.
|
||||
|
@ -109,41 +176,6 @@ class EnableDisableCoreTestCase extends DrupalWebTestCase {
|
|||
$this->assertNoFieldByName('modules[Core][' . $module . '][enable]');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert tables that begin with the specified base table name.
|
||||
*
|
||||
* @param string $base_table Beginning of table name to look for.
|
||||
* @param boolean $count Assert tables that match specified base table.
|
||||
* @return boolean Tables with specified base table.
|
||||
*/
|
||||
function assertTableCount($base_table, $count) {
|
||||
$tables = db_find_tables(Database::getActiveConnection()->prefixTables('{' . $base_table . '}') . '%');
|
||||
|
||||
if ($count) {
|
||||
return $this->assertTrue($tables, t('Tables matching "@base_table" found.', array('@base_table' => $base_table)));
|
||||
}
|
||||
return $this->assertFalse($tables, t('Tables matching "@base_table" not found.', array('@base_table' => $base_table)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the list of modules are enabled or disabled.
|
||||
*
|
||||
* @param array $modules Modules to check.
|
||||
* @param boolean $enabled Module state.
|
||||
*/
|
||||
function assertModules(array $modules, $enabled) {
|
||||
module_list(TRUE);
|
||||
foreach ($modules as $module) {
|
||||
if ($enabled) {
|
||||
$message = 'Module "@module" is enabled.';
|
||||
}
|
||||
else {
|
||||
$message = 'Module "@module" is not enabled.';
|
||||
}
|
||||
$this->assertEqual(module_exists($module), $enabled, t($message, array('@module' => $module)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class IPAddressBlockingTestCase extends DrupalWebTestCase {
|
||||
|
|
Loading…
Reference in New Issue