diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeTypeTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeTypeTest.php
index e84a930..b1c0f2a 100644
--- a/core/modules/node/lib/Drupal/node/Tests/NodeTypeTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/NodeTypeTest.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\node\Tests;
 
+use stdClass;
+
 /**
  * Tests related to node types.
  */
@@ -163,4 +165,31 @@ class NodeTypeTest extends NodeTestBase {
       $this->assertTrue(isset($types[$type]->disabled) && empty($types[$type]->disabled), t('%type type is enabled.', array('%type' => $type)));
     }
   }
+
+  /**
+   * Tests the handling of node types that are undefined.
+   */
+  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' => TRUE))
+      ->condition('type', $test_type->type)
+      ->execute();
+    $this->assertTrue(($updated == TRUE), 'Successfully disabled custom content type.');
+    $info = node_type_load($test_type->type);
+    $this->assertTrue($info !== FALSE, 'Return data from node_type_load() 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_load($target_type);
+    $this->assertTrue($info !== FALSE, 'Return data from node_type_load() is valid for a disabled content type with base of node_content().');
+
+  }
+
 }
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index 42d968d..d1efa43 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -407,14 +407,17 @@ function _node_extract_type($node) {
 }
 
 /**
- * Returns a list of all the available node types.
+ * Returns a list of a site's known node types.
  *
- * This list can include types that are queued for addition or deletion.
- * See _node_types_build() for details.
+ * 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.
  *
  * @return
  *   An array of node types, as objects, keyed by the type.
  *
+ * @see _node_types_build()
  * @see node_type_load()
  */
 function node_type_get_types() {
@@ -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 create 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);
+  $t_arguments = array('%type' => $type->name);
+  if (!$type->custom && $type->base == 'node_content') {
+    // If a module provides a node type with a 'base' key of 'node_content', the
+    // node type will not be automatically disabled when the module is disabled.
+    drupal_set_message(t('The content type %type will not be disabled.', $t_arguments), 'warning');
+  }
+  elseif (!$type->custom && $type->module == '') {
+    // The content types list is built by content_types.inc using node_hook().
+    // However, when the module is disabled, the [base]_form is hidden 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 content type %type must either have 'base' key set to 'node_content' or be defined via hook_node_info().", $t_arguments), 'error');
+  }
 
   $fields = array(
     'type' => (string) $type->type,
@@ -711,28 +728,36 @@ function node_type_update_nodes($old_type, $type) {
 }
 
 /**
- * Builds and returns the list of available node types.
+ * Gets a list of all node types that were not deleted by node_type_delete().
  *
- * 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.
+ * This function provides the {node_type} table state as it would appear after
+ * a rebuild. When the $rebuild parameter is set to TRUE, the table is also
+ * actually rebuilt. The returned node type's 'disabled' property indicates
+ * whether the module that defines it has been disabled. However, any node
+ * type with a value of 'node_content' for the base will never be disabled.
  *
  * @param $rebuild
- *  TRUE to rebuild node types. Equivalent to calling node_types_rebuild().
- *
+ *   TRUE to rebuild {node_type} table of the database.
  * @return
  *   An object with two properties:
- *   - 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.
+ *   - names: An 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 is equal to TRUE. The property is a thin version of the 'types'
+ *     property. See below for exactly what membership in this array means.
+ *   - types: An 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 {node_type}.base is equal to 'node_content', then this
+ *       property is always FALSE. Otherwise, the $type->disabled flag
+ *       takes a value of FALSE for any type in the {node_type} table whose
+ *       module is enabled, or it takes a value of TRUE 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 TRUE for node types implemented by
+ *       hook_node_info() that do not yet appear in the {node_type} table.
+ *       The property is left unset otherwise.
  */
 function _node_types_build($rebuild = FALSE) {
   $cid = 'node_types:' . drupal_container()->get(LANGUAGE_TYPE_INTERFACE)->langcode;
@@ -748,65 +773,97 @@ 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;
+
+      // Overwrite the defaults of 'is_new' and 'disabled' for the new active
+      // type.
+      $active_types->types[$type]->is_new = TRUE;
+      $active_types->types[$type]->disabled = FALSE;
+      // When rebuilding, mark this type as new to ensure it overwrites any
+      // existing values in the database.
+      if ($rebuild) {
+        $active_types->types[$type]->new_state = TRUE;
+      }
     }
   }
-  $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();
+  // Synchronize 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() 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;
+      $type->disabled = FALSE;
     }
-    if (isset($_node_types->types[$type_db])) {
-      $type_object->disabled = FALSE;
+    else {
+      // A hook_node_info() was unavailable for the current occurence from the
+      // database, so enable or disable the occurence depending on its module's
+      // state.
+      if ($type->module == '' || (isset($type->base) && $type->base == 'node_content')) {
+        // Special case: always enabled.
+        $new_state = FALSE;
+        $type->disabled = FALSE;
+      }
+      elseif (isset($enabled_modules[$db_type->module])) {
+        // The module is enabled.
+        $new_state = $type->disabled;
+        $type->disabled = FALSE;
+      }
+      else {
+        // The module is disabled, uninstalled, or missing entirely.
+        $new_state = !$type->disabled;
+        $type->disabled = TRUE;
+      }
     }
-    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]);
-      }
+    // Do not 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 the 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; it is for internal API use
+        // only.
+        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;
 }
 
 /**
@@ -1931,6 +1988,9 @@ function node_menu() {
   );
   // @todo Remove this loop when we have a 'description callback' property.
   foreach (node_type_get_types() as $type) {
+    if ($type->disabled) {
+      continue;
+    }
     $items['node/add/' . $type->type] = array(
       'title' => $type->name,
       'title callback' => 'check_plain',
@@ -3608,8 +3668,9 @@ function _node_access_rebuild_batch_finished($success, $results, $operations) {
  * Implements hook_form().
  */
 function node_content_form(Node $node, $form_state) {
-  // @todo It is impossible to define a content type without implementing
-  //   hook_form(). Remove this requirement.
+  // @todo It is impossible to define a content type with a base that is not
+  //   'node_content' without also implementing hook_form(). Remove this
+  //   requirement.
   $form = array();
   $type = node_type_load($node->type);
 
