Index: profiles/default/default.profile =================================================================== --- profiles/default/default.profile (revision 10) +++ profiles/default/default.profile (working copy) @@ -8,7 +8,7 @@ * An array of modules to be enabled. */ function default_profile_modules() { - return array('color', 'comment', 'help', 'taxonomy', 'dblog'); + return array('color', 'comment', 'help', 'taxonomy', 'dblog', 'update'); } /** Index: modules/update/update.compare.inc =================================================================== --- modules/update/update.compare.inc (revision 0) +++ modules/update/update.compare.inc (revision 0) @@ -0,0 +1,396 @@ +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($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($file) { + $project = ''; + if (isset($file->info['project'])) { + $project = $file->info['project']; + } + elseif (isset($file->info['package']) + && (strpos($file->info['package'], 'Core -') !== FALSE)) { + $project = '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 = '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 = $last; + } + else { + continue; + } + } + return $project; +} + +/** + * 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; + } + + // Initialize variables needed to find the recommended version. + $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('No filedate available'); + } + 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'); + } + } + return $projects; +} Index: modules/update/update.fetch.inc =================================================================== --- modules/update/update.fetch.inc (revision 0) +++ modules/update/update.fetch.inc (revision 0) @@ -0,0 +1,209 @@ + $project) { + $url = _update_build_fetch_url($project, $site_key); + $xml = drupal_http_request($url); + $data[] = $xml->data; + } + + if ($data) { + $parser = new update_xml_parser; + $available = $parser->parse($data); + $frequency = variable_get('update_check_frequency', 1); + cache_set('update_info', $available, 'cache', time() + (60 * 60 * 24 * $frequency)); + variable_set('update_last_check', time()); + watchdog('update', t('Fetched data on all available new releases and updates.'), WATCHDOG_NOTICE, l('view', 'admin/logs/updates')); + } + return $available; +} + +/** + * Generates the URL to fetch information about project updates. + * + * This figures out the right URL to use, based on the project's .info file + * and the global defaults. Appends optional query arguments when the site is + * configured to report usage stats. + * + * @param $project + * The array of project information from update_get_projects(). + * @param $site_key + * The anonymous site key hash (optional). + * + * @see update_refresh() + * @see update_get_projects() + */ +function _update_build_fetch_url($project, $site_key = '') { + if (!isset($project['info']['project status url'])) { + $project['info']['project status url'] = UPDATE_DEFAULT_URL; + } + $name = $project['name']; + $url = $project['info']['project status url']; + $url .= '/'. $name .'/'. DRUPAL_CORE_COMPATIBILITY; + if (!empty($site_key)) { + $url .= (strpos($url, '?') === TRUE) ? '&' : '?'; + $url .= 'site_key='; + $url .= drupal_urlencode($site_key); + if (!empty($project['info']['version'])) { + $url .= '&version='; + $url .= drupal_urlencode($project['info']['version']); + } + } + return $url; +} + +/** + * Perform any notifications that should be done once cron fetches new data. + * + * This method checks the status of the site using the new data and depending + * on the configuration of the site, notifys administrators via email if there + * are new releases or missing security updates. + * + * @see update_requirements() + */ +function _update_cron_notify() { + $status = update_requirements('runtime', $plaintext = TRUE); + $body = ''; + foreach (array('core', 'contrib') as $report_type) { + $type = 'update_'. $report_type; + if (isset($status[$type]['severity']) + && $status[$type]['severity'] == REQUIREMENT_ERROR) { + $body .= empty($body) ? '' : "\n\n"; + $body .= wordwrap($status[$type]['description'], 70); + } + } + if (!empty($body)) { + $notify_list = variable_get('update_notify_emails', ''); + if (!empty($notify_list)) { + $body .= "\n\n"; + $body .= t('See the available updates page for more information:') ."\n"; + $body .= url('admin/logs/updates', NULL, NULL, TRUE) ."\n\n"; + $subject = t('New release(s) available for !site_name', array('!site_name' => variable_get('site_name', 'Drupal'))); + foreach ($notify_list as $target) { + drupal_mail('update-status', $target, $subject, $body); + } + } + } +} + +/** + * XML Parser object to read Drupal's release history info files. + * This uses PHP4's lame XML parsing, but it works. + */ +class update_xml_parser { + var $projects = array(); + var $current_project; + var $current_release; + var $current_term; + var $current_tag; + var $current_object; + + /** + * Parse an array of XML data files. + */ + function parse($data) { + foreach ($data as $datum) { + $parser = xml_parser_create(); + xml_set_object($parser, $this); + xml_set_element_handler($parser, 'start', 'end'); + xml_set_character_data_handler($parser, "data"); + xml_parse($parser, $datum); + xml_parser_free($parser); + } + return $this->projects; + } + + function start($parser, $name, $attr) { + $this->current_tag = $name; + switch ($name) { + case 'PROJECT': + unset($this->current_object); + $this->current_project = array(); + $this->current_object = &$this->current_project; + break; + case 'RELEASE': + unset($this->current_object); + $this->current_release = array(); + $this->current_object = &$this->current_release; + break; + case 'TERM': + unset($this->current_object); + $this->current_term = array(); + $this->current_object = &$this->current_term; + break; + } + } + + function end($parser, $name) { + switch ($name) { + case 'PROJECT': + unset($this->current_object); + $this->projects[$this->current_project['short_name']] = $this->current_project; + $this->current_project = array(); + break; + case 'RELEASE': + unset($this->current_object); + $this->current_project['releases'][$this->current_release['version']] = $this->current_release; + break; + case 'RELEASES': + $this->current_object = &$this->current_project; + break; + case 'TERM': + unset($this->current_object); + $term_name = $this->current_term['name']; + if (!isset($this->current_release['terms'])) { + $this->current_release['terms'] = array(); + } + if (!isset($this->current_release['terms'][$term_name])) { + $this->current_release['terms'][$term_name] = array(); + } + $this->current_release['terms'][$term_name][] = $this->current_term['value']; + break; + case 'TERMS': + $this->current_object = &$this->current_release; + break; + default: + $this->current_object[strtolower($this->current_tag)] = trim($this->current_object[strtolower($this->current_tag)]); + $this->current_tag = ''; + } + } + + function data($parser, $data) { + if ($this->current_tag && !in_array($this->current_tag, array('PROJECT', 'RELEASE', 'RELEASES', 'TERM', 'TERMS'))) { + $tag = strtolower($this->current_tag); + if (isset($this->current_object[$tag])) { + $this->current_object[$tag] .= $data; + } + else { + $this->current_object[$tag] = $data; + } + } + } +} Index: modules/update/update.report.inc =================================================================== --- modules/update/update.report.inc (revision 0) +++ modules/update/update.report.inc (revision 0) @@ -0,0 +1,223 @@ +'. t('Last checked: ') . ($last ? format_date($last) : t('Never')); + $output .= ' '. l(t('Check manually'), 'admin/logs/updates/force-check') .'

'; + + if (!is_array($data)) { + $output .= '

'. $data .'

'; + 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 = '
'; + switch ($project['status']) { + case UPDATE_CURRENT: + $row .= t('Up to date'); + break; + case UPDATE_NOT_SECURE: + $row .= ''; + $row .= t('Security update required!'); + $row .= ''; + break; + case UPDATE_NOT_CURRENT: + if ($notification_level == 'all') { + $row .= t('Update available'); + break; + } + // Otherwise, deliberate no break and just ignore this project. + $project['reason'] = t('Not a security update'); + default: + $row .= t('Ignored'); + $row .= ' ('. $project['reason'] .')'; + break; + } + $row .= ''. $icon .''; + $row .= "
\n"; + + $row .= '
'; + 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 .= "
\n"; + + $row .= "
\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 .= "
\n"; // versions div. + + $row .= "
\n"; + if (!empty($project['notes'])) { + $row .= '
'; + $row .= t('Administrator note: %notes', array('%notes' => $project['notes'])); + $row .= "
\n"; + } + + $row .= '
'; + sort($project['includes']); + $row .= t('Includes: %includes', array('%includes' => implode(', ', $project['includes']))); + $row .= "
\n"; + + $row .= "
\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

". $type_label ."

\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 .= ''; + $output .= ''; + $output .= '\n"; + $output .= '\n"; + $output .= ''; + $output .= ''; + $output .= "
'. $tag ."'; + $output .= l($version['version'], $version['release_link']); + $output .= ' ('. format_date($version['date'], 'custom', 'Y-M-d') .') '; + $output .= "
\n"; + return $output; +} Index: modules/update/update.module =================================================================== --- modules/update/update.module (revision 0) +++ modules/update/update.module (revision 0) @@ -0,0 +1,240 @@ +'. 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.') .'

'; + + 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 '

'. t('See the !available_updates page for information on installed modules and themes with new versions released.', array('!available_updates' => l(t('available updates'), 'admin/logs/updates'))) .'

'; + } +} + +/** + * 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/force-check'] = array( + 'title' => 'Manual update check', + 'page callback' => 'update_force_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 + */ +function update_requirements($phase, $plaintext = FALSE) { + if ($phase == 'runtime') { + $requirements['update_core']['title'] = t('Drupal core update status'); + $notification_level = variable_get('update_notification_threshold', 'all'); + $see_more = $plaintext ? '' : ' '. t('See the !available_updates page for more information.', array('!available_updates' => l(t('available updates'), 'admin/logs/updates'))); + 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_drupal']['description'] = 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.') . $see_more; + 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']['description'] = t('There is a security update available for your version of Drupal. To ensure the security of your server, you should update immediately.') . $see_more; + 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']['description'] = 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.') . $see_more; + 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']['description'] = 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.') . $see_more; + } + } + } + else { + $requirements['update_core']['value'] = t('No update data available'); + $requirements['update_core']['severity'] = REQUIREMENT_WARNING; + $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 !run_cron or you can !check_manually. Please note that checking for available updates can take a long time, so please be patient.', array( + '!run_cron' => l(t('run cron'), 'admin/logs/status/run-cron', array('query' => $destination)), + '!check_manually' => l(t('check manually'), 'admin/logs/updates/force-check', array('query' => $destination)), + )); +} + +/** + * Internal helper to try to get the update information from the cache + * if possible, and to refresh the cache when necessary. + * + * @param $refresh + * Boolean to indicate if this method should refresh the cache automatically + * if there's no data. + */ +function update_get_available($refresh = FALSE) { + $available = array(); + if (($cache = cache_get('update_info', 'cache')) + && $cache->expire > time()) { + $available = $cache->data; + } + elseif ($refresh) { + $available = update_refresh(); + } + return $available; +} + +/** + * Invalidates any cached data relating to update status. + */ +function update_invalidate_cache() { + cache_clear_all('update_info', 'cache'); +} + +/** + * Wrapper to load the include file and then refresh the release data. + */ +function update_refresh() { + include_once './modules/update/update.fetch.inc'; + _update_refresh(); +} Index: modules/update/update-rtl.css =================================================================== --- modules/update/update-rtl.css (revision 0) +++ modules/update/update-rtl.css (revision 0) @@ -0,0 +1,27 @@ +/* $Id: update-rtl.css,v 1.1 2007/06/28 20:38:26 dww Exp $ */ + +.update .project { + padding-right: .25em; +} + +.update .version-status { + float: left; + padding-left: 10px; +} + +.update .version-status .icon { + padding-right: .5em; +} + +.update table.version .version-title { + padding-left: 1em; +} + +.update table.version .version-details { + padding-left: .5em; +} + +.update table.version .version-links { + text-align: left; + padding-left: 1em; +} Index: modules/update/update.css =================================================================== --- modules/update/update.css (revision 0) +++ modules/update/update.css (revision 0) @@ -0,0 +1,97 @@ +/* $Id: update.css,v 1.6 2007/06/29 09:08:07 dww Exp $ */ +.update .project { + font-weight: bold; + font-size: 110%; + padding-left: .25em; /* LTR */ + height: 22px; +} + +.update .version-status { + float: right; /* LTR */ + padding-right: 10px; /* LTR */ + font-size: 110%; + height: 20px; +} + +.update .version-status .icon { + padding-left: .5em; /* LTR */ +} + +.update .info { + margin: 0; + padding: 1em 1em .25em 1em; +} + +.update tr td { + border-top: 1px solid #ccc; + border-bottom: 1px solid #ccc; +} + +.update tr.error { + background: #fcc; +} + +.update tr.error .version-recommended { + background: #fdd; +} + +.update tr.ok { + background: #dfd; +} + +.update tr.warning { + background: #ffd; +} + +.update tr.warning .version-recommended { + background: #ffe; +} + +.current-version, .new-version { + direction: ltr; /* Note: version numbers should always be LTR. */ +} + +table.update, +.update table.version { + width: 100%; + margin-top: .5em; +} + +.update table.version tbody { + border: none; +} + +.update table.version tr, +.update table.version td { + line-height: .9em; + padding: 0; + margin: 0; + border: none; +} + +.update table.version .version-title { + padding-left: 1em; /* LTR */ + width: 14em; +} + +.update table.version .version-details { + padding-right: .5em; /* LTR */ +} + +.update table.version .version-links { + text-align: right; /* LTR */ + padding-right: 1em; /* LTR */ +} + +.update table.version-security .version-title { + color: #970F00; +} + +.update table.version-recommended-strong .version-title { + font-weight: bold; +} + +.update .security-error { + font-weight: bold; + color: #970F00; +} Index: modules/update/update.settings.inc =================================================================== --- modules/update/update.settings.inc (revision 0) +++ modules/update/update.settings.inc (revision 0) @@ -0,0 +1,108 @@ + '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 !status_report, the !modules_page, and the !themes_page. 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' => l(t('status report'), 'admin/logs/status'), '!modules_page' => l(t('modules page'), 'admin/build/modules'), '!themes_page' => l(t('themes page'), '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('%email' => 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); +} Index: modules/update/update.info =================================================================== --- modules/update/update.info (revision 0) +++ modules/update/update.info (revision 0) @@ -0,0 +1,6 @@ +; $Id: update.info,v 1.3 2007/06/28 02:20:47 dww Exp $ +name = Update status +description = Checks the status of available updates for Drupal and your installed modules and themes. +version = VERSION +package = Core - optional +core = 6.x