summaryrefslogtreecommitdiff
path: root/modules/update
diff options
context:
space:
mode:
authorDries Buytaert <dries@buytaert.net>2007-07-11 15:15:40 +0000
committerDries Buytaert <dries@buytaert.net>2007-07-11 15:15:40 +0000
commit6ec2ff7e1563f7da82f9411878e2c7482b671983 (patch)
tree484b05be33ba384db140fa8f3f3c7386fa2c63f9 /modules/update
parent70f9297c100eaa1736b8e136a2e32c9d87b56de4 (diff)
downloadbrdo-6ec2ff7e1563f7da82f9411878e2c7482b671983.tar.gz
brdo-6ec2ff7e1563f7da82f9411878e2c7482b671983.tar.bz2
- Patch #94154 by dww, Earl et al: update notifications for Drupal!
Woot, woot! :)
Diffstat (limited to 'modules/update')
-rw-r--r--modules/update/update-rtl.css27
-rw-r--r--modules/update/update.compare.inc398
-rw-r--r--modules/update/update.css97
-rw-r--r--modules/update/update.fetch.inc223
-rw-r--r--modules/update/update.info6
-rw-r--r--modules/update/update.install30
-rw-r--r--modules/update/update.module387
-rw-r--r--modules/update/update.report.inc224
-rw-r--r--modules/update/update.schema7
-rw-r--r--modules/update/update.settings.inc108
10 files changed, 1507 insertions, 0 deletions
diff --git a/modules/update/update-rtl.css b/modules/update/update-rtl.css
new file mode 100644
index 000000000..5a91860a9
--- /dev/null
+++ b/modules/update/update-rtl.css
@@ -0,0 +1,27 @@
+/* $Id$ */
+
+.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;
+}
diff --git a/modules/update/update.compare.inc b/modules/update/update.compare.inc
new file mode 100644
index 000000000..4cbf34081
--- /dev/null
+++ b/modules/update/update.compare.inc
@@ -0,0 +1,398 @@
+<?php
+// $Id$
+
+/**
+ * @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;
+ }
+ }
+ 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;
+}
diff --git a/modules/update/update.css b/modules/update/update.css
new file mode 100644
index 000000000..58f3867b9
--- /dev/null
+++ b/modules/update/update.css
@@ -0,0 +1,97 @@
+/* $Id$ */
+.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;
+}
diff --git a/modules/update/update.fetch.inc b/modules/update/update.fetch.inc
new file mode 100644
index 000000000..c6b108c0b
--- /dev/null
+++ b/modules/update/update.fetch.inc
@@ -0,0 +1,223 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Code required only when fetching information about available updates.
+ */
+
+/**
+ * Callback to manually check the update status without cron.
+ */
+function update_manual_status() {
+ if (_update_refresh()) {
+ drupal_set_message(t('Fetched information about all available new releases and updates.'));
+ }
+ else {
+ drupal_set_message(t('Unable to fetch any information on available new releases and updates.'), 'error');
+ }
+ 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);
+ if (isset($xml->data)) {
+ $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_update', time() + (60 * 60 * 24 * $frequency));
+ variable_set('update_last_check', time());
+ watchdog('update', 'Fetched information on all available new releases and updates.', array(), WATCHDOG_NOTICE, l('view', 'admin/logs/updates'));
+ }
+ else {
+ watchdog('update', 'Unable to fetch any information on available new releases and updates.', array(), WATCHDOG_ERROR, 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 = '') {
+ $default_url = variable_get('update_fetch_url', UPDATE_DEFAULT_URL);
+ if (!isset($project['info']['project status url'])) {
+ $project['info']['project status url'] = $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;
+ }
+ }
+ }
+}
diff --git a/modules/update/update.info b/modules/update/update.info
new file mode 100644
index 000000000..9c6172a4f
--- /dev/null
+++ b/modules/update/update.info
@@ -0,0 +1,6 @@
+; $Id$
+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
diff --git a/modules/update/update.install b/modules/update/update.install
new file mode 100644
index 000000000..8c974dbe2
--- /dev/null
+++ b/modules/update/update.install
@@ -0,0 +1,30 @@
+<?php
+// $Id$
+
+/**
+ * Implementation of hook_install().
+ */
+function update_install() {
+ // Create cache table.
+ drupal_install_schema('update');
+}
+
+/**
+ * Implementation of hook_uninstall().
+ */
+function update_uninstall() {
+ // Remove cache table.
+ drupal_uninstall_schema('update');
+ // Clear any variables that might be in use
+ $variables = array(
+ 'update_check_frequency',
+ 'update_fetch_url',
+ 'update_last_check',
+ 'update_notification_threshold',
+ 'update_notify_emails',
+ );
+ foreach ($variables as $variable) {
+ variable_del($variable);
+ }
+ menu_rebuild();
+}
diff --git a/modules/update/update.module b/modules/update/update.module
new file mode 100644
index 000000000..8a6554840
--- /dev/null
+++ b/modules/update/update.module
@@ -0,0 +1,387 @@
+<?php
+// $Id$
+
+/**
+ * @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>';
+
+ case 'admin/logs/updates/settings':
+ case 'admin/logs/status':
+ // These two pages don't need additional nagging.
+ break;
+
+ default:
+ // Otherwise, if we're on *any* admin page and there's a security
+ // update missing, print an error message about it.
+ if (arg(0) == 'admin' && strpos($path, '#') === FALSE
+ && user_access('administer site configuration')) {
+ include_once './includes/install.inc';
+ $status = update_requirements('runtime');
+ foreach (array('core', 'contrib') as $report_type) {
+ $type = 'update_'. $report_type;
+ if (isset($status[$type])
+ && isset($status[$type]['reason'])
+ && $status[$type]['reason'] === UPDATE_NOT_SECURE) {
+ drupal_set_message($status[$type]['description'], 'error');
+ }
+ }
+ }
+
+ }
+}
+
+/**
+ * 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_update'))
+ && $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_update');
+}
+
+/**
+ * Wrapper to load the include file and then refresh the release data.
+ */
+function update_refresh() {
+ include_once './modules/update/update.fetch.inc';
+ return _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;
+}
diff --git a/modules/update/update.report.inc b/modules/update/update.report.inc
new file mode 100644
index 000000000..5f68e09b7
--- /dev/null
+++ b/modules/update/update.report.inc
@@ -0,0 +1,224 @@
+<?php
+// $Id$
+
+/**
+ * @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 = '<div class="checked">'. t('Last checked: ') . ($last ? format_interval(time() - $last) .' '. t('ago') : t('Never'));
+ $output .= ' <span class="check-manually">'. l(t('Check manually'), 'admin/logs/updates/check') .'</span>';
+ $output .= "</div>\n";
+
+ 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:
+ $row .= t('Update available');
+ break;
+ default:
+ $row .= check_plain($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['extra'])) {
+ $row .= '<div class="extra">' ."\n";
+ foreach ($project['extra'] as $key => $value) {
+ $row .= '<div class="'. $value['class'] .'">';
+ $row .= check_plain($value['label']) .': ';
+ $row .= theme('placeholder', $value['data']);
+ $row .= "</div>\n";
+ }
+ $row .= "</div>\n"; // extra div.
+ }
+
+ $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;
+}
diff --git a/modules/update/update.schema b/modules/update/update.schema
new file mode 100644
index 000000000..20663197b
--- /dev/null
+++ b/modules/update/update.schema
@@ -0,0 +1,7 @@
+<?php
+// $Id$
+
+function update_schema() {
+ $schema['cache_update'] = drupal_get_schema_unprocessed('system', 'cache');
+ return $schema;
+}
diff --git a/modules/update/update.settings.inc b/modules/update/update.settings.inc
new file mode 100644
index 000000000..cdb07efd5
--- /dev/null
+++ b/modules/update/update.settings.inc
@@ -0,0 +1,108 @@
+<?php
+// $Id$
+
+/**
+ * @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);
+}