diff options
author | Dries Buytaert <dries@buytaert.net> | 2007-07-11 15:15:40 +0000 |
---|---|---|
committer | Dries Buytaert <dries@buytaert.net> | 2007-07-11 15:15:40 +0000 |
commit | 6ec2ff7e1563f7da82f9411878e2c7482b671983 (patch) | |
tree | 484b05be33ba384db140fa8f3f3c7386fa2c63f9 /modules/update | |
parent | 70f9297c100eaa1736b8e136a2e32c9d87b56de4 (diff) | |
download | brdo-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.css | 27 | ||||
-rw-r--r-- | modules/update/update.compare.inc | 398 | ||||
-rw-r--r-- | modules/update/update.css | 97 | ||||
-rw-r--r-- | modules/update/update.fetch.inc | 223 | ||||
-rw-r--r-- | modules/update/update.info | 6 | ||||
-rw-r--r-- | modules/update/update.install | 30 | ||||
-rw-r--r-- | modules/update/update.module | 387 | ||||
-rw-r--r-- | modules/update/update.report.inc | 224 | ||||
-rw-r--r-- | modules/update/update.schema | 7 | ||||
-rw-r--r-- | modules/update/update.settings.inc | 108 |
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); +} |