Index: CHANGELOG.txt
===================================================================
RCS file: /cvs/drupal/drupal/CHANGELOG.txt,v
retrieving revision 1.216
diff -u -p -r1.216 CHANGELOG.txt
--- CHANGELOG.txt	3 Jul 2007 18:48:41 -0000	1.216
+++ CHANGELOG.txt	6 Jul 2007 03:07:09 -0000
@@ -62,6 +62,7 @@ Drupal 6.0, xxxx-xx-xx (development vers
 - Added support for OpenID.
 - Added support for configurable actions.
 - Made user profiles easier to theme by using array rendering and supplying template files.
+- Added the Update status module to automatically check for available updates and warn sites if they are missing security updates or newer versions.
 
 Drupal 5.0, 2007-01-15
 ----------------------
Index: MAINTAINERS.txt
===================================================================
RCS file: /cvs/drupal/drupal/MAINTAINERS.txt,v
retrieving revision 1.16
diff -u -p -r1.16 MAINTAINERS.txt
--- MAINTAINERS.txt	1 Jun 2007 09:42:44 -0000	1.16
+++ MAINTAINERS.txt	6 Jul 2007 03:07:09 -0000
@@ -66,6 +66,10 @@ STATISTICS MODULE
 M: Jeremy Andrews <jeremy@kerneltrap.com>
 S: maintained
 
+UPDATE MODULE
+M: Derek Wright <http://drupal.org/user/46549/contact>
+S: maintained
+
 XML-RPC SERVER/CLIENT
 M: John VanDyk <http://drupal.org/user/2375/contact>
 S: maintained
Index: profiles/default/default.profile
===================================================================
RCS file: /cvs/drupal/drupal/profiles/default/default.profile,v
retrieving revision 1.15
diff -u -p -r1.15 default.profile
--- profiles/default/default.profile	4 Jul 2007 15:49:44 -0000	1.15
+++ profiles/default/default.profile	6 Jul 2007 03:07:09 -0000
@@ -8,7 +8,7 @@
  *  An array of modules to be enabled.
  */
 function default_profile_modules() {
-  return array('color', 'comment', 'help', 'menu', 'taxonomy', 'dblog');
+  return array('color', 'comment', 'help', 'menu', 'taxonomy', 'dblog', 'update');
 }
 
 /**
Index: modules/update/update-rtl.css
===================================================================
RCS file: modules/update/update-rtl.css
diff -N modules/update/update-rtl.css
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/update/update-rtl.css	6 Jul 2007 03:07:09 -0000
@@ -0,0 +1,27 @@
+/* $Id: update-rtl.css,v 1.1 2007/06/28 20:38:26 dww Exp $ */
+
+.update .project {
+  padding-right: .25em;
+}
+
+.update .version-status {
+  float: left;
+  padding-left: 10px;
+}
+
+.update .version-status .icon {
+  padding-right: .5em;
+}
+
+.update table.version .version-title {
+  padding-left: 1em;
+}
+
+.update table.version .version-details {
+  padding-left: .5em;
+}
+
+.update table.version .version-links {
+  text-align: left;
+  padding-left: 1em;
+}
Index: modules/update/update.compare.inc
===================================================================
RCS file: modules/update/update.compare.inc
diff -N modules/update/update.compare.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/update/update.compare.inc	6 Jul 2007 03:07:09 -0000
@@ -0,0 +1,401 @@
+<?php
+// $Id: update.compare.inc,v 1.1 2007/06/29 08:01:04 dww Exp $
+
+/**
+ * @file
+ * Code required only when comparing available updates to existing data.
+ */
+
+/**
+ * Fetch an array of installed and enabled projects.
+ *
+ * This is only responsible for generating an array of projects (taking into
+ * account projects that include more than one module or theme). Other
+ * information like the specific version and install type (official release,
+ * dev snapshot, etc) is handled later in update_process_project_info() since
+ * that logic is only required when preparing the status report, not for
+ * fetching the available release data.
+ *
+ * @see update_process_project_info()
+ * @see update_calculate_project_data()
+ *
+ */
+function update_get_projects() {
+  static $projects = array();
+  if (empty($projects)) {
+    _update_process_info_list($projects, module_rebuild_cache(), 'module');
+    _update_process_info_list($projects, system_theme_data(), 'theme');
+  }
+  return $projects;
+}
+
+/**
+ * Populate an array of project data.
+ */
+function _update_process_info_list(&$projects, &$list, $project_type) {
+  foreach ($list as $file) {
+    if (empty($file->status)) {
+      // Skip disabled modules or themes.
+      continue;
+    }
+
+    // Skip if the .info file is broken.
+    if (empty($file->info)) {
+      continue;
+    }
+
+    // If the .info doesn't define the 'project', try to figure it out.
+    if (!isset($file->info['project'])) {
+      $file->info['project'] = update_get_project_name($file);
+    }
+
+    if (!isset($projects[$file->info['project']])) {
+      // Only process this if we haven't done this project, since a single
+      // project can have multiple modules or themes.
+      $projects[$file->info['project']] = array(
+        'name' => $file->info['project'],
+        'info' => $file->info,
+        'datestamp' => isset($file->info['datestamp']) ? $file->info['datestamp'] : 0,
+        'includes' => array($file->name => $file->info['name']),
+        'project_type' => $file->info['project'] == 'drupal' ? 'core' : $project_type,
+      );
+    }
+    else {
+      $projects[$file->info['project']]['includes'][$file->name] = $file->info['name'];
+    }
+  }
+}
+
+/**
+ * Given a $file object (as returned by system_get_files_database()), figure
+ * out what project it belongs to.
+ *
+ * @see system_get_files_database()
+ */
+function update_get_project_name($file) {
+  $project_name = '';
+  if (isset($file->info['project'])) {
+    $project_name = $file->info['project'];
+  }
+  elseif (isset($file->info['package']) && (strpos($file->info['package'], 'Core -') !== FALSE)) {
+    $project_name = 'drupal';
+  }
+  elseif (in_array($file->name, array('bluemarine', 'chameleon', 'garland', 'marvin', 'minnelli', 'pushbutton'))) {
+    // Unfortunately, there's no way to tell if a theme is part of core,
+    // so we must hard-code a list here.
+    $project_name = 'drupal';
+  }
+  else {
+    // This isn't part of core, so guess the project from the directory.
+    $last = '';
+    foreach (array_reverse(explode('/', $file->filename)) as $dir) {
+      if ($dir == 'modules' || $dir == 'themes') {
+        break;
+      }
+      $last = $dir;
+    }
+    if ($last) {
+      $project_name = $last;
+    }
+    else {
+      continue;
+    }
+  }
+  return $project_name;
+}
+
+/**
+ * Process the list of projects on the system to figure out the currently
+ * installed versions, and other information that is required before we can
+ * compare against the available releases to produce the status report.
+ *
+ * @param $projects
+ *   Array of project information from update_get_projects().
+ */
+function update_process_project_info(&$projects) {
+  foreach ($projects as $key => $project) {
+    // Assume an official release until we see otherwise.
+    $install_type = 'official';
+
+    $info = $project['info'];
+
+    if (isset($info['version'])) {
+      // Check for development snapshots
+      if (preg_match('@(dev|HEAD)@', $info['version'])) {
+        $install_type = 'dev';
+      }
+
+      // Figure out what the currently installed major version is. We need
+      // to handle both contribution (e.g. "5.x-1.3", major = 1) and core
+      // (e.g. "5.1", major = 5) version strings.
+      $matches = array();
+      if (preg_match('/^(\d+\.x-)?(\d+)\..*$/', $info['version'], $matches)) {
+        $info['major'] = $matches[2];
+      }
+      elseif (!isset($info['major'])) {
+        // This would only happen for version strings that don't follow the
+        // drupal.org convention. We let contribs define "major" in their
+        // .info in this case, and only if that's missing would we hit this.
+        $info['major'] = -1;
+      }
+    }
+    else {
+      // No version info available at all.
+      $install_type = 'unknown';
+      $info['version'] = t('Unknown');
+      $info['major'] = -1;
+    }
+
+    // Finally, save the results we care about into the $projects array.
+    $projects[$key]['existing_version'] = $info['version'];
+    $projects[$key]['existing_major'] = $info['major'];
+    $projects[$key]['install_type'] = $install_type;
+    unset($projects[$key]['info']);
+  }
+}
+
+/**
+ * Given the installed projects and the available release data retrieved from
+ * 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).
+ *
+ * Given a target major version, it scans the available releases looking for
+ * the specific release to recommend (avoiding beta releases and development
+ * snapshots if possible). This is complicated to describe, but an example
+ * will help clarify. For the target major version, find the highest patch
+ * level. If there is a release at that patch level with no extra ("beta",
+ * etc), then we recommend the release at that patch level with the most
+ * recent release date. If every release at that patch level has extra (only
+ * betas), then recommend the latest release from the previous patch
+ * level. For example:
+ *
+ * 1.6-bugfix <-- recommended version because 1.6 already exists.
+ * 1.6
+ *
+ * or
+ *
+ * 1.6-beta
+ * 1.5 <-- recommended version because no 1.6 exists.
+ * 1.4
+ *
+ * It also looks for the latest release from the same major version, even a
+ * beta release, to display to the user as the "Latest version" option.
+ * Additionally, it finds the latest official release from any higher major
+ * versions that have been released to provide a set of "Also available"
+ * options.
+ *
+ * Finally, and most importantly, it keeps scanning the release history until
+ * it gets to the currently installed release, searching for anything marked
+ * as a security update. If any security updates have been found between the
+ * recommended release and the installed version, all of the releases that
+ * included a security fix are recorded so that the site administrator can be
+ * warned their site is insecure, and links pointing to the release notes for
+ * each security update can be included (which, in turn, will link to the
+ * official security announcements for each vulnerability).
+ *
+ * This function relies on the fact that the .xml release history data comes
+ * sorted based on major version and patch level, then finally by release date
+ * if there are multiple releases such as betas from the same major.patch
+ * version (e.g. 5.x-1.5-beta1, 5.x-1.5-beta2, and 5.x-1.5). Development
+ * snapshots for a given major version are always listed last.
+ *
+ * @param $available
+ *  Array of data about available project releases.
+ *
+ * @see update_get_available()
+ * @see update_get_projects()
+ * @see update_process_project_info()
+ */
+function update_calculate_project_data($available) {
+  $projects = update_get_projects();
+  update_process_project_info($projects);
+  foreach ($projects as $project => $project_info) {
+    if (isset($available[$project])) {
+      // 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);
+      }
+      else {
+        $target_major = $existing_major;
+      }
+
+      $version_patch_changed = '';
+      $patch = '';
+
+      foreach ($available[$project]['releases'] as $version => $release) {
+        // Ignore unpublished releases.
+        if ($release['status'] != 'published') {
+          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) {
+          if (!isset($available[$project]['also'])) {
+            $available[$project]['also'] = array();
+          }
+          if (!isset($available[$project]['also'][$release['version_major']])) {
+            $available[$project]['also'][$release['version_major']] = $version;
+          }
+          // Otherwise, this release can't matter to us, since it's neither
+          // from the release series we're currently using nor the recommended
+          // release. We don't even care about security updates for this
+          // branch, since if a project maintainer puts out a security release
+          // at a higher major version and not at the lower major version,
+          // they must change the default major release at the same time, in
+          // which case we won't hit this code.
+          continue;
+        }
+
+        // Look for the 'latest version' if we haven't found it yet. Latest is
+        // defined as the most recent version for the target major version.
+        if (!isset($available[$project]['latest_version'])
+            && $release['version_major'] == $target_major) {
+          $available[$project]['latest_version'] = $version;
+        }
+
+        // Look for the development snapshot release for this branch.
+        if (!isset($available[$project]['dev_version'])
+            && $release['version_major'] == $target_major
+            && isset($release['version_extra'])
+            && $release['version_extra'] == 'dev') {
+          $available[$project]['dev_version'] = $version;
+        }
+
+        // Look for the 'recommended' version if we haven't found it yet (see
+        // phpdoc at the top of this function for the definition).
+        if (!isset($available[$project]['recommended'])
+            && $release['version_major'] == $target_major
+            && isset($release['version_patch'])) {
+          if ($patch != $release['version_patch']) {
+            $patch = $release['version_patch'];
+            $version_patch_changed = $release['version'];
+          }
+          if (empty($release['version_extra']) && $patch == $release['version_patch']) { 
+            $available[$project]['recommended'] = $version_patch_changed;
+          }
+        }
+
+        // Stop searching once we hit the currently installed version.
+        if ($projects[$project]['existing_version'] == $version) {
+          break;
+        }
+
+        // If we're running a dev snapshot and have a timestamp, stop
+        // searching for security updates once we hit an official release
+        // older than what we've got.  Allow 100 seconds of leeway to handle
+        // differences between the datestamp in the .info file and the
+        // timestamp of the tarball itself (which are usually off by 1 or 2
+        // seconds) so that we don't flag that as a new release.
+        if ($projects[$project]['install_type'] == 'dev') {
+          if (empty($projects[$project]['datestamp'])) {
+            // We don't have current timestamp info, so we can't know.
+            continue;
+          }
+          elseif (isset($release['date']) && ($projects[$project]['datestamp'] + 100 > $release['date'])) {
+            // We're newer than this, so we can skip it.
+            continue;
+          }
+        }
+
+        // See if this release is a security update.
+        if (isset($release['terms'])
+            && isset($release['terms']['Release type'])
+            && in_array('Security update', $release['terms']['Release type'])) {
+          $projects[$project]['security updates'][] = $release;
+        }
+      }
+
+      // If we were unable to find a recommended version, then make the latest
+      // version the recommended version if possible.
+      if (!isset($available[$project]['recommended']) && isset($available[$project]['latest_version'])) {
+        $available[$project]['recommended'] = $available[$project]['latest_version'];
+      }
+
+      // 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]['install_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'];
+        }
+      }
+
+      // Stash the info about available releases into our $projects array.
+      $projects[$project] += $available[$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 (!isset($projects[$project]['recommended'])) {
+        $projects[$project]['status'] = UPDATE_UNKNOWN;
+        $projects[$project]['reason'] = t('No available releases found');
+        continue;
+      }
+
+      // Check based upon install type and the site-wide threshold setting.
+      $error_level = variable_get('update_notification_threshold', 'all');
+
+      switch ($projects[$project]['install_type']) {
+        case 'official':
+          if ($projects[$project]['existing_version'] == $projects[$project]['recommended'] || $projects[$project]['existing_version'] == $projects[$project]['latest_version']) {
+            $projects[$project]['status'] = UPDATE_CURRENT;
+          }
+          else {
+            if (!empty($projects[$project]['security updates'])) {
+              $projects[$project]['status'] = UPDATE_NOT_SECURE;
+            }
+            else {
+              $projects[$project]['status'] = UPDATE_NOT_CURRENT;
+            }
+          }
+          break;
+        case 'dev':
+          if (!empty($projects[$project]['security updates'])) {
+            $projects[$project]['status'] = UPDATE_NOT_SECURE;
+            break;
+          }
+
+          $latest = $available[$project]['releases'][$projects[$project]['latest_dev']];
+          if (empty($projects[$project]['datestamp'])) {
+            $projects[$project]['status'] = UPDATE_NOT_CHECKED;
+            $projects[$project]['reason'] = t('Unknown release date');
+          }
+          elseif (($projects[$project]['datestamp'] + 100 > $latest['date'])) {
+            $projects[$project]['status'] = UPDATE_CURRENT;
+          }
+          else {
+            $projects[$project]['status'] = UPDATE_NOT_CURRENT;
+          }
+          break;
+
+        default:
+          $projects[$project]['status'] = UPDATE_UNKNOWN;
+          $projects[$project]['reason'] = t('Invalid info');
+      }
+    }
+    else {
+      $projects[$project]['status'] = UPDATE_UNKNOWN;
+      $projects[$project]['reason'] = t('No available releases found');
+    }
+  }
+  // Give other modules a chance to alter the status (for example, to allow a
+  // contrib module to provide fine-grained settings to ignore specific
+  // projects or releases).
+  drupal_alter('update_status', $projects);
+  return $projects;
+}
Index: modules/update/update.css
===================================================================
RCS file: modules/update/update.css
diff -N modules/update/update.css
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/update/update.css	6 Jul 2007 03:07:09 -0000
@@ -0,0 +1,97 @@
+/* $Id: update.css,v 1.6 2007/06/29 09:08:07 dww Exp $ */
+.update .project {
+  font-weight: bold;
+  font-size: 110%;
+  padding-left: .25em; /* LTR */
+  height: 22px;
+}
+
+.update .version-status {
+  float: right; /* LTR */
+  padding-right: 10px; /* LTR */
+  font-size: 110%;
+  height: 20px;
+}
+
+.update .version-status .icon {
+  padding-left: .5em; /* LTR */
+}
+
+.update .info {
+  margin: 0;
+  padding: 1em 1em .25em 1em;
+}
+
+.update tr td {
+  border-top: 1px solid #ccc;
+  border-bottom: 1px solid #ccc;
+}
+
+.update tr.error {
+  background: #fcc;
+}
+
+.update tr.error .version-recommended {
+  background: #fdd;
+}
+
+.update tr.ok {
+  background: #dfd;
+}
+
+.update tr.warning {
+  background: #ffd;
+}
+
+.update tr.warning .version-recommended {
+  background: #ffe;
+}
+
+.current-version, .new-version {
+  direction: ltr; /* Note: version numbers should always be LTR. */
+}
+
+table.update,
+.update table.version {
+  width: 100%;
+  margin-top: .5em;
+}
+
+.update table.version tbody {
+  border: none;
+}
+
+.update table.version tr,
+.update table.version td {
+  line-height: .9em;
+  padding: 0;
+  margin: 0;
+  border: none;
+}
+
+.update table.version .version-title {
+  padding-left: 1em; /* LTR */
+  width: 14em;
+}
+
+.update table.version .version-details {
+  padding-right: .5em; /* LTR */
+}
+
+.update table.version .version-links {
+  text-align: right; /* LTR */
+  padding-right: 1em; /* LTR */
+}
+
+.update table.version-security .version-title {
+  color: #970F00;
+}
+
+.update table.version-recommended-strong .version-title {
+  font-weight: bold;
+}
+
+.update .security-error {
+  font-weight: bold;
+  color: #970F00;
+}
Index: modules/update/update.fetch.inc
===================================================================
RCS file: modules/update/update.fetch.inc
diff -N modules/update/update.fetch.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/update/update.fetch.inc	6 Jul 2007 03:07:09 -0000
@@ -0,0 +1,212 @@
+<?php
+// $Id: update.fetch.inc,v 1.3 2007/06/29 08:01:04 dww Exp $
+
+/**
+ * @file
+ * Code required only when fetching information about available updates.
+ */
+
+/**
+ * Callback to manually check the update status without cron.
+ */
+function update_manual_status() {
+  _update_refresh();
+  drupal_goto('admin/logs/updates');
+}
+
+/**
+ * Fetch project info via XML from a central server.
+ */
+function _update_refresh() {
+  global $base_url;
+  include_once './modules/update/update.compare.inc';
+
+  $available = array();
+  $data = array();
+  $site_key = '';
+  $drupal_private_key = variable_get('drupal_private_key', '');
+  $site_key = md5($base_url . $drupal_private_key);
+  $projects = update_get_projects();
+
+  foreach ($projects as $key => $project) {
+    $url = _update_build_fetch_url($project, $site_key);
+    $xml = drupal_http_request($url);
+    $data[] = $xml->data;
+  }
+
+  if ($data) {
+    $parser = new update_xml_parser;
+    $available = $parser->parse($data);
+    $frequency = variable_get('update_check_frequency', 1);
+    cache_set('update_info', $available, 'cache', time() + (60 * 60 * 24 * $frequency));
+    variable_set('update_last_check', time());
+    watchdog('update', t('Fetched data on all available new releases and updates.'), WATCHDOG_NOTICE, l('view', 'admin/logs/updates'));
+  }
+  return $available;
+}
+
+/**
+ * Generates the URL to fetch information about project updates.
+ *
+ * This figures out the right URL to use, based on the project's .info file
+ * and the global defaults. Appends optional query arguments when the site is
+ * configured to report usage stats.
+ *
+ * @param $project
+ *   The array of project information from update_get_projects().
+ * @param $site_key
+ *   The anonymous site key hash (optional).
+ *
+ * @see update_refresh()
+ * @see update_get_projects()
+ */
+function _update_build_fetch_url($project, $site_key = '') {
+  if (!isset($project['info']['project status url'])) {
+    $project['info']['project status url'] = UPDATE_DEFAULT_URL;
+  }
+  $name = $project['name'];
+  $url = $project['info']['project status url'];
+  $url .= '/'. $name .'/'. DRUPAL_CORE_COMPATIBILITY;
+  if (!empty($site_key)) {
+    $url .= (strpos($url, '?') === TRUE) ? '&' : '?';
+    $url .= 'site_key=';
+    $url .= drupal_urlencode($site_key);
+    if (!empty($project['info']['version'])) {
+      $url .= '&version=';
+      $url .= drupal_urlencode($project['info']['version']);
+    }
+  }
+  return $url;
+}
+
+/**
+ * Perform any notifications that should be done once cron fetches new data.
+ *
+ * This method checks the status of the site using the new data and depending
+ * on the configuration of the site, notifys administrators via email if there
+ * are new releases or missing security updates.
+ *
+ * @see update_requirements()
+ */
+function _update_cron_notify() {
+  include_once './includes/install.inc';
+  $status = update_requirements('runtime');
+  $params = array();
+  foreach (array('core', 'contrib') as $report_type) {
+    $type = 'update_'. $report_type;
+    if (isset($status[$type]['severity']) 
+        && $status[$type]['severity'] == REQUIREMENT_ERROR) {
+      $params[$report_type] = $status[$type]['reason'];
+    }
+  }
+  if (!empty($params)) {
+    $notify_list = variable_get('update_notify_emails', '');
+    if (!empty($notify_list)) {
+      $default_language = language_default();
+      foreach ($notify_list as $target) {
+        if ($target_user = user_load(array('mail' => $target))) {
+          $target_language = user_preferred_language($target_user);
+        }
+        else {
+          $target_language = $default_language;
+        }
+        drupal_mail('update', 'status_notify', $target, $target_language, $params);
+      }
+    }
+  }
+}
+
+/**
+ * XML Parser object to read Drupal's release history info files.
+ * This uses PHP4's lame XML parsing, but it works.
+ */
+class update_xml_parser {
+  var $projects = array();
+  var $current_project;
+  var $current_release;
+  var $current_term;
+  var $current_tag;
+  var $current_object;
+
+  /**
+   * Parse an array of XML data files.
+   */
+  function parse($data) {
+    foreach ($data as $datum) {
+      $parser = xml_parser_create();
+      xml_set_object($parser, $this);
+      xml_set_element_handler($parser, 'start', 'end');
+      xml_set_character_data_handler($parser, "data");
+      xml_parse($parser, $datum);
+      xml_parser_free($parser);
+    }
+    return $this->projects;
+  }
+
+  function start($parser, $name, $attr) {
+    $this->current_tag = $name;
+    switch ($name) {
+      case 'PROJECT':
+        unset($this->current_object);
+        $this->current_project = array();
+        $this->current_object = &$this->current_project;
+        break;
+      case 'RELEASE':
+        unset($this->current_object);
+        $this->current_release = array();
+        $this->current_object = &$this->current_release;
+        break;
+      case 'TERM':
+        unset($this->current_object);
+        $this->current_term = array();
+        $this->current_object = &$this->current_term;
+        break;
+    }
+  }
+
+  function end($parser, $name) {
+    switch ($name) {
+      case 'PROJECT':
+        unset($this->current_object);
+        $this->projects[$this->current_project['short_name']] = $this->current_project;
+        $this->current_project = array();
+        break;
+      case 'RELEASE':
+        unset($this->current_object);
+        $this->current_project['releases'][$this->current_release['version']] = $this->current_release;
+        break;
+      case 'RELEASES':
+        $this->current_object = &$this->current_project;
+        break;
+      case 'TERM':
+        unset($this->current_object);
+        $term_name = $this->current_term['name'];
+        if (!isset($this->current_release['terms'])) {
+          $this->current_release['terms'] = array();
+        }
+        if (!isset($this->current_release['terms'][$term_name])) {
+          $this->current_release['terms'][$term_name] = array();
+        }
+        $this->current_release['terms'][$term_name][] = $this->current_term['value'];
+        break;
+      case 'TERMS':
+        $this->current_object = &$this->current_release;
+        break;
+      default:
+        $this->current_object[strtolower($this->current_tag)] = trim($this->current_object[strtolower($this->current_tag)]);
+        $this->current_tag = '';
+    }
+  }
+
+  function data($parser, $data) {
+    if ($this->current_tag && !in_array($this->current_tag, array('PROJECT', 'RELEASE', 'RELEASES', 'TERM', 'TERMS'))) {
+      $tag = strtolower($this->current_tag);
+      if (isset($this->current_object[$tag])) {
+        $this->current_object[$tag] .= $data;
+      }
+      else {
+        $this->current_object[$tag] = $data;
+      }
+    }
+  }
+}
Index: modules/update/update.info
===================================================================
RCS file: modules/update/update.info
diff -N modules/update/update.info
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/update/update.info	6 Jul 2007 03:07:09 -0000
@@ -0,0 +1,6 @@
+; $Id: update.info,v 1.3 2007/06/28 02:20:47 dww Exp $
+name = Update status
+description = Checks the status of available updates for Drupal and your installed modules and themes.
+version = VERSION
+package = Core - optional
+core = 6.x
Index: modules/update/update.module
===================================================================
RCS file: modules/update/update.module
diff -N modules/update/update.module
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/update/update.module	6 Jul 2007 03:07:09 -0000
@@ -0,0 +1,364 @@
+<?php
+// $Id: update.module,v 1.26 2007/06/29 08:01:04 dww Exp $
+
+/**
+ * @file
+ * The "Update status" module checks for available updates of Drupal core and
+ * any installed contributed modules and themes. It warns site administrators
+ * if newer releases are available via the system status report
+ * (admin/logs/status), the module and theme pages, and optionally via email.
+ */
+
+/**
+ * URL to check for updates, if a given project doesn't define its own.
+ */
+define('UPDATE_DEFAULT_URL', 'http://updates.drupal.org/release-history');
+
+// These are internally used constants for this code, do not modify.
+
+/**
+ * Project is up to date.
+ */
+define('UPDATE_CURRENT', 1);
+
+/**
+ * Project is missing security update(s).
+ */
+define('UPDATE_NOT_SECURE', 2);
+
+/**
+ * Project has a new release available, but it is not a security release.
+ */
+define('UPDATE_NOT_CURRENT', 3);
+
+/**
+ * Project's status cannot be checked.
+ */
+define('UPDATE_NOT_CHECKED', 4);
+
+/**
+ * No available update data was found for project.
+ */
+define('UPDATE_UNKNOWN', 5);
+
+/**
+ * Implementation of hook_help().
+ */
+function update_help($path, $arg) {
+  switch ($path) {
+    case 'admin/logs/updates':
+      return '<p>'. t('Here you can find information about available updates for your installed modules and themes. Note that each module or theme is part of a "project", which may or may not have the same name, and might include multiple modules or themes within it.') .'</p>';
+
+    case 'admin/build/themes':
+    case 'admin/build/modules':
+      include_once './includes/install.inc';
+      $status = update_requirements('runtime');
+      foreach (array('core', 'contrib') as $report_type) {
+        $type = 'update_'. $report_type;
+        if (isset($status[$type]['severity'])) {
+          if ($status[$type]['severity'] == REQUIREMENT_ERROR) {
+            drupal_set_message($status[$type]['description'], 'error');
+          }
+          elseif ($status[$type]['severity'] == REQUIREMENT_WARNING) {
+            drupal_set_message($status[$type]['description']);
+          }
+        }
+      }
+      return '<p>'. t('See the <a href="!available_updates">available updates</a> page for information on installed modules and themes with new versions released.', array('!available_updates' => url('admin/logs/updates'))) .'</p>';
+  }
+}
+
+/** 
+ * Implementation of hook_menu().
+ */
+function update_menu() {
+  $items = array();
+
+  $items['admin/logs/updates'] = array(
+    'title' => 'Available updates',
+    'description' => 'Get a status report about available updates for your installed modules and themes.',
+    'page callback' => 'update_status',
+    'access arguments' => array('administer site configuration'),
+    'file' => 'update.report.inc',
+    'weight' => 10,
+  );
+  $items['admin/logs/updates/list'] = array(
+    'title' => 'List',
+    'page callback' => 'update_status',
+    'access arguments' => array('administer site configuration'),
+    'file' => 'update.report.inc',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+  );
+  $items['admin/logs/updates/settings'] = array(
+    'title' => 'Settings',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('update_settings'),
+    'access arguments' => array('administer site configuration'),
+    'file' => 'update.settings.inc',
+    'type' => MENU_LOCAL_TASK,
+  );
+  $items['admin/logs/updates/check'] = array(
+    'title' => 'Manual update check',
+    'page callback' => 'update_manual_status',
+    'access arguments' => array('administer site configuration'),
+    'file' => 'update.fetch.inc',
+    'type' => MENU_CALLBACK,
+  );
+
+  return $items;
+}
+
+/**
+ * Implementation of the hook_theme() registry.
+ */
+function update_theme() {
+  return array(
+    'update_settings' => array(
+      'arguments' => array('form' => NULL),
+    ),
+    'update_report' => array(
+      'arguments' => array('data' => NULL),
+    ),
+    'update_version' => array(
+      'arguments' => array('version' => NULL, 'tag' => NULL, 'class' => NULL),
+    ),
+  );
+}
+
+/**
+ * Implementation of hook_requirements.
+ *
+ * @return
+ *   An array describing the status of the site regarding available updates.
+ *   If there is no update data, only one record will be returned, indicating
+ *   that the status of core can't be determined. If data is available, there
+ *   will be two records: one for core, and another for all of contrib
+ *   (assuming there are any contributed modules or themes enabled on the
+ *   site). In addition to the fields expected by hook_requirements ('value',
+ *   'severity', and optionally 'description'), this array will contain a
+ *   'reason' attribute, which is an integer constant to indicate why the
+ *   given status is being returned (UPDATE_NOT_SECURE, UPDATE_NOT_CURRENT, or
+ *   UPDATE_UNKNOWN). This is used for generating the appropriate e-mail
+ *   notification messages during update_cron(), and might be useful for other
+ *   modules that invoke update_requirements() to find out if the site is up
+ *   to date or not.
+ *
+ * @see _update_message_text()
+ * @see _update_cron_notify()
+ */
+function update_requirements($phase) {
+  if ($phase == 'runtime') {
+    $requirements['update_core']['title'] = t('Drupal core update status');
+    $notification_level = variable_get('update_notification_threshold', 'all');
+    if ($available = update_get_available(FALSE)) {
+      include_once './modules/update/update.compare.inc';
+      $data = update_calculate_project_data($available);
+      switch ($data['drupal']['status']) {
+        case UPDATE_NOT_CURRENT:
+          $requirements['update_core']['value'] = t('Out of date (version @version available)', array('@version' => $data['drupal']['recommended']));
+          $requirements['update_core']['severity'] = $notification_level == 'all' ? REQUIREMENT_ERROR : REQUIREMENT_WARNING;
+          $requirements['update_core']['reason'] = UPDATE_NOT_CURRENT;
+          $requirements['update_core']['description'] = _update_message_text('core', UPDATE_NOT_CURRENT, TRUE);
+          break;
+
+        case UPDATE_NOT_SECURE:
+          $requirements['update_core']['value'] = t('Not secure! (version @version available)', array('@version' => $data['drupal']['recommended']));
+          $requirements['update_core']['severity'] = REQUIREMENT_ERROR;
+          $requirements['update_core']['reason'] = UPDATE_NOT_SECURE;
+          $requirements['update_core']['description'] = _update_message_text('core', UDPDATE_NOT_SECURE, TRUE);
+          break;
+
+        default:
+          $requirements['update_core']['value'] = t('Up to date');
+          break;
+      }
+      // We don't want to check drupal a second time.
+      unset($data['drupal']);
+      $not_current = FALSE;
+      if (!empty($data)) {
+        $requirements['update_contrib']['title'] = t('Module and theme update status');
+        // Default to being current until we see otherwise.
+        $requirements['update_contrib']['value'] = t('Up to date');
+        foreach (array_keys($data) as $project) {
+          if (isset($available[$project])) {
+            if ($data[$project]['status'] == UPDATE_NOT_SECURE) {
+              $requirements['update_contrib']['value'] = t('Not secure!');
+              $requirements['update_contrib']['severity'] = REQUIREMENT_ERROR;
+              $requirements['update_contrib']['reason'] = UPDATE_NOT_SECURE;
+              $requirements['update_contrib']['description'] = _update_message_text('contrib', UPDATE_NOT_SECURE, TRUE);
+              break;
+            }
+            elseif ($data[$project]['status'] == UPDATE_NOT_CURRENT) {
+              $not_current = TRUE;
+            }
+          }
+        }
+        if (!isset($requirements['update_contrib']['severity']) && $not_current) {
+          $requirements['update_contrib']['severity'] = $notification_level == 'all' ? REQUIREMENT_ERROR : REQUIREMENT_WARNING;
+          $requirements['update_contrib']['value'] = t('Out of date');
+          $requirements['update_contrib']['reason'] = UPDATE_NOT_CURRENT;
+          $requirements['update_contrib']['description'] = _update_message_text('contrib', UPDATE_NOT_CURRENT, TRUE);
+        }
+      }
+    }
+    else {
+      $requirements['update_core']['value'] = t('No update data available');
+      $requirements['update_core']['severity'] = REQUIREMENT_WARNING;
+      $requirements['update_core']['reason'] = UPDATE_UNKNOWN;
+      $requirements['update_core']['description'] = _update_no_data();
+    }
+    return $requirements;
+  }
+}
+
+/**
+ * Implementation of hook_cron().
+ */
+function update_cron() {
+  $frequency = variable_get('update_check_frequency', 1);
+  $interval = 60 * 60 * 24 * $frequency;
+  if (time() - variable_get('update_last_check', 0) > $interval) {
+    update_refresh();
+    _update_cron_notify();
+  }
+}
+
+/**
+ * Implementation of hook_form_alter().
+ *
+ * Adds a submit handler to the system modules and themes forms, so that if a
+ * site admin saves either form, we invalidate the cache of available updates.
+ *
+ * @see update_invalidate_cache()
+ */
+function update_form_alter(&$form, $form_state, $form_id) {
+  if ($form_id == 'system_modules' || $form_id == 'system_themes' ) {
+    $form['#submit'][] = 'update_invalidate_cache';
+  }
+}
+
+/**
+ * Prints a warning message when there is no data about available updates.
+ */
+function _update_no_data() {
+  $destination = drupal_get_destination();
+  return t('No information is available about potential new releases for currently installed modules and themes. To check for updates, you may need to <a href="!run_cron">run cron</a> or you can <a href="!check_manually">check manually</a>. Please note that checking for available updates can take a long time, so please be patient.', array(
+    '!run_cron' => url('admin/logs/status/run-cron', array('query' => $destination)),
+    '!check_manually' => url('admin/logs/updates/check', array('query' => $destination)),
+  ));
+}
+
+/**
+ * Internal helper to try to get the update information from the cache
+ * if possible, and to refresh the cache when necessary.
+ *
+ * @param $refresh
+ *   Boolean to indicate if this method should refresh the cache automatically
+ *   if there's no data.
+ */
+function update_get_available($refresh = FALSE) {
+  $available = array();
+  if (($cache = cache_get('update_info', 'cache'))
+       && $cache->expire > time()) {
+    $available = $cache->data;
+  }
+  elseif ($refresh) {
+    $available = update_refresh();
+  }
+  return $available;
+}
+
+/**
+ * Invalidates any cached data relating to update status.
+ */
+function update_invalidate_cache() {
+  cache_clear_all('update_info', 'cache');
+}
+
+/**
+ * Wrapper to load the include file and then refresh the release data.
+ */
+function update_refresh() {
+  include_once './modules/update/update.fetch.inc';
+  _update_refresh();
+}
+
+/**
+ * Implementation of hook_mail().
+ *
+ * Constructs the email notification message when the site is out of date.
+ *
+ * @param $key
+ *   Unique key to indicate what message to build, always 'status_notify'.
+ * @param $message
+ *   Reference to the message array being built.
+ * @param $params
+ *   Array of parameters to indicate what kind of text to include in the
+ *   message body. This is a keyed array of message type ('core' or 'contrib')
+ *   as the keys, and the status reason constant (UPDATE_NOT_SECURE, etc) for
+ *   the values.
+ *
+ * @see drupal_mail();
+ * @see _update_cron_notify();
+ * @see _update_message_text();
+ */
+function update_mail($key, &$message, $params) {
+  $language = $message['language'];
+  $langcode = $language->language;
+  $message['subject'] .= t('New release(s) available for !site_name', array('!site_name' => variable_get('site_name', 'Drupal')), $langcode);
+  foreach ($params as $msg_type => $msg_reason) {
+    $message['body'][] = _update_message_text($msg_type, $msg_reason, FALSE, $language);
+  }
+  $message['body'][] = t('See the available updates page for more information:', array(), $langcode) ."\n". url('admin/logs/updates', array('absolute' => TRUE, 'language' => $language));
+}
+
+/**
+ * Helper function to return the appropriate message text when the site is out
+ * of date or missing a security update.
+ *
+ * These error messages are shared by both update_requirements() for the
+ * site-wide status report at admin/logs/status and in the body of the
+ * notification emails generated by update_cron().
+ *
+ * @param $msg_type
+ *   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_NOT_CURRENT or UPDATE_NOT_SECURE.
+ * @param $report_link
+ *   Boolean that controls if a link to the updates report should be added.
+ * @param $language
+ *   An optional language object to use.
+ * @return
+ *   The properly translated error message for the given key.
+ */
+function _update_message_text($msg_type, $msg_reason, $report_link = FALSE, $language = NULL) {
+  $langcode = isset($language) ? $language->language : NULL;
+  $text = '';
+  switch ($msg_reason) {
+    case UPDATE_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.', array(), $langcode);
+      }
+      else {
+        $text = t('There are updates available for one or more of your modules or themes. To ensure the proper functioning of your site, you should update as soon as possible.', array(), $langcode);
+      }
+      break;
+
+    case UPDATE_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.', array(), $langcode);
+      }
+      else {
+        $text = t('There are security updates available for one or more of your modules or themes. To ensure the security of your server, you should update immediately.', array(), $langcode);
+      }
+      break;
+  }
+
+  if ($report_link) {
+    $text .= ' '. t('See the <a href="!available_updates">available updates</a> page for more information.', array('!available_updates' => url('admin/logs/updates', array('language' => $language))), $langcode);
+  }
+
+  return $text;
+}
Index: modules/update/update.report.inc
===================================================================
RCS file: modules/update/update.report.inc
diff -N modules/update/update.report.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/update/update.report.inc	6 Jul 2007 03:07:10 -0000
@@ -0,0 +1,223 @@
+<?php
+// $Id: update.report.inc,v 1.2 2007/06/29 08:01:04 dww Exp $
+
+/**
+ * @file
+ * Code required only when rendering the available updates report.
+ */
+
+/**
+ * Menu callback. Generate a page about the update status of projects.
+ */
+function update_status() {
+  if ($available = update_get_available(TRUE)) {
+    include_once './modules/update/update.compare.inc';
+    $data = update_calculate_project_data($available);
+    return theme('update_report', $data);
+  }
+  else {
+    return theme('update_report', _update_no_data());
+  }
+}
+
+/**
+ * Theme project status report.
+ */
+function theme_update_report($data) {
+  $last = variable_get('update_last_check', 0);
+  $output = '<p>'. t('Last checked: ') . ($last  ? format_date($last) : t('Never'));
+  $output .= ' '. l(t('Check manually'), 'admin/logs/updates/check') .'</p>';
+
+  if (!is_array($data)) {
+    $output .= '<p>'. $data .'</p>';
+    return $output;
+  }
+
+  $header = array();
+  $rows = array();
+
+  $notification_level = variable_get('update_notification_threshold', 'all');
+
+  foreach ($data as $project) {
+    switch ($project['status']) {
+      case UPDATE_CURRENT:
+        $class = 'ok';
+        $icon = theme('image', 'misc/watchdog-ok.png');
+        break;
+      case UPDATE_NOT_SECURE:
+      case UPDATE_NOT_CURRENT:
+        if ($notification_level == 'all'
+            || $project['status'] == UPDATE_NOT_SECURE) {
+          $class = 'error';
+          $icon = theme('image', 'misc/watchdog-error.png');
+          break;
+        }
+        // Otherwise, deliberate no break and use the warning class/icon.
+      default:
+        $class = 'warning';
+        $icon = theme('image', 'misc/watchdog-warning.png');
+        break;
+    }
+
+    $row = '<div class="version-status">';
+    switch ($project['status']) {
+      case UPDATE_CURRENT:
+        $row .= t('Up to date');
+        break;
+      case UPDATE_NOT_SECURE:
+        $row .= '<span class="security-error">';
+        $row .= t('Security update required!');
+        $row .= '</span>';
+        break;
+      case UPDATE_NOT_CURRENT:
+        if ($notification_level == 'all') {
+          $row .= t('Update available');
+          break;
+        }
+        // Otherwise, deliberate no break and just ignore this project.
+        $project['reason'] = t('Not a security update');
+      default:
+        $row .= t('Ignored');
+        $row .= ' ('. $project['reason'] .')';
+        break;
+    }
+    $row .= '<span class="icon">'. $icon .'</span>';
+    $row .= "</div>\n";
+
+    $row .= '<div class="project">';
+    if (isset($project['title'])) {
+      if (isset($project['link'])) {
+        $row .= l($project['title'], $project['link']);
+      }
+      else {
+        $row .= check_plain($project['title']);
+      }
+    }
+    else {
+      $row .= check_plain($project['name']);
+    }
+    $row .= ' '. check_plain($project['existing_version']);
+    if ($project['install_type'] == 'dev' && !empty($project['datestamp'])) {
+      $row .= ' ('. format_date($project['datestamp'], 'custom', 'Y-M-d') .') ';
+    }
+    $row .= "</div>\n";
+
+    $row .= "<div class=\"versions\">\n";
+
+    if (isset($project['recommended'])) {
+      if ($project['status'] != UPDATE_CURRENT || $project['existing_version'] != $project['recommended']) {
+
+        // First, figure out what to recommend.
+        // If there's only 1 security update and it has the same version we're
+        // recommending, give it the same CSS class as if it was recommended,
+        // but don't print out a separate "Recommended" line for this project.
+        if (!empty($project['security updates']) && count($project['security updates']) == 1 && $project['security updates'][0]['version'] == $project['recommended']) {
+          $security_class = ' version-recommended version-recommended-strong';
+        }
+        else {
+          $security_class = '';
+          $version_class = 'version-recommended';
+          // Apply an extra class if we're displaying both a recommended
+          // version and anything else for an extra visual hint.
+          if ($project['recommended'] != $project['latest_version']
+              || !empty($project['also'])
+              || ($project['install_type'] == 'dev'
+                 && $project['latest_version'] != $project['dev_version']
+                 && $project['recommended'] != $project['dev_version'])
+              || (isset($project['security updates'][0])
+                 && $project['recommended'] != $project['security updates'][0])
+              ) { 
+            $version_class .= ' version-recommended-strong';
+          }
+          $row .= theme('update_version', $project['releases'][$project['recommended']], t('Recommended version:'), $version_class);
+        }
+
+        // Now, print any security updates.
+        if (!empty($project['security updates'])) {
+          foreach ($project['security updates'] as $security_update) {
+            $row .= theme('update_version', $security_update, t('Security update:'), 'version-security'. $security_class);
+          }
+        }
+      }
+
+      if ($project['recommended'] != $project['latest_version']) {
+        $row .= theme('update_version', $project['releases'][$project['latest_version']], t('Latest version:'), 'version-latest');
+      }
+      if ($project['install_type'] == 'dev'
+          && $project['status'] != UPDATE_CURRENT
+          && $project['recommended'] != $project['dev_version']) {
+        $row .= theme('update_version', $project['releases'][$project['dev_version']], t('Development version:'), 'version-latest');
+      }
+    }
+
+    if (isset($project['also'])) {
+      foreach ($project['also'] as $also) {
+        $row .= theme('update_version', $project['releases'][$also], t('Also available:'), 'version-also-available');
+      }        
+    }
+
+    $row .= "</div>\n"; // versions div.
+
+    $row .= "<div class=\"info\">\n";
+    if (!empty($project['notes'])) {
+      $row .= '<div class="notes">';
+      $row .= t('Administrator note: %notes', array('%notes' => $project['notes']));
+      $row .= "</div>\n";
+    }
+
+    $row .= '<div class="includes">';
+    sort($project['includes']);
+    $row .= t('Includes: %includes', array('%includes' => implode(', ', $project['includes'])));
+    $row .= "</div>\n";
+
+    $row .= "</div>\n"; // info div.
+
+    if (!isset($rows[$project['project_type']])) {
+      $rows[$project['project_type']] = array();
+    }
+    $rows[$project['project_type']][] = array(
+      'class' => $class,
+      'data' => array($row),
+    );
+  }
+
+  $project_types = array(
+    'core' => t('Drupal core'),
+    'module' => t('Modules'),
+    'theme' => t('Themes'),
+  );
+  foreach ($project_types as $type_name => $type_label) {
+    if (!empty($rows[$type_name])) {
+      $output .= "\n<h3>". $type_label ."</h3>\n";
+      $output .= theme('table', $header, $rows[$type_name], array('class' => 'update'));
+    }
+  }
+  drupal_add_css(drupal_get_path('module', 'update') .'/update.css');
+  return $output;
+}
+
+function theme_update_version($version, $tag, $class) {
+  $output = '';
+  $output .= '<table class="version '. $class .'">';
+  $output .= '<tr>';
+  $output .= '<td class="version-title">'. $tag ."</td>\n";
+  $output .= '<td class="version-details">';
+  $output .= l($version['version'], $version['release_link']);
+  $output .= ' ('. format_date($version['date'], 'custom', 'Y-M-d') .') ';
+  $output .= "</td>\n";
+  $output .= '<td class="version-links">';
+  $links = array();
+  $links['update-download'] = array(
+    'title' => t('Download'),
+    'href' => $version['download_link'],
+  );
+  $links['update-release-notes'] = array(
+    'title' => t('Release notes'),
+    'href' => $version['release_link'],
+  );
+  $output .= theme('links', $links);
+  $output .= '</td>';
+  $output .= '</tr>';
+  $output .= "</table>\n";
+  return $output;
+}
Index: modules/update/update.settings.inc
===================================================================
RCS file: modules/update/update.settings.inc
diff -N modules/update/update.settings.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/update/update.settings.inc	6 Jul 2007 03:07:10 -0000
@@ -0,0 +1,108 @@
+<?php
+// $Id: update.settings.inc,v 1.6 2007/06/29 09:07:21 dww Exp $
+
+/**
+ * @file
+ * Code required only for the update status settings form.
+ */
+
+/**
+ * Form builder for the update settings tab.
+ */
+function update_settings() {
+  $form = array();
+
+  $notify_emails = variable_get('update_notify_emails', array());
+  $form['update_notify_emails'] = array(
+    '#type' => 'textarea',
+    '#title' => t('E-mail addresses to notify when updates are available'),
+    '#rows' => 4,
+    '#default_value' => implode("\n", $notify_emails),
+    '#description' => t('Whenever your site checks for available updates and finds new releases, it can notify a list of users via e-email. Put each address on a separate line. If blank, no e-mails will be sent.'),
+  );
+    
+  $form['update_check_frequency'] = array(
+    '#type' => 'radios',
+    '#title' => t('Check for updates'),
+    '#default_value' => variable_get('update_check_frequency', 1),
+    '#options' => array(
+      '1' => t('Daily'),
+      '7' => t('Weekly'),
+    ),
+    '#description' => t('Select how frequently you want to automatically check for new releases of your currently installed modules and themes.'),
+  );
+
+  $form['update_notification_threshold'] = array(
+    '#type' => 'radios',
+    '#title' => t('Notification threshold'),
+    '#default_value' => variable_get('update_notification_threshold', 'all'),
+    '#options' => array(
+      'all' => t('All newer versions'),
+      'security' => t('Only security updates'),
+    ),
+    '#description' => t('If there are updates available of Drupal core or any of your installed modules and themes, your site will print an error message on the <a href="!status_report">status report</a>, the <a href="!modules_page">modules page</a>, and the <a href="!themes_page">themes page</a>. You can choose to only see these error messages if a security update is available, or to be notified about any newer versions.', array('!status_report' => url('admin/logs/status'), '!modules_page' => url('admin/build/modules'), '!themes_page' => url('admin/build/themes')))
+  );
+
+  $form = system_settings_form($form);
+  // Custom valiation callback for the email notification setting.
+  $form['#validate'][] = 'update_settings_validate';
+  // We need to call our own submit callback first, not the one from
+  // system_settings_form(), so that we can process and save the emails.
+  unset($form['#submit']);
+
+  return $form;
+}
+
+/**
+ * Validation callback for the settings form.
+ *
+ * Validates the email addresses and ensures the field is formatted correctly.
+ */
+function update_settings_validate($form, &$form_state) {
+  if (!empty($form_state['values']['update_notify_emails'])) {
+    $valid = array();
+    $invalid = array();
+    foreach (explode("\n", trim($form_state['values']['update_notify_emails'])) as $email) {
+      $email = trim($email);
+      if (!empty($email)) {
+        if (valid_email_address($email)) {
+          $valid[] = $email;
+        }
+        else {
+          $invalid[] = $email;
+        }
+      }
+    }
+    if (empty($invalid)) {
+      $form_state['notify_emails'] = $valid;
+    }
+    elseif (count($invalid) == 1) {
+      form_set_error('update_notify_emails', t('%email is not a valid e-mail address.', array('%email' => reset($invalid))));
+    }
+    else {
+      form_set_error('update_notify_emails', t('%emails are not valid e-mail addresses.', array('%emails' => implode(', ', $invalid))));
+    }
+  }
+}
+
+/**
+ * Submit handler for the settings tab.
+ */
+function update_settings_submit($form, $form_state) {
+  $op = $form_state['values']['op'];
+  
+  if ($op == t('Reset to defaults')) {
+    unset($form_state['notify_emails']);
+  }
+  else {
+    if (empty($form_state['notify_emails'])) {
+      variable_del('update_notify_emails');
+    }
+    else {
+      variable_set('update_notify_emails', $form_state['notify_emails']);
+    }
+    unset($form_state['notify_emails']);
+    unset($form_state['values']['update_notify_emails']);
+  }
+  system_settings_form_submit($form, $form_state);
+}
