diff --git a/core/modules/node/node.module b/core/modules/node/node.module index b5ed7e8..72f1ba7 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -388,9 +388,12 @@ function _node_extract_type($node) { } /** - * Returns a list of all the available node types. - * - * This list can include types that are queued for addition or deletion. + * Returns a list of a site's known node types. + + * Any node type added by hook_node_info() or node_type_save() that has not been + * explicitly removed by node_type_delete or direct database interaction will + * appear in this list. The list can include types that are queued for addition + * or deletion. * See _node_types_build() for details. * * @return @@ -473,10 +476,8 @@ function node_type_get_name($node) { /** * Updates the database cache of node types. * - * All new module-defined node types are saved to the database via a call to - * node_type_save(), and obsolete ones are deleted via a call to - * node_type_delete(). See _node_types_build() for an explanation of the new - * and obsolete types. + * All new node types defined by hook_node_info() are saved to the database via + * a call to node_type_save(). */ function node_types_rebuild() { _node_types_build(TRUE); @@ -499,6 +500,9 @@ function node_type_load($name) { /** * Saves a node type to the database. * + * Modules should not implement node types + * with this function, but should use hook_node_info() instead. + * * @param $info * The node type to save, as an object. * @@ -509,6 +513,19 @@ function node_type_save($info) { $existing_type = !empty($info->old_type) ? $info->old_type : $info->type; $is_existing = (bool) db_query_range('SELECT 1 FROM {node_type} WHERE type = :type', 0, 1, array(':type' => $existing_type))->fetchField(); $type = node_type_set_defaults($info); + $map = array('%type' => $type->name); + if (!$type->custom && $type->base == 'node_content') { + // A modular node type with base==node_content does not behave like something + // owned by its module. + drupal_set_message(t('The content type %type will not be disabled with its parent module.', $map), 'warning'); + } + elseif (!$type->custom && $type->module == '') { + // content_types.inc uses node_hook() to build the `Content types' list, + // but when the module is disabled, the [base]_form vanishes if it was + // implemented by the same module. Under this condition, one can add content + // of the disabled module, but cannot manage the content type. + drupal_set_message(t('The modular content type %type must either use base==node_content or be implemented by hook_node_info().', $map), 'error'); + } $fields = array( 'type' => (string) $type->type, @@ -669,30 +686,38 @@ function node_type_update_nodes($old_type, $type) { ->execute(); } -/** - * Builds and returns the list of available node types. - * - * The list of types is built by invoking hook_node_info() on all modules and - * comparing this information with the node types in the {node_type} table. - * These two information sources are not synchronized during module installation - * until node_types_rebuild() is called. - * - * @param $rebuild - * TRUE to rebuild node types. Equivalent to calling node_types_rebuild(). - * - * @return - * Associative array with two components: - * - names: Associative array of the names of node types, keyed by the type. - * - types: Associative array of node type objects, keyed by the type. - * Both of these arrays will include new types that have been defined by - * hook_node_info() implementations but not yet saved in the {node_type} - * table. These are indicated in the type object by $type->is_new being set - * to the value 1. These arrays will also include obsolete types: types that - * were previously defined by modules that have now been disabled, or for - * whatever reason are no longer being defined in hook_node_info() - * implementations, but are still in the database. These are indicated in the - * type object by $type->disabled being set to TRUE. - */ + /** + * Gets a list of all node types that aren't deleted by node_type_delete(). + * + * This function provides the {node_type} table state as it would appear after + * a rebuild, actually rebuilding the table for $rebuild==TRUE. The returned + * node type's disabled property indicates if the node type's implementing + * module has been disabled, however, any node type with a {base} value of + * {node_content} always remains non-disabled making Node Content a + * poor name choice for a module. + * + * @param $rebuild + * TRUE to rebuild {node_types} table of the database. + * @return + * An object with two properties: + * - names: Associative array of the names corresponding to active or + * disabled node types, keyed by node type. This property does not + * necessarily reflect the state of the {node_type} table unless + * $rebuild==TRUE. The property is a thin version of the {types} property. + * See below for exactly what membership in this array means. + * - types: Associative array of node type objects, keyed by node type. + * Like the {names} array, this data structure does not necessarily reflect + * the state of the {node_type} table. The node type objects contain + * fresher data. + * - disabled: If {base==node_content}, then this property does not vary + * from 0. Otherwise, the $type->disabled flag takes a value of 0 for any + * type in the {node_type} table whose module is enabled, or it takes a + * value of 1 if its module is disabled (or possibly uninstalled). + * - is_new: The $type->is_new flag does not appear in the {node_type} + * table. The property gets set to 1 for node types implemented by + * hook_node_info() that did not appear in the {node_type} table at + * _node_types_build() entry. The property is left unset otherwise. + */ function _node_types_build($rebuild = FALSE) { $cid = 'node_types:' . $GLOBALS['language_interface']->langcode; @@ -707,65 +732,101 @@ function _node_types_build($rebuild = FALSE) { } } - $_node_types = (object) array('types' => array(), 'names' => array()); + // Gather node types from the hook_node_info() implementations of active + // modules. + $active_types = (object) array('types' => array(), 'names' => array()); foreach (module_implements('node_info') as $module) { - $info_array = module_invoke($module, 'node_info'); - foreach ($info_array as $type => $info) { - $info['type'] = $type; - $_node_types->types[$type] = node_type_set_defaults($info); - $_node_types->types[$type]->module = $module; - $_node_types->names[$type] = $info['name']; + foreach (module_invoke($module, 'node_info') as $type => $node_info) { + $node_info['type'] = $type; + $active_types->types[$type] = node_type_set_defaults($node_info); + $active_types->types[$type]->module = $module; + // active_type not in node_type table => active_type is new, so remove + // this default value if the type is found in the database. + $active_types->types[$type]->is_new = 1; + // active_type is new => active_type is not disabled, so be consistent + // with other defaults for now + $active_types->types[$type]->disabled = 0; + // attach flag to indicate whether the type needs to be (re)written to the + // database + if ($rebuild) { + $active_types->types[$type]->new_state = 1; + } + + // Copy subset of the types property's data to the name property + $active_types->names[$type] = $active_types->types[$type]->name; } } - $query = db_select('node_type', 'nt') + + // Gather node types from the node_type table + $db_types = db_select('node_type', 'nt') ->addTag('translatable') ->addTag('node_type_access') ->fields('nt') - ->orderBy('nt.type', 'ASC'); - if (!$rebuild) { - $query->condition('disabled', 0); - } - foreach ($query->execute() as $type_object) { - $type_db = $type_object->type; - // Original disabled value. - $disabled = $type_object->disabled; - // Check for node types from disabled modules and mark their types for removal. - // Types defined by the node module in the database (rather than by a separate - // module using hook_node_info) have a base value of 'node_content'. The isset() - // check prevents errors on old (pre-Drupal 7) databases. - if (isset($type_object->base) && $type_object->base != 'node_content' && empty($_node_types->types[$type_db])) { - $type_object->disabled = TRUE; + ->orderBy('nt.type', 'ASC') + ->execute(); + // Sync the node_type table against the enabled modules list. Any node type + // with a base other than node_content inherits its disabled state from that + // of its module. + $enabled_modules = module_list(); + foreach ($db_types as $db_type) { + // Remove any type information from hook_node_info() with that from the + // database to preserve any modifications + $is_hook_implemented = isset($active_types->types[$db_type->type]); + $active_types->types[$db_type->type] = $db_type; + $type = $active_types->types[$db_type->type]; + if ($is_hook_implemented) { + $new_state = ($type->disabled != 0); + $type->disabled = 0; } - if (isset($_node_types->types[$type_db])) { - $type_object->disabled = FALSE; + else { + // A hook_node_info() was unavailable for the current occurance from the + // database, so enable or disable the occurance depending on its module's + // state + if ($type->module=='' || (isset($type->base) && $type->base=='node_content')) { + // Special case: always enabled + $new_state = 0; + $type->disabled = 0; + } + elseif (isset($enabled_modules[$db_type->module])) { + // The module is enabled + $new_state = ($type->disabled != 0); + $type->disabled = 0; + } + else { + // The module is disabled, uninstalled, or missing entirely + $new_state = ($type->disabled != 1); + $type->disabled = 1; + } } - if (!isset($_node_types->types[$type_db]) || $type_object->modified) { - $_node_types->types[$type_db] = $type_object; - $_node_types->names[$type_db] = $type_object->name; - if ($type_db != $type_object->orig_type) { - unset($_node_types->types[$type_object->orig_type]); - unset($_node_types->names[$type_object->orig_type]); - } + // Don't litter the return data structure with internal data + if ($rebuild) { + $active_types->types[$db_type->type]->new_state = $new_state; } - $_node_types->types[$type_db]->disabled = $type_object->disabled; - $_node_types->types[$type_db]->disabled_changed = $disabled != $type_object->disabled; + + // Copy data to names array. + $active_types->names[$db_type->type] = $active_types->types[$db_type->type]->name; } + // Save new node types if a rebuild was requested. Needs a fresh loop, since + // the loop above doesn't touch new hook_node_info() implementors. if ($rebuild) { - foreach ($_node_types->types as $type => $type_object) { - if (!empty($type_object->is_new) || !empty($type_object->disabled_changed)) { - node_type_save($type_object); + foreach ($active_types->types as $type) { + if (isset($type->is_new) || isset($type->new_state)) { + node_type_save($type); + // explicitly remove the new_state flag or someone will start depending + // on unspecified behavior + unset($type->new_state); } } } - asort($_node_types->names); + asort($active_types->names); - cache()->set($cid, $_node_types); + cache()->set($cid, $active_types); - return $_node_types; + return $active_types; } /** @@ -1256,8 +1317,8 @@ function node_delete_multiple($nids) { // Remove this node from the search index if needed. // This code is implemented in node module rather than in search module, - // because node module is implementing search module's API, not the other - // way around. + // because node module is implementing search module's API, + // not the other way around. if (module_exists('search')) { search_reindex($nid, 'node'); } @@ -2086,10 +2147,10 @@ function node_menu() { 'type' => MENU_CALLBACK, ); // @todo Remove this loop when we have a 'description callback' property. - // Resets the internal static cache of _node_types_build() and forces a - // rebuild of the node type information. - node_type_cache_reset(); foreach (node_type_get_types() as $type) { + if ($type->disabled) { + continue; + } $type_url_str = str_replace('_', '-', $type->type); $items['node/add/' . $type_url_str] = array( 'title' => $type->name, @@ -3719,7 +3780,8 @@ function _node_access_rebuild_batch_finished($success, $results, $operations) { * Implements hook_form(). */ function node_content_form($node, $form_state) { - // It is impossible to define a content type without implementing hook_form() + // It is impossible to define a content type with a base that is not + // node_content without also implementing hook_form() // @todo: remove this requirement. $form = array(); $type = node_type_get_type($node); diff --git a/core/modules/node/node.test b/core/modules/node/node.test index 1dbcaf3..2a0f10c 100644 --- a/core/modules/node/node.test +++ b/core/modules/node/node.test @@ -521,7 +521,7 @@ class NodeCreationTestCase extends NodeWebTestCase { } /** - * Create a page node and verify that a transaction rolls back the failed creation + * Create a page node and verify that a transaction rolls back the failed creation. */ function testFailedPageCreation() { // Create a node. @@ -1447,6 +1447,32 @@ class NodeTypeTestCase extends NodeWebTestCase { $this->assertTrue(isset($types[$type]->disabled) && empty($types[$type]->disabled), t('%type type is enabled.', array('%type' => $type))); } } + + /** + * Tests handling of node type types that are undefiined. + */ + public function testNodeTypeDisabled() { + $test_type = new stdClass(); + $test_type->base = 'not_node_content'; + $test_type->name = t('Test Type'); + $test_type->type = 'test'; + + node_type_save($test_type); + $updated = db_update('node_type') + ->fields(array('disabled' => 1)) + ->condition('type', $test_type->type) + ->execute(); + $this->assertTrue(($updated == 1), 'Successfully disabled custom content type.'); + $info = node_type_get_type($test_type->type); + $this->assertTrue($info !== FALSE, 'Return data from node_type_get_type is valid for a disabled content type with a base that is not node_content.'); + + $target_type = 'book'; + module_enable(array($target_type), FALSE); + module_disable(array($target_type)); + $info = node_type_get_type($target_type); + $this->assertTrue($info !== FALSE, 'Return data from node_type_get_type is valid for a disabled content type with base of node_content.'); + + } } /**