Index: modules/filter/filter.admin.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/filter/filter.admin.inc,v
retrieving revision 1.69
diff -u -p -r1.69 filter.admin.inc
--- modules/filter/filter.admin.inc	22 Oct 2010 16:36:14 -0000	1.69
+++ modules/filter/filter.admin.inc	25 Oct 2010 13:47:45 -0000
@@ -17,8 +17,20 @@ function filter_admin_overview($form) {
   $formats = filter_formats();
   $fallback_format = filter_fallback_format();
 
+  // Output a warning if there are formats containing missing filters.
+  $missing_filters = _filter_get_missing_filters();
+  if ($missing_filters) {
+    drupal_set_message(format_plural(count($missing_filters),
+      'The highlighted text format needs to be re-configured, because it is using filters that no longer exist.',
+      'The highlighted text formats need to be re-configured, because they are using filters that no longer exist.'
+    ), 'error');
+  }
+
   $form['#tree'] = TRUE;
   foreach ($formats as $id => $format) {
+    // Set a flag for text formats containing missing filters that need to be
+    // reconfigured.
+    $form['formats'][$id]['#is_missing'] = isset($missing_filters[$id]);
     // Check whether this is the fallback text format. This format is available
     // to all roles and cannot be disabled via the admin interface.
     $form['formats'][$id]['#is_fallback'] = ($id == $fallback_format);
@@ -83,7 +95,7 @@ function theme_filter_admin_overview($va
         drupal_render($form['formats'][$id]['configure']),
         drupal_render($form['formats'][$id]['disable']),
       ),
-      'class' => array('draggable'),
+      'class' => array('draggable', ($form['formats'][$id]['#is_missing'] ? 'error' : '')),
     );
   }
   $header = array(t('Name'), t('Roles'), t('Weight'), array('data' => t('Operations'), 'colspan' => 2));
Index: modules/filter/filter.install
===================================================================
RCS file: /cvs/drupal/drupal/modules/filter/filter.install,v
retrieving revision 1.49
diff -u -p -r1.49 filter.install
--- modules/filter/filter.install	23 Oct 2010 00:43:48 -0000	1.49
+++ modules/filter/filter.install	25 Oct 2010 13:47:45 -0000
@@ -7,6 +7,27 @@
  */
 
 /**
+ * Implements hook_requirements().
+ */
+function filter_requirements($phase) {
+  $requirements = array();
+  if ($phase == 'runtime') {
+    // Check for text formats containing missing/vanished filters.
+    if (_filter_get_missing_filters()) {
+      $requirements['filter'] = array(
+        'title' => t('Text formats'),
+        'value' => t('Missing filters'),
+        'severity' => REQUIREMENT_ERROR,
+        'description' => t('At least one text format contains a filter that was provided by a module that has been disabled. All content using one of these text formats will be hidden until <a href="@text-format-url">text formats have been re-configured</a>.', array(
+          '@text-format-url' => url('admin/config/content/formats'),
+        )),
+      );
+    }
+  }
+  return $requirements;
+}
+
+/**
  * Implements hook_schema().
  */
 function filter_schema() {
Index: modules/filter/filter.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/filter/filter.module,v
retrieving revision 1.355
diff -u -p -r1.355 filter.module
--- modules/filter/filter.module	22 Oct 2010 16:36:14 -0000	1.355
+++ modules/filter/filter.module	25 Oct 2010 13:50:31 -0000
@@ -7,6 +7,15 @@
  */
 
 /**
+ * Filter status indicating a missing filter.
+ *
+ * @see _filter_get_missing_filters()
+ * @see filter_list_format()
+ * @see check_markup()
+ */
+define('FILTER_MISSING', -1);
+
+/**
  * Implements hook_help().
  */
 function filter_help($path, $arg) {
@@ -212,6 +221,18 @@ function filter_format_save($format) {
   if (!isset($format->filters)) {
     $format->filters = array();
   }
+
+  // When a text format is updated, its filter configuration is assumed as
+  // being verified and any pointers to potentially missing filters can be
+  // safely removed.
+  if ($missing_filters = _filter_get_missing_filters($format->format, NULL, FALSE)) {
+    db_delete('filter')
+      ->condition('format', $format->format)
+      ->condition('name', $missing_filters, 'IN')
+      ->condition('name', array_keys($format->filters), 'NOT IN')
+      ->execute();
+  }
+
   $filter_info = filter_get_filters();
   foreach ($filter_info as $name => $filter) {
     // Add new filters without weight to the bottom.
@@ -262,6 +283,7 @@ function filter_format_save($format) {
     cache_clear_all($format->format . ':', 'cache_filter', TRUE);
   }
 
+  // Clear the filter cache whenever a text format is deleted.
   filter_formats_reset();
 
   return $return;
@@ -378,11 +400,42 @@ function filter_modules_enabled($modules
 
 /**
  * Implements hook_modules_disabled().
+ *
+ * If a module is disabled that provides a filter, all text formats using the
+ * filter have to be manually updated. This cannot be automated, because that
+ * would always break the intended filter processing logic.
+ *
+ * @see filter_formats()
+ * @see check_markup()
  */
 function filter_modules_disabled($modules) {
   // Reset the static cache of module-provided filters, in case any of the
   // newly disabled modules defined or altered any filters.
   drupal_static_reset('filter_get_filters');
+
+  if (_filter_get_missing_filters(NULL, $modules)) {
+    drupal_set_message(t('At least one text format contains a filter that was provided by a module that has been disabled. All content using one of these text formats will be hidden until <a href="@text-format-url">text formats have been re-configured</a>.', array(
+      '@text-format-url' => url('admin/config/content/formats'),
+    )), 'error');
+  }
+}
+
+/**
+ * Implements hook_modules_uninstalled().
+ *
+ * If a module is uninstalled that provides a filter, all text formats using the
+ * filter have to be manually updated. This cannot be automated, because that
+ * would always break the intended filter processing logic.
+ *
+ * @see filter_formats()
+ * @see check_markup()
+ */
+function filter_modules_uninstalled($modules) {
+  if (_filter_get_missing_filters(NULL, $modules)) {
+    drupal_set_message(t('At least one text format contains a filter that was provided by a module that has been uninstalled. All content using one of these text formats will be hidden until <a href="@text-format-url">text formats have been re-configured</a>.', array(
+      '@text-format-url' => url('admin/config/content/formats'),
+    )), 'error');
+  }
 }
 
 /**
@@ -688,9 +741,14 @@ function filter_list_format($format_id) 
         if (isset($filter_info[$name]['default settings'])) {
           $filter->settings += $filter_info[$name]['default settings'];
         }
-
-        $format_filters[$name] = $filter;
       }
+      else {
+        // The filter is assigned to the text format, but the module providing
+        // the filter no longer exists. We assign a negative status, so
+        // check_markup() can bail out.
+        $filter->status = FILTER_MISSING;
+      }
+      $format_filters[$name] = $filter;
     }
     $filters[$format_id] = $format_filters;
   }
@@ -699,6 +757,72 @@ function filter_list_format($format_id) 
 }
 
 /**
+ * Helper function to return a list of missing filters keyed by text format.
+ *
+ * Filters registered/exposed by modules may vanish or no longer exist over time
+ * (e.g., when disabling or uninstalling modules providing input filters),
+ * potentially leaving configured text formats in an invalid state. Modules
+ * cannot and must not update text format configurations automatically, since it
+ * is impossible to know the intended purpose of a filter in a text format. When
+ * a text format contains an enabled filter that no longer exists, it cannot
+ * safely filter the user input and outputs an empty string instead (until the
+ * text format configuration has been confirmed by the site administrator).
+ * Therefore, the Filter API internally uses this helper function to check and
+ * retrieve a list of missing filters whenever a module is disabled or
+ * uninstalled, to warn site administrators about stale text format
+ * configurations.
+ *
+ * @param $format_id
+ *   (optional) A text format id to limit the query to.
+ * @param $modules
+ *   (optional) A list of module names to limit the query to.
+ * @param $only_enabled
+ *   (optional) Boolean whether to search only for enabled filters. Defaults to
+ *   TRUE.
+ *
+ * @return
+ *   A list of assigned filters that no longer exist, keyed by text format.
+ *   Optionally limited to the passed arguments.
+ *
+ * @see filter_list_format()
+ * @see check_markup()
+ * @see filter_format_save()
+ */
+function _filter_get_missing_filters($format_id = NULL, array $modules = NULL, $only_enabled = TRUE) {
+  // Retrieve all filters exposed by modules.
+  $filter_info = filter_get_filters();
+
+  // Query all filters not contained in the list of available filters.
+  $query = db_select('filter', 'f')
+    ->fields('f', array('format', 'name'))
+    ->condition('name', array_keys($filter_info), 'NOT IN');
+
+  // Optionally limit to enabled filters.
+  if ($only_enabled) {
+    $query->condition('status', 1);
+  }
+  // Optionally limit to a text format.
+  if (isset($format_id)) {
+    $query->condition('format', $format_id);
+  }
+  // Optionally limit to a list of modules.
+  if (isset($modules)) {
+    $query->condition('module', $modules, 'IN');
+  }
+
+  // Compile the list of vanished filters.
+  $missing_filters = array();
+  foreach ($query->execute() as $filter) {
+    $missing_filters[$filter->format][] = $filter->name;
+  }
+
+  if (isset($format_id)) {
+    return isset($missing_filters[$format_id]) ? $missing_filters[$format_id] : array();
+  }
+  return $missing_filters;
+}
+
+/**
  * Run all the enabled filters on a piece of text.
  *
  * Note: Because filters can inject JavaScript or execute PHP code, security is
@@ -728,7 +852,8 @@ function check_markup($text, $format_id 
     $format_id = filter_fallback_format();
   }
   // If the requested text format does not exist, the text cannot be filtered.
-  if (!$format = filter_format_load($format_id)) {
+  $format = filter_format_load($format_id);
+  if (!$format) {
     watchdog('filter', 'Missing text format: %format.', array('%format' => $format_id), WATCHDOG_ALERT);
     return '';
   }
@@ -753,7 +878,13 @@ function check_markup($text, $format_id 
 
   // Give filters the chance to escape HTML-like data such as code or formulas.
   foreach ($filters as $name => $filter) {
-    if ($filter->status && isset($filter_info[$name]['prepare callback']) && function_exists($filter_info[$name]['prepare callback'])) {
+    if ($filter->status == FILTER_MISSING) {
+      // If a filter ought to run, but no longer exists, return an empty text.
+      // The text stays empty until the text format has been re-configured.
+      watchdog('filter', 'Missing filter %name in text format %format.', array('%name' => $name, '%format' => $format->name), WATCHDOG_ALERT);
+      return '';
+    }
+    elseif ($filter->status && isset($filter_info[$name]['prepare callback']) && function_exists($filter_info[$name]['prepare callback'])) {
       $function = $filter_info[$name]['prepare callback'];
       $text = $function($text, $filter, $format, $langcode, $cache, $cache_id);
     }
Index: modules/filter/filter.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/filter/filter.test,v
retrieving revision 1.80
diff -u -p -r1.80 filter.test
--- modules/filter/filter.test	23 Oct 2010 02:26:11 -0000	1.80
+++ modules/filter/filter.test	25 Oct 2010 13:47:45 -0000
@@ -783,6 +783,34 @@ class FilterSecurityTestCase extends Dru
     $this->assertNoText($body_raw, t('Node body not found.'));
     $this->assertText('Filter: Testing filter', t('Testing filter output found.'));
 
+    // Force disable the filter_test module, without disabling dependent modules.
+    module_disable(array('filter_test'), FALSE);
+
+    // Verify that the content is now empty, because the text format contains
+    // a filter that no longer exists.
+    $this->drupalGet('node/' . $node->nid);
+    $this->assertNoText($body_raw, t('Node body not found.'));
+    $this->assertNoText('Filter: Testing filter', t('Testing filter output not found.'));
+
+    // Ensure we get an error message on the text format overview page.
+    $this->drupalGet('admin/config/content/formats');
+    $this->assertText(t('The highlighted text format needs to be re-configured, because it is using filters that no longer exist.'), t('Error message about broken text formats found.'));
+
+    // Also ensure we get a requirements error.
+    $this->drupalGet('admin/reports/status');
+    $this->assertRaw(t('At least one text format contains a filter that was provided by a module that has been disabled. All content using one of these text formats will be hidden until <a href="@text-format-url">text formats have been re-configured</a>.', array('@text-format-url' => url('admin/config/content/formats'))), t('Requirements error found.'));
+
+    // Update text format.
+    $this->drupalPost('admin/config/content/formats/' . $format_id, array(), t('Save configuration'));
+
+    // Verify no error message.
+    $this->drupalGet('admin/config/content/formats');
+    $this->assertNoText(t('The highlighted text format needs to be re-configured, because it is using filters that no longer exist.'), t('Error message about broken text formats not found.'));
+
+    // Verify node content is displayed.
+    $this->drupalGet('node/' . $node->nid);
+    $this->assertText($body_raw, t('Node body found.'));
+
     // Disable the text format entirely.
     $this->drupalPost('admin/config/content/formats/' . $format_id . '/disable', array(), t('Disable'));
 
Index: modules/php/php.install
===================================================================
RCS file: /cvs/drupal/drupal/modules/php/php.install,v
retrieving revision 1.19
diff -u -p -r1.19 php.install
--- modules/php/php.install	20 Oct 2010 01:15:58 -0000	1.19
+++ modules/php/php.install	25 Oct 2010 13:47:45 -0000
@@ -37,10 +37,3 @@ function php_enable() {
     drupal_set_message(t('A <a href="@php-code">PHP code</a> text format has been created.', array('@php-code' => url('admin/config/content/formats/' . $php_format->format))));
   }
 }
-
-/**
- * Implements hook_disable().
- */
-function php_disable() {
-  drupal_set_message(t('The PHP module has been disabled. Any existing content that was using the PHP filter will now be visible in plain text. This might pose a security risk by exposing sensitive information, if any, used in the PHP code.'));
-}
