Skip to content
Snippets Groups Projects
Commit 9119f026 authored by Lucas Hedding's avatar Lucas Hedding Committed by Lucas Hedding
Browse files

Issue #3059742 by heddn, catch: Backport base readiness checker logic to 7.x

parent 08519bd2
No related branches found
No related tags found
No related merge requests found
<?php
/**
* Warn if PHP SAPI changes between checker executions.
*/
class PhpSapi implements ReadinessCheckerInterface {
/**
* {@inheritdoc}
*/
public static function run() {
$messages = [];
$php_sapi = variable_get('automatic_updates.php_sapi', PHP_SAPI);
if ($php_sapi !== PHP_SAPI) {
$messages[] = t('PHP changed from running as "@previous" to "@current". This can lead to inconsistent and misleading results.', ['@previous' => $php_sapi, '@current' => PHP_SAPI]);
}
variable_set('automatic_updates.php_sapi', PHP_SAPI);
return $messages;
}
}
<?php
/**
* Interface for objects capable of readiness checking.
*/
interface ReadinessCheckerInterface {
/**
* Run check.
*
* @return array
* An array of translatable strings.
*/
public static function run();
}
<?php
/**
* Defines a chained readiness checker implementation combining multiple checks.
*/
class ReadinessCheckerManager {
/**
* An unsorted array of arrays of active checkers.
*
* An associative array. The keys are integers that indicate priority. Values
* are arrays of ReadinessCheckerInterface objects.
*
* @var ReadinessCheckerInterface[][]
*/
protected static $checkers = [];
/**
* Get checkers.
*
* @return array
* The registered checkers.
*/
protected static function getCheckers() {
static::$checkers['warning'][0][] = 'PhpSapi';
static::$checkers['error'][0][] = 'PhpSapi';
return static::$checkers;
}
/**
* Run checks.
*
* @param string $category
* The category of check.
*
* @return array
* An array of translatable strings.
*/
public static function run($category) {
$messages = [];
if (!static::isEnabled()) {
return $messages;
}
if (!isset(static::getSortedCheckers()[$category])) {
throw new \InvalidArgumentException(sprintf('No readiness checkers exist of category "%s"', $category));
}
foreach (static::getSortedCheckers()[$category] as $checker) {
$messages = array_merge($messages, $checker::run());
}
// Guard against variable_set stampede by checking if the values have
// changed since previous run.
$previous_messages = variable_get("automatic_updates.readiness_check_results.$category");
if ($previous_messages !== $messages) {
variable_set("automatic_updates.readiness_check_results.$category", $messages);
}
if (variable_get('automatic_updates.readiness_check_timestamp') !== REQUEST_TIME) {
variable_set('automatic_updates.readiness_check_timestamp', REQUEST_TIME);
}
return $messages;
}
/**
* {@inheritdoc}
*/
public static function getResults($category) {
$results = [];
if (static::isEnabled()) {
$results = variable_get("automatic_updates.readiness_check_results.$category", []);
}
return $results;
}
/**
* {@inheritdoc}
*/
public static function timestamp() {
return variable_get('automatic_updates.readiness_check_timestamp', 0);
}
/**
* {@inheritdoc}
*/
public static function isEnabled() {
return variable_get('automatic_updates_enable_readiness_checks', TRUE);
}
/**
* {@inheritdoc}
*/
public static function getCategories() {
return ['warning', 'error'];
}
/**
* Sorts checkers according to priority.
*
* @return ReadinessCheckerInterface[]
* A sorted array of checker objects.
*/
protected static function getSortedCheckers() {
$sorted = [];
foreach (static::getCheckers() as $category => $priorities) {
foreach ($priorities as $checkers) {
krsort($checkers);
$sorted[$category] = isset($sorted[$category]) ? array_merge($sorted[$category], $checkers) : array_merge([], $checkers);
}
}
return $sorted;
}
}
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
/** /**
* Form callback for administrator interface. * Form callback for administrator interface.
*/ */
function automatic_updates_admin_form($form, &$form_state) { function automatic_updates_admin_form() {
$form['description'] = [ $form['description'] = [
'#markup' => '<p>' . t('Public service announcements are compared against the entire code for the site, not just installed extensions.') . '</p>', '#markup' => '<p>' . t('Public service announcements are compared against the entire code for the site, not just installed extensions.') . '</p>',
]; ];
...@@ -17,5 +17,42 @@ function automatic_updates_admin_form($form, &$form_state) { ...@@ -17,5 +17,42 @@ function automatic_updates_admin_form($form, &$form_state) {
'#title' => t('Show public service announcements on administrative pages.'), '#title' => t('Show public service announcements on administrative pages.'),
'#default_value' => variable_get('automatic_updates_enable_psa', TRUE), '#default_value' => variable_get('automatic_updates_enable_psa', TRUE),
]; ];
$last_check_timestamp = ReadinessCheckerManager::timestamp();
$form['automatic_updates_enable_readiness_checks'] = [
'#type' => 'checkbox',
'#title' => t('Check the readiness of automatically updating the site.'),
'#default_value' => variable_get('automatic_updates_enable_readiness_checks', TRUE),
];
if (ReadinessCheckerManager::isEnabled()) {
$form['automatic_updates_enable_readiness_checks']['#description'] = t('Readiness checks were last run @time ago. Manually <a href="@link">run the readiness checks</a>.', [
'@time' => format_interval(REQUEST_TIME - $last_check_timestamp),
'@link' => url('admin/config/system/automatic_updates/readiness'),
]);
}
$form['automatic_updates_ignored_paths'] = [
'#type' => 'textarea',
'#title' => t('Paths to ignore for readiness checks'),
'#description' => t('Paths relative to %drupal_root. One path per line.', ['%drupal_root' => DRUPAL_ROOT]),
'#default_value' => variable_get('automatic_updates_ignored_paths', "modules/custom/*\nthemes/custom/*\nprofiles/custom/*"),
'#states' => [
'visible' => [
':input[name="automatic_updates_enable_readiness_checks"]' => ['checked' => TRUE],
],
],
];
return system_settings_form($form); return system_settings_form($form);
} }
/**
* Page callback to run all checkers.
*/
function automatic_updates_run_checks() {
$messages = [];
foreach (ReadinessCheckerManager::getCategories() as $category) {
$messages = array_merge(ReadinessCheckerManager::run($category), $messages);
}
if (empty($messages)) {
drupal_set_message(t('No issues found. Your site is completely ready for <a href="@readiness_checks">automatic updates</a>.', ['@readiness_checks' => 'https://www.drupal.org/docs/8/update/automatic-updates#readiness-checks']), 'status', FALSE);
}
drupal_goto('admin/config/system/automatic_updates');
}
...@@ -6,3 +6,6 @@ package = Security ...@@ -6,3 +6,6 @@ package = Security
files[] = tests/automatic_updates.test files[] = tests/automatic_updates.test
files[] = AutomaticUpdatesPsa.php files[] = AutomaticUpdatesPsa.php
files[] = ReadinessCheckers/ReadinessCheckerManager.php
files[] = ReadinessCheckers/ReadinessCheckerInterface.php
files[] = ReadinessCheckers/PhpSapi.php
...@@ -11,6 +11,11 @@ ...@@ -11,6 +11,11 @@
function automatic_updates_uninstall() { function automatic_updates_uninstall() {
variable_del('automatic_updates_psa_endpoint'); variable_del('automatic_updates_psa_endpoint');
variable_del('automatic_updates_enable_psa'); variable_del('automatic_updates_enable_psa');
variable_del('automatic_updates_enable_readiness_checks');
variable_del('automatic_updates.readiness_check_results.error');
variable_del('automatic_updates.readiness_check_results.warning');
variable_del('automatic_updates.readiness_check_timestamp');
variable_del('automatic_updates.php_sapi');
} }
/** /**
...@@ -22,10 +27,57 @@ function automatic_updates_requirements($phase) { ...@@ -22,10 +27,57 @@ function automatic_updates_requirements($phase) {
} }
$requirements = array(); $requirements = array();
_automatic_updates_checker_requirements($requirements);
_automatic_updates_psa_requirements($requirements); _automatic_updates_psa_requirements($requirements);
return $requirements; return $requirements;
} }
/**
* Display requirements from results of readiness checker.
*
* @param array $requirements
* The requirements array.
*/
function _automatic_updates_checker_requirements(array &$requirements) {
if (!ReadinessCheckerManager::isEnabled()) {
return;
}
$last_check_timestamp = ReadinessCheckerManager::timestamp();
$requirements['automatic_updates_readiness'] = [
'title' => t('Update readiness checks'),
'severity' => REQUIREMENT_OK,
'value' => t('Your site is ready to for <a href="@readiness_checks">automatic updates</a>.', ['@readiness_checks' => 'https://www.drupal.org/docs/8/update/automatic-updates#readiness-checks']),
];
$error_results = ReadinessCheckerManager::getResults('error');
$warning_results = ReadinessCheckerManager::getResults('warning');
$checker_results = array_merge($error_results, $warning_results);
if (!empty($checker_results)) {
$requirements['automatic_updates_readiness']['severity'] = $error_results ? REQUIREMENT_ERROR : REQUIREMENT_WARNING;
$requirements['automatic_updates_readiness']['value'] = format_plural(count($checker_results), '@count check failed:', '@count checks failed:');
$requirements['automatic_updates_readiness']['description'] = [
'#theme' => 'item_list',
'#items' => $checker_results,
];
}
if (REQUEST_TIME > $last_check_timestamp + 3600 * 24) {
$requirements['automatic_updates_readiness']['severity'] = REQUIREMENT_ERROR;
$requirements['automatic_updates_readiness']['value'] = t('Your site has not recently checked if it is ready to apply <a href="@readiness_checks">automatic updates</a>.', ['@readiness_checks' => 'https://www.drupal.org/docs/8/update/automatic-updates#readiness-checks']);
$time_ago = format_interval(REQUEST_TIME - $last_check_timestamp);
if ($last_check_timestamp === 0) {
$requirements['automatic_updates_readiness']['description'] = t('<a href="@link">Run readiness checks</a> manually.', [
'@link' => url('admin/config/system/automatic_updates/readiness'),
]);
}
else {
$requirements['automatic_updates_readiness']['description'] = t('Last run @time ago. <a href="@link">Run readiness checks</a> manually.', [
'@time' => $time_ago,
'@link' => url('admin/config/system/automatic_updates/readiness'),
]);
}
}
}
/** /**
* Display requirements from public service announcements. * Display requirements from public service announcements.
* *
......
...@@ -29,9 +29,28 @@ function automatic_updates_init() { ...@@ -29,9 +29,28 @@ function automatic_updates_init() {
$messages = AutomaticUpdatesPsa::getPublicServiceMessages(); $messages = AutomaticUpdatesPsa::getPublicServiceMessages();
if ($messages) { if ($messages) {
drupal_set_message(t('Drupal public service announcements:'), 'error'); drupal_set_message(t('Drupal public service announcements:'), 'error', FALSE);
foreach ($messages as $message) { foreach ($messages as $message) {
drupal_set_message($message, 'error'); drupal_set_message($message, 'error', FALSE);
}
}
$last_check_timestamp = ReadinessCheckerManager::timestamp();
if (REQUEST_TIME > $last_check_timestamp + 3600 * 24) {
drupal_set_message(t('Your site has not recently run an update readiness check.'), 'error', FALSE);
}
$results = ReadinessCheckerManager::getResults('error');
if ($results) {
drupal_set_message(t('Your site is currently failing readiness checks for automatic updates. It cannot be <a href="@readiness_checks">automatically updated</a> until further action is performed:', ['@readiness_checks' => 'https://www.drupal.org/docs/8/update/automatic-updates#readiness-checks']), 'error', FALSE);
foreach ($results as $message) {
drupal_set_message($message, 'error', FALSE);
}
}
$results = ReadinessCheckerManager::getResults('warning');
if ($results) {
drupal_set_message(t('Your site does not pass some readiness checks for automatic updates. Depending on the nature of the failures, it might effect the eligibility for <a href="@readiness_checks">automatic updates</a>.', ['@readiness_checks' => 'https://www.drupal.org/docs/8/update/automatic-updates#readiness-checks']), 'warning', FALSE);
foreach ($results as $message) {
drupal_set_message($message, 'warning', FALSE);
} }
} }
} }
...@@ -52,6 +71,22 @@ function automatic_updates_menu() { ...@@ -52,6 +71,22 @@ function automatic_updates_menu() {
'access arguments' => ['administer software updates'], 'access arguments' => ['administer software updates'],
'tab parent' => 'admin/config/system', 'tab parent' => 'admin/config/system',
]; ];
$items['admin/config/system/automatic_updates/readiness'] = [
'title' => 'Update readiness checking...',
'page callback' => 'automatic_updates_run_checks',
'file' => 'automatic_updates.admin.inc',
'file path' => drupal_get_path('module', 'automatic_updates'),
'access arguments' => ['administer software updates'],
];
return $items; return $items;
} }
/**
* Implements hook_cron().
*/
function automatic_updates_cron() {
foreach (ReadinessCheckerManager::getCategories() as $category) {
ReadinessCheckerManager::run($category);
}
}
...@@ -72,4 +72,17 @@ class AutomaticUpdatesTestCase extends DrupalWebTestCase { ...@@ -72,4 +72,17 @@ class AutomaticUpdatesTestCase extends DrupalWebTestCase {
$this->assertText('automatic_updates/test-json-denied is unreachable.'); $this->assertText('automatic_updates/test-json-denied is unreachable.');
} }
/**
* Tests manually running readiness checks.
*/
public function testReadinessChecks() {
// Fabricate a readiness issue.
$this->drupalGet($this->getAbsoluteUrl('admin/config/system/automatic_updates'));
variable_set('automatic_updates.php_sapi', 'foo');
$this->clickLink('run the readiness checks');
$this->assertText('Your site does not pass some readiness checks for automatic updates. Depending on the nature of the failures, it might effect the eligibility for automatic updates.');
$this->assertText('PHP changed from running as');
$this->assertText('This can lead to inconsistent and misleading results.');
}
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment