Index: update_status.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/update_status/update_status.module,v
retrieving revision 1.83.2.24
diff -u -p -r1.83.2.24 update_status.module
--- update_status.module	1 Dec 2007 04:53:19 -0000	1.83.2.24
+++ update_status.module	11 Jan 2008 06:38:04 -0000
@@ -9,11 +9,41 @@ define('UPDATE_STATUS_CORE_VERSION', '5.
 define('UPDATE_STATUS_DEFAULT_URL', 'http://updates.drupal.org/release-history');
 
 // These are internally used constants for this code, do not modify.
-define('UPDATE_STATUS_CURRENT', 1);       // Up to date.
-define('UPDATE_STATUS_NOT_SECURE', 2);    // Missing security update(s).
-define('UPDATE_STATUS_NOT_CURRENT', 3);   // New available, but not security.
-define('UPDATE_STATUS_NOT_CHECKED', 4);   // Can't compare for some reason.
-define('UPDATE_STATUS_UNKNOWN', 5);       // No available update data.
+/**
+ * Project is missing security update(s).
+ */
+define('UPDATE_STATUS_NOT_SECURE', 1);
+
+/**
+ * Current release has been unpublished and is no longer available.
+ */
+define('UPDATE_STATUS_REVOKED', 2);
+
+/**
+ * Current release is no longer supported by the project maintainer.
+ */
+define('UPDATE_STATUS_NOT_SUPPORTED', 3);
+
+/**
+ * Project has a new release available, but it is not a security release.
+ */
+define('UPDATE_STATUS_NOT_CURRENT', 4);
+
+/**
+ * Project is up to date.
+ */
+define('UPDATE_STATUS_CURRENT', 5);
+
+/**
+ * Project's status cannot be checked.
+ */
+define('UPDATE_STATUS_NOT_CHECKED', -1);
+
+/**
+ * No available update data was found for project.
+ */
+define('UPDATE_STATUS_UNKNOWN', -2);
+
 
 /**
  * Implementation of hook_help().
@@ -325,59 +355,24 @@ function update_status_settings_submit($
  */
 function update_status_requirements($phase) {
   if ($phase == 'runtime') {
-    $requirements['update_status_core']['title'] = t('Drupal core update status');
-    $notification_level = variable_get('update_status_notification_threshold', 'all');
     if ($available = update_status_get_available(FALSE)) {
       $data = update_status_calculate_project_data($available);
-      switch ($data['drupal']['status']) {
-        case UPDATE_STATUS_NOT_CURRENT:
-          $requirements['update_status_core']['value'] = t('Out of date (version @version available)', array('@version' => $data['drupal']['recommended']));
-          $requirements['update_status_core']['severity'] = $notification_level == 'all' ? REQUIREMENT_ERROR : REQUIREMENT_WARNING;
-          $requirements['update_status_core']['reason'] = UPDATE_STATUS_NOT_CURRENT;
-          $requirements['update_status_core']['description'] = _update_status_message_text('core', UPDATE_STATUS_NOT_CURRENT, TRUE);
-          break;
-
-        case UPDATE_STATUS_NOT_SECURE:
-          $requirements['update_status_core']['value'] = t('Not secure! (version @version available)', array('@version' => $data['drupal']['recommended']));
-          $requirements['update_status_core']['severity'] = REQUIREMENT_ERROR;
-          $requirements['update_status_core']['reason'] = UPDATE_STATUS_NOT_SECURE;
-          $requirements['update_status_core']['description'] = _update_status_message_text('core', UPDATE_STATUS_NOT_SECURE, TRUE);
-          break;
-
-        default:
-          $requirements['update_status_core']['value'] = t('Up to date');
-          break;
-      }
+      // First, populate the requirements for core:
+      $requirements['update_status_core'] = _update_status_requirement_check($data['drupal'], 'core');
       // We don't want to check drupal a second time.
       unset($data['drupal']);
-      $not_current = FALSE;
       if (!empty($data)) {
-        $requirements['update_status_contrib']['title'] = t('Module update status');
-        // Default to being current until we see otherwise.
-        $requirements['update_status_contrib']['value'] = t('Up to date');
-        foreach (array_keys($data) as $project) {
-          if (isset($available[$project])) {
-            if ($data[$project]['status'] == UPDATE_STATUS_NOT_SECURE) {
-              $requirements['update_status_contrib']['value'] = t('Not secure!');
-              $requirements['update_status_contrib']['severity'] = REQUIREMENT_ERROR;
-              $requirements['update_status_contrib']['reason'] = UPDATE_STATUS_NOT_SECURE;
-              $requirements['update_status_contrib']['description'] = _update_status_message_text('contrib', UPDATE_STATUS_NOT_SECURE, TRUE);
-              break;
-            }
-            elseif ($data[$project]['status'] == UPDATE_STATUS_NOT_CURRENT) {
-              $not_current = TRUE;
-            }
-          }
-        }
-        if (!isset($requirements['update_status_contrib']['severity']) && $not_current) {
-          $requirements['update_status_contrib']['severity'] = $notification_level == 'all' ? REQUIREMENT_ERROR : REQUIREMENT_WARNING;
-          $requirements['update_status_contrib']['value'] = t('Out of date');
-          $requirements['update_status_contrib']['reason'] = UPDATE_STATUS_NOT_CURRENT;
-          $requirements['update_status_contrib']['description'] = _update_status_message_text('contrib', UPDATE_STATUS_NOT_CURRENT, TRUE);
-        }
+        // Now, sort our $data array based on each project's status. The
+        // status constants are numbered in the right order of precedence, so
+        // we just need to make sure the projects are sorted in ascending
+        // order of status, and we can look at the first project we find.
+        uasort($data, '_update_status_project_status_sort');
+        $first_project = reset($data);
+        $requirements['update_status_contrib'] = _update_status_requirement_check($first_project, 'contrib');
       }
     }
     else {
+      $requirements['update_status_core']['title'] = t('Drupal core update status');
       $requirements['update_status_core']['value'] = t('No update data available');
       $requirements['update_status_core']['severity'] = REQUIREMENT_WARNING;
       $requirements['update_status_core']['reason'] = UPDATE_STATUS_UNKNOWN;
@@ -388,6 +383,67 @@ function update_status_requirements($pha
 }
 
 /**
+ * Private helper method to fill in the requirements array.
+ *
+ * This is shared for both core and contrib to generate the right elements in
+ * the array for hook_requirements().
+ *
+ * @param $project
+ *  Array of information about the project we're testing as returned by
+ *  update_calculate_project_data().
+ * @param $type
+ *  What kind of project is this ('core' or 'contrib').
+ *
+ * @return
+ *  An array to be included in the nested $requirements array.
+ *
+ * @see hook_requirements()
+ * @see update_status_requirements()
+ * @see update_status_calculate_project_data()
+ */
+function _update_status_requirement_check($project, $type) {
+  $requirement = array();
+  if ($type == 'core') {
+    $requirement['title'] = t('Drupal core update status');
+  }
+  else {
+    $requirement['title'] = t('Module update status');
+  }
+  $status = $project['status'];
+  if ($status != UPDATE_STATUS_CURRENT) {
+    $requirement['reason'] = $status;
+    $requirement['description'] = _update_status_message_text($type, $status, TRUE);
+    $requirement['severity'] = REQUIREMENT_ERROR;
+  }
+  switch ($status) {
+    case UPDATE_STATUS_NOT_SECURE:
+      $requirement['value'] = t('Not secure!');
+      break;
+    case UPDATE_STATUS_REVOKED:
+      $requirement['value'] = t('Revoked!');
+      break;
+    case UPDATE_STATUS_NOT_SUPPORTED:
+      $requirement['value'] = t('Unsupported release');
+      break;
+    case UPDATE_STATUS_NOT_CURRENT:
+      $requirement['value'] = t('Out of date');
+      $requirement['severity'] = variable_get('update_notification_threshold', 'all') == 'all' ? REQUIREMENT_ERROR : REQUIREMENT_WARNING;
+      break;
+    case UPDATE_STATUS_UNKNOWN:
+    case UPDATE_STATUS_NOT_CHECKED:
+      $requirement['value'] = isset($project['reason']) ? $project['reason'] : t('Can not determine status');
+      $requirement['severity'] = REQUIREMENT_WARNING;      
+      break;
+    default:
+      $requirement['value'] = t('Up to date');
+  }
+  if ($status != UPDATE_STATUS_CURRENT && $type == 'core' && isset($project['recommended'])) {
+    $requirement['value'] .= ' '. t('(version @version available)', array('@version' => $project['recommended']));
+  }
+  return $requirement;
+}
+
+/**
  * Implementation of hook_cron().
  */
 function update_status_cron() {
@@ -465,8 +521,8 @@ function update_status_system_submit($fo
  *   String to indicate what kind of message to generate. Can be either
  *   'core' or 'contrib'.
  * @param $msg_reason
- *   Integer constant specifying why message is generated. Can be either
- *   UPDATE_STATUS_NOT_CURRENT or UPDATE_STATUS_NOT_SECURE.
+ *   Integer constant specifying why message is generated. Can be any of the
+ *   UPDATE_STATUS_* constants from the top of this file.
  * @param $report_link
  *   Boolean that controls if a link to the updates report should be added.
  * @return
@@ -475,6 +531,33 @@ function update_status_system_submit($fo
 function _update_status_message_text($msg_type, $msg_reason, $report_link = FALSE) {
   $text = '';
   switch ($msg_reason) {
+    case UPDATE_STATUS_NOT_SECURE:
+      if ($msg_type == 'core') {
+        $text = t('There is a security update available for your version of Drupal. To ensure the security of your server, you should update immediately!');
+      }
+      else {
+        $text = t('There are security updates available for one or more of your modules. To ensure the security of your server, you should update immediately!');
+      }
+      break;
+
+    case UPDATE_STATUS_REVOKED:
+      if ($msg_type == 'core') {
+        $text = t('Your version of Drupal has been revoked and is no longer available for download. Upgrading is strongly recommended!');
+      }
+      else {
+        $text = t('The installed version of at least one of your modules or themes has been revoked and is no longer available for download. Upgrading or disabling is strongly recommended!');
+      }
+      break;
+
+    case UPDATE_NOT_SUPPORTED:
+      if ($msg_type == 'core') {
+        $text = t('Your version of Drupal is no longer supported. Upgrading is strongly recommended!');
+      }
+      else {
+        $text = t('The installed version of at least one of your modules or themes is no longer supported. Upgrading or disabling is strongly recommended! Please see the project homepage for more details.');
+      }
+      break;
+
     case UPDATE_STATUS_NOT_CURRENT:
       if ($msg_type == 'core') {
         $text = t('There are updates available for your version of Drupal. To ensure the proper functioning of your site, you should update as soon as possible.');
@@ -484,12 +567,13 @@ function _update_status_message_text($ms
       }
       break;
 
-    case UPDATE_STATUS_NOT_SECURE:
+    case UPDATE_STATUS_UNKNOWN:
+    case UPDATE_STATUS_NOT_CHECKED:
       if ($msg_type == 'core') {
-        $text = t('There is a security update available for your version of Drupal. To ensure the security of your server, you should update immediately!');
+        $text = t('There was a problem determining the status of available updates for your version of Drupal.');
       }
       else {
-        $text = t('There are security updates available for one or more of your modules. To ensure the security of your server, you should update immediately!');
+        $text = t('There was a problem determining the status of available updates for one or more of your modules or themes.');
       }
       break;
   }
@@ -688,10 +772,20 @@ function update_status_process_project_i
  * remote servers, calculate the current status.
  *
  * This function is the heart of the update status feature. It iterates over
- * every currently installed project, and for each one, decides what major
- * release series to consider (the larger of the major version currently
- * installed and the default major version specified by the maintainer of that
- * project).
+ * every currently installed project. For each one, it first checks if the
+ * project has been flagged with a special status like "unsupported" or
+ * "insecure", or if the project node itself has been unpublished. In any of
+ * those cases, the project is marked with an error and the next project is
+ * considered.
+ *
+ * If the project itself is valid, the function decides what major release
+ * series to consider. The project defines what the currently supported major
+ * versions are for each version of core, so the first step is to make sure
+ * the current version is still supported. If so, that's the target version.
+ * If the current version is unsupported, the project maintainer's recommended
+ * major version is used. There's also a check to make sure that this function
+ * never recommends an earlier release than the currently installed major
+ * version.
  *
  * Given a target major version, it scans the available releases looking for
  * the specific release to recommend (avoiding beta releases and development
@@ -746,28 +840,155 @@ function update_status_calculate_project
   $settings = variable_get('update_status_settings', array());
   foreach ($projects as $project => $project_info) {
     if (isset($available[$project])) {
+
+      // If the project status is marked as something bad, there's nothing
+      // else to consider.
+      if (isset($available[$project]['project_status'])) {
+        switch ($available[$project]['project_status']) {
+          case 'insecure':
+            $projects[$project]['status'] = UPDATE_STATUS_NOT_SECURE;
+            if (empty($projects[$project]['extra'])) {
+              $projects[$project]['extra'] = array();
+            }
+            $projects[$project]['extra'][] = array(
+              'class' => 'project-not-secure',
+              'label' => t('Project not secure'),
+              'data' => t('This project has been labeled insecure by the Drupal security team, and is no longer available for download. Immediately disabling everything included by this project is strongly recommended!'),
+            );
+            break;
+          case 'unpublished':
+          case 'revoked':
+            $projects[$project]['status'] = UPDATE_STATUS_REVOKED;
+            if (empty($projects[$project]['extra'])) {
+              $projects[$project]['extra'] = array();
+            }
+            $projects[$project]['extra'][] = array(
+              'class' => 'project-revoked',
+              'label' => t('Project revoked'),
+              'data' => t('This project has been revoked, and is no longer available for download. Disabling everything included by this project is strongly recommended!'),
+            );
+            break;
+          case 'unsupported':
+            $projects[$project]['status'] = UPDATE_STATUS_NOT_SUPPORTED;
+            if (empty($projects[$project]['extra'])) {
+              $projects[$project]['extra'] = array();
+            }
+            $projects[$project]['extra'][] = array(
+              'class' => 'project-not-supported',
+              'label' => t('Project not supported'),
+              'data' => t('This project is no longer supported, and is no longer available for download. Disabling everything included by this project is strongly recommended!'),
+            );
+            break;
+          default:
+            // Assume anything else (e.g. 'published') is valid and we should
+            // perform the rest of the logic in this function.
+            break;
+        }
+      }
+
+      if (!empty($projects[$project]['status'])) {
+        // We already know the status for this project, so there's nothing
+        // else to compute. Just record everything else we fetched from the
+        // XML file into our projects array and move to the next project.
+        $projects[$project] += $available[$project];
+        continue;
+      }
+
       // Figure out the target major version.
       $existing_major = $project_info['existing_major'];
-      if (isset($available[$project]['default_major'])) {
-        $default_major = $available[$project]['default_major'];
-        $target_major = max($existing_major, $default_major);
+      $supported_majors = array();
+      if (isset($available[$project]['supported_majors'])) {
+        $supported_majors = explode(',', $available[$project]['supported_majors']);
+      }
+      elseif (isset($available[$project]['default_major'])) {
+        // Older release history XML file without supported or recommended.
+        $supported_majors[] = $available[$project]['default_major'];
+      }
+
+      if (in_array($existing_major, $supported_majors)) {
+        // Still supported, stay at the current major version.
+        $target_major = $existing_major;
+      }
+      elseif (isset($available[$project]['recommended_major'])) {
+        // Since 'recommended_major' is defined, we know this is the new XML
+        // format. Therefore, we know the current release is unsupported since
+        // its major version was not in the 'supported_majors' list. We should
+        // find the best release from the recommended major version.
+        $target_major = $available[$project]['recommended_major'];
+        $projects[$project]['status'] = UPDATE_STATUS_NOT_SUPPORTED;
+      }
+      elseif (isset($available[$project]['default_major'])) {
+        // Older release history XML file without recommended, so recommend
+        // the currently defined "default_major" version.
+        $target_major = $available[$project]['default_major'];
       }
       else {
+        // Malformed XML file? Stick with the current version.
         $target_major = $existing_major;
       }
 
+      // Make sure we never tell the admin to downgrade. If we recommended an
+      // earlier version than the one they're running, they'd face an
+      // impossible data migration problem, since Drupal never supports a DB
+      // downgrade path. In the unfortunate case that what they're running is
+      // unsupported, and there's nothing newer for them to upgrade to, we
+      // can't print out a "Recommended version", but just have to tell them
+      // what they have is unsupported and let them figure it out.
+      $target_major = max($existing_major, $target_major);
+
       $version_patch_changed = '';
       $patch = '';
 
+      // Defend ourselves from XML history files that contain no releases.
+      if (empty($available[$project]['releases'])) {
+        $projects[$project]['status'] = UPDATE_STATUS_UNKNOWN;
+        $projects[$project]['reason'] = t('No available releases found');
+        continue;
+      }
       foreach ($available[$project]['releases'] as $version => $release) {
-        // Ignore unpublished releases.
-        if ($release['status'] != 'published') {
+        // First, if this is the existing release, check a few conditions.
+        if ($projects[$project]['existing_version'] == $version) {
+          if (isset($release['terms']['Release type']) &&
+              in_array('Insecure', $release['terms']['Release type'])) {
+            $projects[$project]['status'] = UPDATE_STATUS_NOT_SECURE;
+          }
+          elseif ($release['status'] == 'unpublished') {
+            $projects[$project]['status'] = UPDATE_STATUS_REVOKED;
+            if (empty($projects[$project]['extra'])) {
+              $projects[$project]['extra'] = array();
+            }
+            $projects[$project]['extra'][] = array(
+              'class' => 'release-revoked',
+              'label' => t('Release revoked'),
+              'data' => t('Your currently installed release has been revoked, and is no longer available for download. Disabling everything included in this release or upgrading is strongly recommended!'),
+            );
+          }
+          elseif (isset($release['terms']['Release type']) &&
+                  in_array('Unsupported', $release['terms']['Release type'])) {
+            $projects[$project]['status'] = UPDATE_STATUS_NOT_SUPPORTED;
+            if (empty($projects[$project]['extra'])) {
+              $projects[$project]['extra'] = array();
+            }
+            $projects[$project]['extra'][] = array(
+              'class' => 'release-not-supported',
+              'label' => t('Release not supported'),
+              'data' => t('Your currently installed release is now unsupported, and is no longer available for download. Disabling everything included in this release or upgrading is strongly recommended!'),
+            );
+          }
+        }
+
+        // Otherwise, ignore unpublished, insecure, or unsupported releases.
+        if ($release['status'] == 'unpublished' || 
+            (isset($release['terms']['Release type']) &&
+             (in_array('Insecure', $release['terms']['Release type']) ||
+              in_array('Unsupported', $release['terms']['Release type'])))) {
           continue;
         }
 
-        // See if this is a higher major version than our target, and if so,
-        // record it as an "Also available" release.
-        if ($release['version_major'] > $target_major) {
+        // See if this is a higher major version than our target and yet still
+        // supported. If so, record it as an "Also available" release.
+        if ($release['version_major'] > $target_major &&
+            in_array($release['version_major'], $supported_majors)) {
           if (!isset($available[$project]['also'])) {
             $available[$project]['also'] = array();
           }
@@ -835,8 +1056,7 @@ function update_status_calculate_project
         }
 
         // See if this release is a security update.
-        if (isset($release['terms'])
-            && isset($release['terms']['Release type'])
+        if (isset($release['terms']['Release type'])
             && in_array('Security update', $release['terms']['Release type'])) {
           $projects[$project]['security updates'][] = $release;
         }
@@ -868,8 +1088,18 @@ function update_status_calculate_project
       // Check to see if we need an update or not.
       //
 
-      // If we don't know what to recommend, there's nothing much we can
-      // report, so bail out early.
+      if (!empty($projects[$project]['security updates'])) {
+        // If we found security updates, that always trumps any other status.
+        $projects[$project]['status'] = UPDATE_STATUS_NOT_SECURE;
+      }
+
+      if (isset($projects[$project]['status'])) {
+        // If we already know the status, we're done.
+        continue;
+      }
+
+      // If we don't know what to recommend, there's nothing we can report.
+      // Bail out early.
       if (!isset($projects[$project]['recommended'])) {
         $projects[$project]['status'] = UPDATE_STATUS_UNKNOWN;
         $projects[$project]['reason'] = t('No available releases found');
@@ -887,29 +1117,31 @@ function update_status_calculate_project
         continue;
       }
 
-      // Then, check based upon type and site-wide error threshold setting.
-      $notification_level = variable_get('update_status_notification_threshold', 'all');
+      // If we're running a dev snapshot, compare the date of the dev snapshot
+      // with the latest official version, and record the absolute latest in
+      // 'latest_dev' so we can correctly decide if there's a newer release
+      // than our current snapshot.
+      if ($projects[$project]['type'] == 'dev') {
+        if (isset($available[$project]['dev_version']) && $available[$project]['releases'][$available[$project]['dev_version']]['date'] > $available[$project]['releases'][$available[$project]['latest_version']]['date']) {
+          $projects[$project]['latest_dev'] = $available[$project]['dev_version'];
+        }
+        else {
+          $projects[$project]['latest_dev'] = $available[$project]['latest_version'];
+        }
+      }
 
+      // Figure out the status, based on what we've seen and the install type.
       switch ($projects[$project]['type']) {
         case 'official':
           if ($projects[$project]['existing_version'] == $projects[$project]['recommended'] || $projects[$project]['existing_version'] == $projects[$project]['latest_version']) {
             $projects[$project]['status'] = UPDATE_STATUS_CURRENT;
           }
           else {
-            if (!empty($projects[$project]['security updates'])) {
-              $projects[$project]['status'] = UPDATE_STATUS_NOT_SECURE;
-            }
-            else {
-              $projects[$project]['status'] = UPDATE_STATUS_NOT_CURRENT;
-            }
+            $projects[$project]['status'] = UPDATE_STATUS_NOT_CURRENT;
           }
           break;
-        case 'dev':
-          if (!empty($projects[$project]['security updates'])) {
-            $projects[$project]['status'] = UPDATE_STATUS_NOT_SECURE;
-            break;
-          }
 
+        case 'dev':
           $latest = $available[$project]['releases'][$projects[$project]['latest_dev']];
           if (empty($projects[$project]['datestamp'])) {
             $projects[$project]['status'] = UPDATE_STATUS_NOT_CHECKED;
@@ -965,9 +1197,11 @@ function theme_update_status_report($dat
         $icon = theme('image', 'misc/watchdog-ok.png');
         break;
       case UPDATE_STATUS_NOT_SECURE:
+      case UPDATE_STATUS_REVOKED:
+      case UPDATE_STATUS_NOT_SUPPORTED:
       case UPDATE_STATUS_NOT_CURRENT:
         if ($notification_level == 'all'
-            || $project['status'] == UPDATE_STATUS_NOT_SECURE) {
+            || $project['status'] != UPDATE_STATUS_NOT_CURRENT) {
           $class = 'error';
           $icon = theme('image', 'misc/watchdog-error.png');
           break;
@@ -981,21 +1215,26 @@ function theme_update_status_report($dat
 
     $row = '<div class="version-status">';
     switch ($project['status']) {
-      case UPDATE_STATUS_CURRENT:
-        $row .= t('Up to date');
-        break;
       case UPDATE_STATUS_NOT_SECURE:
-        $row .= '<span class="security-error">';
-        $row .= t('Security update required!');
-        $row .= '</span>';
+        $row .= '<span class="security-error">'. t('Security update required!') .'</span>';
+        break;
+      case UPDATE_STATUS_REVOKED:
+        $row .= '<span class="revoked">'. t('Revoked!') .'</span>';
+        break;
+      case UPDATE_STATUS_NOT_SUPPORTED:
+        $row .= '<span class="not-supported">'. t('Not supported!') .'</span>';
         break;
       case UPDATE_STATUS_NOT_CURRENT:
-        $row .= t('Update available');
+        $row .= '<span class="not-current">'. t('Update available') .'</span>';
+        break;
+      case UPDATE_STATUS_CURRENT:
+        $row .= '<span class="current">'. t('Up to date') .'</span>';
         break;
       default:
         $row .= check_plain($project['reason']);
         break;
     }
+
     $row .= '<span class="icon">'. $icon .'</span>';
     $row .= "</div>\n";
 
@@ -1037,6 +1276,7 @@ function theme_update_status_report($dat
           if ($project['recommended'] != $project['latest_version']
               || !empty($project['also'])
               || ($project['type'] == 'dev'
+                 && isset($project['dev_version'])
                  && $project['latest_version'] != $project['dev_version']
                  && $project['recommended'] != $project['dev_version'])
               || (isset($project['security updates'][0])
@@ -1060,6 +1300,7 @@ function theme_update_status_report($dat
       }
       if ($project['type'] == 'dev'
           && $project['status'] != UPDATE_STATUS_CURRENT
+          && isset($project['dev_version'])
           && $project['recommended'] != $project['dev_version']) {
         $row .= theme('update_status_version', $project['releases'][$project['dev_version']], t('Development version:'), 'version-latest');
       }
@@ -1303,3 +1544,19 @@ class update_status_xml_parser {
     }
   }
 }
+
+/**
+ * Private sort function to order projects based on their status.
+ *
+ * @see update_requirements()
+ * @see uasort()
+ */
+function _update_status_project_status_sort($a, $b) {
+  // The status constants are numerically in the right order, so we can
+  // usually subtract the two to compare in the order we want. However,
+  // negative status values should be treated as if they are huge, since we
+  // always want them at the bottom of the list.
+  $a_status = $a['status'] > 0 ? $a['status'] : (-10 * $a['status']);
+  $b_status = $b['status'] > 0 ? $b['status'] : (-10 * $b['status']);
+  return $a_status - $b_status;
+}
