Commit 550b9255 authored by Gábor Hojtsy's avatar Gábor Hojtsy
Browse files

#197186 by dww, testing by catch, webernet, greggles: (critical security...

#197186 by dww, testing by catch, webernet, greggles: (critical security functionality) update.module did not inform users when their current release became revoked/not supported
parent d6c286a5
......@@ -156,10 +156,20 @@ function update_process_project_info(&$projects) {
* remote servers, calculate the current status.
*
* This function is the heart of the update status feature. It iterates over
* every currently installed project, and for each one, decides what major
* release series to consider (the larger of the major version currently
* installed and the default major version specified by the maintainer of that
* project).
* every currently installed project. For each one, it first checks if the
* project has been flagged with a special status like "unsupported" or
* "insecure", or if the project node itself has been unpublished. In any of
* those cases, the project is marked with an error and the next project is
* considered.
*
* If the project itself is valid, the function decides what major release
* series to consider. The project defines what the currently supported major
* versions are for each version of core, so the first step is to make sure
* the current version is still supported. If so, that's the target version.
* If the current version is unsupported, the project maintainer's recommended
* major version is used. There's also a check to make sure that this function
* never recommends an earlier release than the currently installed major
* version.
*
* Given a target major version, it scans the available releases looking for
* the specific release to recommend (avoiding beta releases and development
......@@ -213,28 +223,155 @@ function update_calculate_project_data($available) {
update_process_project_info($projects);
foreach ($projects as $project => $project_info) {
if (isset($available[$project])) {
// If the project status is marked as something bad, there's nothing
// else to consider.
if (isset($available[$project]['project_status'])) {
switch ($available[$project]['project_status']) {
case 'insecure':
$projects[$project]['status'] = UPDATE_NOT_SECURE;
if (empty($projects[$project]['extra'])) {
$projects[$project]['extra'] = array();
}
$projects[$project]['extra'][] = array(
'class' => 'project-not-secure',
'label' => t('Project not secure'),
'data' => t('This project has been labeled insecure by the Drupal security team, and is no longer available for download. Immediately disabling everything included by this project is strongly recommended!'),
);
break;
case 'unpublished':
case 'revoked':
$projects[$project]['status'] = UPDATE_REVOKED;
if (empty($projects[$project]['extra'])) {
$projects[$project]['extra'] = array();
}
$projects[$project]['extra'][] = array(
'class' => 'project-revoked',
'label' => t('Project revoked'),
'data' => t('This project has been revoked, and is no longer available for download. Disabling everything included by this project is strongly recommended!'),
);
break;
case 'unsupported':
$projects[$project]['status'] = UPDATE_NOT_SUPPORTED;
if (empty($projects[$project]['extra'])) {
$projects[$project]['extra'] = array();
}
$projects[$project]['extra'][] = array(
'class' => 'project-not-supported',
'label' => t('Project not supported'),
'data' => t('This project is no longer supported, and is no longer available for download. Disabling everything included by this project is strongly recommended!'),
);
break;
default:
// Assume anything else (e.g. 'published') is valid and we should
// perform the rest of the logic in this function.
break;
}
}
if (!empty($projects[$project]['status'])) {
// We already know the status for this project, so there's nothing
// else to compute. Just record everything else we fetched from the
// XML file into our projects array and move to the next project.
$projects[$project] += $available[$project];
continue;
}
// Figure out the target major version.
$existing_major = $project_info['existing_major'];
if (isset($available[$project]['default_major'])) {
$default_major = $available[$project]['default_major'];
$target_major = max($existing_major, $default_major);
$supported_majors = array();
if (isset($available[$project]['supported_majors'])) {
$supported_majors = explode(',', $available[$project]['supported_majors']);
}
elseif (isset($available[$project]['default_major'])) {
// Older release history XML file without supported or recommended.
$supported_majors[] = $available[$project]['default_major'];
}
if (in_array($existing_major, $supported_majors)) {
// Still supported, stay at the current major version.
$target_major = $existing_major;
}
elseif (isset($available[$project]['recommended_major'])) {
// Since 'recommended_major' is defined, we know this is the new XML
// format. Therefore, we know the current release is unsupported since
// its major version was not in the 'supported_majors' list. We should
// find the best release from the recommended major version.
$target_major = $available[$project]['recommended_major'];
$projects[$project]['status'] = UPDATE_NOT_SUPPORTED;
}
elseif (isset($available[$project]['default_major'])) {
// Older release history XML file without recommended, so recommend
// the currently defined "default_major" version.
$target_major = $available[$project]['default_major'];
}
else {
// Malformed XML file? Stick with the current version.
$target_major = $existing_major;
}
// Make sure we never tell the admin to downgrade. If we recommended an
// earlier version than the one they're running, they'd face an
// impossible data migration problem, since Drupal never supports a DB
// downgrade path. In the unfortunate case that what they're running is
// unsupported, and there's nothing newer for them to upgrade to, we
// can't print out a "Recommended version", but just have to tell them
// what they have is unsupported and let them figure it out.
$target_major = max($existing_major, $target_major);
$version_patch_changed = '';
$patch = '';
// Defend ourselves from XML history files that contain no releases.
if (empty($available[$project]['releases'])) {
$projects[$project]['status'] = UPDATE_UNKNOWN;
$projects[$project]['reason'] = t('No available releases found');
continue;
}
foreach ($available[$project]['releases'] as $version => $release) {
// Ignore unpublished releases.
if ($release['status'] != 'published') {
// First, if this is the existing release, check a few conditions.
if ($projects[$project]['existing_version'] == $version) {
if (isset($release['terms']['Release type']) &&
in_array('Insecure', $release['terms']['Release type'])) {
$projects[$project]['status'] = UPDATE_NOT_SECURE;
}
elseif ($release['status'] == 'unpublished') {
$projects[$project]['status'] = UPDATE_REVOKED;
if (empty($projects[$project]['extra'])) {
$projects[$project]['extra'] = array();
}
$projects[$project]['extra'][] = array(
'class' => 'release-revoked',
'label' => t('Release revoked'),
'data' => t('Your currently installed release has been revoked, and is no longer available for download. Disabling everything included in this release or upgrading is strongly recommended!'),
);
}
elseif (isset($release['terms']['Release type']) &&
in_array('Unsupported', $release['terms']['Release type'])) {
$projects[$project]['status'] = UPDATE_NOT_SUPPORTED;
if (empty($projects[$project]['extra'])) {
$projects[$project]['extra'] = array();
}
$projects[$project]['extra'][] = array(
'class' => 'release-not-supported',
'label' => t('Release not supported'),
'data' => t('Your currently installed release is now unsupported, and is no longer available for download. Disabling everything included in this release or upgrading is strongly recommended!'),
);
}
}
// Otherwise, ignore unpublished, insecure, or unsupported releases.
if ($release['status'] == 'unpublished' ||
(isset($release['terms']['Release type']) &&
(in_array('Insecure', $release['terms']['Release type']) ||
in_array('Unsupported', $release['terms']['Release type'])))) {
continue;
}
// See if this is a higher major version than our target, and if so,
// record it as an "Also available" release.
if ($release['version_major'] > $target_major) {
// See if this is a higher major version than our target and yet still
// supported. If so, record it as an "Also available" release.
if ($release['version_major'] > $target_major &&
in_array($release['version_major'], $supported_majors)) {
if (!isset($available[$project]['also'])) {
$available[$project]['also'] = array();
}
......@@ -303,8 +440,7 @@ function update_calculate_project_data($available) {
}
// See if this release is a security update.
if (isset($release['terms'])
&& isset($release['terms']['Release type'])
if (isset($release['terms']['Release type'])
&& in_array('Security update', $release['terms']['Release type'])) {
$projects[$project]['security updates'][] = $release;
}
......@@ -316,19 +452,6 @@ function update_calculate_project_data($available) {
$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];
......@@ -336,37 +459,49 @@ function update_calculate_project_data($available) {
// Check to see if we need an update or not.
//
// If we don't know what to recommend, there's nothing much we can
// report, so bail out early.
if (!empty($projects[$project]['security updates'])) {
// If we found security updates, that always trumps any other status.
$projects[$project]['status'] = UPDATE_NOT_SECURE;
}
if (isset($projects[$project]['status'])) {
// If we already know the status, we're done.
continue;
}
// If we don't know what to recommend, there's nothing we can report.
// Bail out early.
if (!isset($projects[$project]['recommended'])) {
$projects[$project]['status'] = UPDATE_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');
// 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'];
}
}
// Figure out the status, based on what we've seen and the install type.
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;
}
$projects[$project]['status'] = UPDATE_NOT_CURRENT;
}
break;
case 'dev':
if (!empty($projects[$project]['security updates'])) {
$projects[$project]['status'] = UPDATE_NOT_SECURE;
break;
}
case 'dev':
$latest = $available[$project]['releases'][$projects[$project]['latest_dev']];
if (empty($projects[$project]['datestamp'])) {
$projects[$project]['status'] = UPDATE_NOT_CHECKED;
......
......@@ -17,29 +17,40 @@
// These are internally used constants for this code, do not modify.
/**
* Project is up to date.
* Project is missing security update(s).
*/
define('UPDATE_CURRENT', 1);
define('UPDATE_NOT_SECURE', 1);
/**
* Project is missing security update(s).
* Current release has been unpublished and is no longer available.
*/
define('UPDATE_NOT_SECURE', 2);
define('UPDATE_REVOKED', 2);
/**
* Current release is no longer supported by the project maintainer.
*/
define('UPDATE_NOT_SUPPORTED', 3);
/**
* Project has a new release available, but it is not a security release.
*/
define('UPDATE_NOT_CURRENT', 3);
define('UPDATE_NOT_CURRENT', 4);
/**
* Project is up to date.
*/
define('UPDATE_CURRENT', 5);
/**
* Project's status cannot be checked.
*/
define('UPDATE_NOT_CHECKED', 4);
define('UPDATE_NOT_CHECKED', -1);
/**
* No available update data was found for project.
*/
define('UPDATE_UNKNOWN', 5);
define('UPDATE_UNKNOWN', -2);
/**
* Implementation of hook_help().
......@@ -179,60 +190,25 @@ function update_theme() {
*/
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', UPDATE_NOT_SECURE, TRUE);
break;
default:
$requirements['update_core']['value'] = t('Up to date');
break;
}
// First, populate the requirements for core:
$requirements['update_core'] = _update_requirement_check($data['drupal'], 'core');
// We don't want to check drupal a second time.
unset($data['drupal']);
$not_current = FALSE;
if (!empty($data)) {
$requirements['update_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);
}
// Now, sort our $data array based on each project's status. The
// status constants are numbered in the right order of precedence, so
// we just need to make sure the projects are sorted in ascending
// order of status, and we can look at the first project we find.
uasort($data, '_update_project_status_sort');
$first_project = reset($data);
$requirements['update_contrib'] = _update_requirement_check($first_project, 'contrib');
}
}
else {
$requirements['update_core']['title'] = t('Drupal core update status');
$requirements['update_core']['value'] = t('No update data available');
$requirements['update_core']['severity'] = REQUIREMENT_WARNING;
$requirements['update_core']['reason'] = UPDATE_UNKNOWN;
......@@ -242,6 +218,67 @@ function update_requirements($phase) {
}
}
/**
* Private helper method to fill in the requirements array.
*
* This is shared for both core and contrib to generate the right elements in
* the array for hook_requirements().
*
* @param $project
* Array of information about the project we're testing as returned by
* update_calculate_project_data().
* @param $type
* What kind of project is this ('core' or 'contrib').
*
* @return
* An array to be included in the nested $requirements array.
*
* @see hook_requirements()
* @see update_requirements()
* @see update_calculate_project_data()
*/
function _update_requirement_check($project, $type) {
$requirement = array();
if ($type == 'core') {
$requirement['title'] = t('Drupal core update status');
}
else {
$requirement['title'] = t('Module and theme update status');
}
$status = $project['status'];
if ($status != UPDATE_CURRENT) {
$requirement['reason'] = $status;
$requirement['description'] = _update_message_text($type, $status, TRUE);
$requirement['severity'] = REQUIREMENT_ERROR;
}
switch ($status) {
case UPDATE_NOT_SECURE:
$requirement['value'] = t('Not secure!');
break;
case UPDATE_REVOKED:
$requirement['value'] = t('Revoked!');
break;
case UPDATE_NOT_SUPPORTED:
$requirement['value'] = t('Unsupported release');
break;
case UPDATE_NOT_CURRENT:
$requirement['value'] = t('Out of date');
$requirement['severity'] = variable_get('update_notification_threshold', 'all') == 'all' ? REQUIREMENT_ERROR : REQUIREMENT_WARNING;
break;
case UPDATE_UNKNOWN:
case UPDATE_NOT_CHECKED:
$requirement['value'] = isset($project['reason']) ? $project['reason'] : t('Can not determine status');
$requirement['severity'] = REQUIREMENT_WARNING;
break;
default:
$requirement['value'] = t('Up to date');
}
if ($status != UPDATE_CURRENT && $type == 'core' && isset($project['recommended'])) {
$requirement['value'] .= ' '. t('(version @version available)', array('@version' => $project['recommended']));
}
return $requirement;
}
/**
* Implementation of hook_cron().
*/
......@@ -355,8 +392,7 @@ function update_mail($key, &$message, $params) {
* 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.
* Integer constant specifying why message is generated.
* @param $report_link
* Boolean that controls if a link to the updates report should be added.
* @param $language
......@@ -368,6 +404,33 @@ function _update_message_text($msg_type, $msg_reason, $report_link = FALSE, $lan
$langcode = isset($language) ? $language->language : NULL;
$text = '';
switch ($msg_reason) {
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;
case UPDATE_REVOKED:
if ($msg_type == 'core') {
$text = t('Your version of Drupal has been revoked and is no longer available for download. Upgrading is strongly recommended!', array(), $langcode);
}
else {
$text = t('The installed version of at least one of your modules or themes has been revoked and is no longer available for download. Upgrading or disabling is strongly recommended!', array(), $langcode);
}
break;
case UPDATE_NOT_SUPPORTED:
if ($msg_type == 'core') {
$text = t('Your version of Drupal is no longer supported. Upgrading is strongly recommended!', array(), $langcode);
}
else {
$text = t('The installed version of at least one of your modules or themes is no longer supported. Upgrading or disabling is strongly recommended! Please see the project homepage for more details.', array(), $langcode);
}
break;
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);
......@@ -377,12 +440,13 @@ function _update_message_text($msg_type, $msg_reason, $report_link = FALSE, $lan
}
break;
case UPDATE_NOT_SECURE:
case UPDATE_UNKNOWN:
case UPDATE_NOT_CHECKED:
if ($msg_type == 'core') {
$text = t('There is a security update available for your version of Drupal. To ensure the security of your server, you should update immediately!', array(), $langcode);
$text = t('There was a problem determining the status of available updates for your version of Drupal.', 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);
$text = t('There was a problem determining the status of available updates for one or more of your modules or themes.', array(), $langcode);
}
break;
}
......@@ -393,3 +457,19 @@ function _update_message_text($msg_type, $msg_reason, $report_link = FALSE, $lan
return $text;
}
/**
* Private sort function to order projects based on their status.
*
* @see update_requirements()
* @see uasort()
*/
function _update_project_status_sort($a, $b) {
// The status constants are numerically in the right order, so we can
// usually subtract the two to compare in the order we want. However,
// negative status values should be treated as if they are huge, since we
// always want them at the bottom of the list.
$a_status = $a['status'] > 0 ? $a['status'] : (-10 * $a['status']);
$b_status = $b['status'] > 0 ? $b['status'] : (-10 * $b['status']);
return $a_status - $b_status;
}
......@@ -48,9 +48,11 @@ function theme_update_report($data) {
$icon = theme('image', 'misc/watchdog-ok.png');
break;
case UPDATE_NOT_SECURE:
case UPDATE_REVOKED:
case UPDATE_NOT_SUPPORTED:
case UPDATE_NOT_CURRENT:
if ($notification_level == 'all'
|| $project['status'] == UPDATE_NOT_SECURE) {
|| $project['status'] != UPDATE_NOT_CURRENT) {
$class = 'error';
$icon = theme('image', 'misc/watchdog-error.png');
break;
......@@ -64,16 +66,20 @@ function theme_update_report($data) {
$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>';
$row .= '<span class="security-error">'. t('Security update required!') .'</span>';
break;
case UPDATE_REVOKED:
$row .= '<span class="revoked">'. t('Revoked!') .'</span>';
break;
case UPDATE_NOT_SUPPORTED:
$row .= '<span class="not-supported">'. t('Not supported!') .'</span>';
break;
case UPDATE_NOT_CURRENT:
$row .= t('Update available');
$row .= '<span class="not-current">'. t('Update available') .'</span>';
break;
case UPDATE_CURRENT:
$row .= '<span class="current">'. t('Up to date') .'</span>';
break;
default:
$row .= check_plain($project['reason']);
......@@ -120,6 +126,7 @@ function theme_update_report($data) {
if ($project['recommended'] != $project['latest_version']
|| !empty($project['also'])
|| ($project['install_type'] == 'dev'
&& isset($project['dev_version'])
&& $project['latest_version'] != $project['dev_version']
&& $project['recommended'] != $project['dev_version'])
|| (isset($project['security updates'][0])
......@@ -143,6 +150,7 @@ function theme_update_report($data) {
}
if ($project['install_type'] == 'dev'
&& $project['status'] != UPDATE_CURRENT
&& isset($project['dev_version'])
&& $project['recommended'] != $project['dev_version']) {
$row .= theme('update_version', $project['releases'][$project['dev_version']], t('Development version:'), 'version-latest');
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment