Skip to content
Snippets Groups Projects
Unverified Commit 304a6416 authored by Ted Bowman's avatar Ted Bowman
Browse files

Switch to codebase from Automatic Updates sandbox from inital core work

parent 30da94ba
No related branches found
No related tags found
No related merge requests found
Showing
with 616 additions and 989 deletions
Automatic Updates Automatic Updates
--------------- ---------------
### About this Module
Updating a Drupal site is difficult, time-consuming, and expensive. It is a
tricky problem that, on its face appears easy, however, ensuring secure and
reliable updates that give assurance to site owners and availability to site
visitors.
The Automatic Updates module is not yet in core. In its initial form, it is
being made available as a contributed module. Please note that Automatic Updates
is a [Strategic Initiative](https://www.drupal.org/project/ideas/issues/2940731)
for the Drupal Project. The Initiative is still in progress and additional
features and bug fixes are regularly added.
The primary use case for this module:
- **Public service announcements (PSAs)**
Announcements for highly critical security releases for core and contrib modules
are done infrequently. When a PSA is released, site owners should review their
sites to verify they are up to date with the latest releases and the site is in
a good state to quickly update once the fixes are provided to the community.
- **Update readiness checks**
Not all sites are able to always update. The readiness checks are an automated
method to determine if a site is ready for automatically updating once a new
release is provided to the community. For example, sites that have un-run
database updates, are mounted on read only filesystems or do not have sufficient
disk space to update in-place can't receive automatic updates. If your site is
failing readiness checks and a PSA is released, it is important to resolve the
underlying readiness issues so the site can quickly be updated.
- **In-Place Updates**
Once the PSA service has notified a Drupal site owner of an available update,
and the readiness checks have confirmed that the site is ready to be updated,
the Automatic Update service can then apply the update.
### Goals
The Automatic Update service for Drupal aims to simplify the update process and
provide confidence that an update will apply cleanly.
### Demo
> [Watch a demo](https://youtu.be/fT2--EBhzuE) of the module from DriesNote at
> DrupalCon Amsterdam 2019.
### Installing the Automatic Updates Module
1. Copy/upload the automatic_updates module to the modules directory of your
Drupal installation.
1. Enable the 'Automatic Updates' module in 'Extend' (/admin/modules).
1. Configure the module to enable PSA notifications, readiness checks and
in-place updates (/admin/config/automatic_updates).
### Automatic Updates Initiative ### Automatic Updates Initiative
- Follow and read up on - Follow and read up on
......
untrusted comment: signify public key
RWQVj5RBijXj1b4WXWOlakzu1EzALf24UtLk1q/D3LV0H5Uh+BP9kdHU
name: 'Automatic Updates' name: 'Automatic Updates'
type: module type: module
description: 'Display Public service announcements and verify readiness for applying automatic updates to the site.' description: 'Experimental module to develop automatic updates. Currently the module provides checks for update readiness but does not yet provide update functionality.'
core: 8.x core_version_requirement: ^9
core_version_requirement: ^8 || ^9
package: 'Security'
configure: automatic_updates.settings
dependencies: dependencies:
- drupal:system (>= 8.7) - automatic_updates_9_3_shim
- drupal:update (>= 8.7) - update
...@@ -2,120 +2,20 @@ ...@@ -2,120 +2,20 @@
/** /**
* @file * @file
* Automatic updates install file. * Contains install and update functions for Automatic Updates.
*/ */
use Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManagerInterface; use Drupal\automatic_updates\Validation\ReadinessRequirements;
use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
use Drupal\Core\Url;
/** /**
* Implements hook_requirements(). * Implements hook_requirements().
*/ */
function automatic_updates_requirements($phase) { function automatic_updates_requirements($phase) {
// Mimic the functionality of the vendor checker procedurally since class
// loading isn't available pre module install.
$vendor_autoloader = [
DRUPAL_ROOT,
'vendor',
'autoload.php',
];
if (!file_exists(implode(DIRECTORY_SEPARATOR, $vendor_autoloader))) {
return [
'not installable' => [
'title' => t('Automatic Updates'),
'severity' => REQUIREMENT_ERROR,
'value' => '1.x',
'description' => t('This module does not currently support relocated vendor folder and composer-based workflows.'),
],
];
}
if ($phase !== 'runtime') { if ($phase !== 'runtime') {
return NULL; return [];
}
$requirements = [];
_automatic_updates_checker_requirements($requirements);
_automatic_updates_psa_requirements($requirements);
return $requirements;
}
/**
* Display requirements from results of readiness checker.
*
* @param array $requirements
* The requirements array.
*/
function _automatic_updates_checker_requirements(array &$requirements) {
/** @var \Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManagerInterface $checker */
$checker = \Drupal::service('automatic_updates.readiness_checker');
if (!$checker->isEnabled()) {
return;
} }
$last_check_timestamp = $checker->timestamp(); /** @var \Drupal\automatic_updates\Validation\ReadinessRequirements $readiness_requirement */
$requirements['automatic_updates_readiness'] = [ $readiness_requirement = \Drupal::classResolver(ReadinessRequirements::class);
'title' => t('Update readiness checks'), return $readiness_requirement->getRequirements();
'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 = $checker->getResults(ReadinessCheckerManagerInterface::ERROR);
$warning_results = $checker->getResults(ReadinessCheckerManagerInterface::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'] = new PluralTranslatableMarkup(count($checker_results), '@count check failed:', '@count checks failed:');
$requirements['automatic_updates_readiness']['description'] = [
'#theme' => 'item_list',
'#items' => $checker_results,
];
}
if (\Drupal::time()->getRequestTime() > $last_check_timestamp + ReadinessCheckerManagerInterface::LAST_CHECKED_WARNING) {
$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']);
$readiness_check = Url::fromRoute('automatic_updates.update_readiness');
$time_ago = \Drupal::service('date.formatter')->formatTimeDiffSince($last_check_timestamp);
if ($last_check_timestamp === 0) {
$requirements['automatic_updates_readiness']['description'] = t('<a href="@link">Run readiness checks</a> manually.', [
'@link' => $readiness_check->toString(),
]);
}
elseif ($readiness_check->access()) {
$requirements['automatic_updates_readiness']['description'] = t('Last run @time ago. <a href="@link">Run readiness checks</a> manually.', [
'@time' => $time_ago,
'@link' => $readiness_check->toString(),
]);
}
else {
$requirements['automatic_updates_readiness']['description'] = t('Readiness checks were last run @time ago.', ['@time' => $time_ago]);
}
}
}
/**
* Display requirements from Public service announcements.
*
* @param array $requirements
* The requirements array.
*/
function _automatic_updates_psa_requirements(array &$requirements) {
if (!\Drupal::config('automatic_updates.settings')->get('enable_psa')) {
return;
}
/** @var \Drupal\automatic_updates\Services\AutomaticUpdatesPsa $psa */
$psa = \Drupal::service('automatic_updates.psa');
$messages = $psa->getPublicServiceMessages();
$requirements['automatic_updates_psa'] = [
'title' => t('<a href="@link">Public service announcements</a>', ['@link' => 'https://www.drupal.org/docs/8/update/automatic-updates#psas']),
'severity' => REQUIREMENT_OK,
'value' => t('No announcements requiring attention.'),
];
if (!empty($messages)) {
$requirements['automatic_updates_psa']['severity'] = REQUIREMENT_ERROR;
$requirements['automatic_updates_psa']['value'] = new PluralTranslatableMarkup(count($messages), '@count urgent announcement requires your attention:', '@count urgent announcements require your attention:');
$requirements['automatic_updates_psa']['description'] = [
'#theme' => 'item_list',
'#items' => $messages,
];
}
} }
automatic_updates.settings:
title: 'Automatic updates'
route_name: automatic_updates.settings
description: 'Configure automatic update settings.'
parent: system.admin_config_system
...@@ -2,198 +2,71 @@ ...@@ -2,198 +2,71 @@
/** /**
* @file * @file
* Contains automatic_updates.module.. * Contains hook implementations for Automatic Updates.
*/ */
use Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManagerInterface; use Drupal\automatic_updates\Validation\AdminReadinessMessages;
use Drupal\automatic_updates\UpdateMetadata; use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url; use Drupal\Core\Url;
use Drupal\update\UpdateManagerInterface;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\Process;
/** /**
* Implements hook_page_top(). * Implements hook_page_top().
*/ */
function automatic_updates_page_top(array &$page_top) { function automatic_updates_page_top() {
/** @var \Drupal\Core\Routing\AdminContext $admin_context */ /** @var \Drupal\automatic_updates\Validation\AdminReadinessMessages $readiness_messages */
$admin_context = \Drupal::service('router.admin_context'); $readiness_messages = \Drupal::classResolver(AdminReadinessMessages::class);
$route_match = \Drupal::routeMatch(); $readiness_messages->displayAdminPageMessages();
if ($admin_context->isAdminRoute($route_match->getRouteObject()) && \Drupal::currentUser()->hasPermission('administer site configuration')) {
$disabled_routes = [
'update.theme_update',
'system.theme_install',
'update.module_update',
'update.module_install',
'update.status',
'update.report_update',
'update.report_install',
'update.settings',
'system.status',
'update.confirmation_page',
];
// These routes don't need additional nagging.
if (in_array(\Drupal::routeMatch()->getRouteName(), $disabled_routes, TRUE)) {
return;
}
/** @var \Drupal\automatic_updates\Services\AutomaticUpdatesPsaInterface $psa */
$psa = \Drupal::service('automatic_updates.psa');
$messages = $psa->getPublicServiceMessages();
if ($messages) {
\Drupal::messenger()->addError(t('Public service announcements:'));
foreach ($messages as $message) {
\Drupal::messenger()->addError($message);
}
}
$last_check_timestamp = \Drupal::service('automatic_updates.readiness_checker')->timestamp();
if (\Drupal::time()->getRequestTime() > $last_check_timestamp + ReadinessCheckerManagerInterface::LAST_CHECKED_WARNING) {
$readiness_settings = Url::fromRoute('automatic_updates.settings');
\Drupal::messenger()->addError(t('Your site has not recently run an update readiness check. <a href="@link">Administer automatic updates</a> and run readiness checks manually.', [
'@link' => $readiness_settings->toString(),
]));
}
/** @var \Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManagerInterface $checker */
$checker = \Drupal::service('automatic_updates.readiness_checker');
$results = $checker->getResults(ReadinessCheckerManagerInterface::ERROR);
if ($results) {
\Drupal::messenger()->addError(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']));
foreach ($results as $message) {
\Drupal::messenger()->addError($message);
}
}
$results = $checker->getResults('warning');
if ($results) {
\Drupal::messenger()->addWarning(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']));
foreach ($results as $message) {
\Drupal::messenger()->addWarning($message);
}
}
}
} }
/** /**
* Implements hook_cron(). * Implements hook_cron().
*/ */
function automatic_updates_cron() { function automatic_updates_cron() {
$state = \Drupal::state(); /** @var \Drupal\automatic_updates\Validation\ReadinessValidationManager $checker_manager */
$request_time = \Drupal::time()->getRequestTime(); $checker_manager = \Drupal::service('automatic_updates.readiness_validation_manager');
$last_check = $state->get('automatic_updates.cron_last_check', 0); $last_results = $checker_manager->getResults();
// Only allow cron to run once every hour. $last_run_time = $checker_manager->getLastRunTime();
if (($request_time - $last_check) < 3600) { // Do not run readiness checks more than once an hour unless there are no
return; // results available.
} if ($last_results === NULL || !$last_run_time || \Drupal::time()->getRequestTime() - $last_run_time > 3600) {
$checker_manager->run();
// Checkers should run before updates because of class caching.
/** @var \Drupal\automatic_updates\Services\NotifyInterface $notify */
$notify = \Drupal::service('automatic_updates.psa_notify');
$notify->send();
/** @var \Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManagerInterface $checker */
$checker = \Drupal::service('automatic_updates.readiness_checker');
foreach ($checker->getCategories() as $category) {
$checker->run($category);
} }
// In-place updates won't function for dev releases of Drupal core.
$dev_core = strpos(\Drupal::VERSION, '-dev') !== FALSE;
/** @var \Drupal\Core\Config\ImmutableConfig $config */
$config = \Drupal::config('automatic_updates.settings');
if (!$dev_core && $config->get('enable_cron_updates')) {
\Drupal::service('update.manager')->refreshUpdateData();
\Drupal::service('update.processor')->fetchData();
$available = update_get_available(TRUE);
$projects = update_calculate_project_data($available);
$not_recommended_version = $projects['drupal']['status'] !== UpdateManagerInterface::CURRENT;
$security_update = in_array($projects['drupal']['status'], [UpdateManagerInterface::NOT_SECURE, UpdateManagerInterface::REVOKED], TRUE);
$recommended_release = isset($projects['drupal']['releases'][$projects['drupal']['recommended']]) ? $projects['drupal']['releases'][$projects['drupal']['recommended']] : NULL;
$existing_minor_version = explode('.', \Drupal::VERSION, -1);
$recommended_minor_version = explode('.', $recommended_release['version'], -1);
$major_upgrade = $existing_minor_version !== $recommended_minor_version;
if ($major_upgrade) {
foreach (range(1, 30) as $point_version) {
$potential_version = implode('.', array_merge($existing_minor_version, (array) $point_version));
if (isset($available['drupal']['releases'][$potential_version])) {
$recommended_release = $available['drupal']['releases'][$potential_version];
}
else {
break;
}
}
}
// Don't automatically update major version bumps or from/to same version.
if ($not_recommended_version && $projects['drupal']['existing_version'] !== $recommended_release['version']) {
if ($config->get('enable_cron_security_updates')) {
if ($security_update) {
$metadata = new UpdateMetadata('drupal', 'core', \Drupal::VERSION, $recommended_release['version']);
/** @var \Drupal\automatic_updates\Services\UpdateInterface $updater */
$updater = \Drupal::service('automatic_updates.update');
$updater->update($metadata);
}
}
else {
$metadata = new UpdateMetadata('drupal', 'core', \Drupal::VERSION, $recommended_release['version']);
/** @var \Drupal\automatic_updates\Services\UpdateInterface $updater */
$updater = \Drupal::service('automatic_updates.update');
$updater->update($metadata);
}
}
}
$state->set('automatic_updates.cron_last_check', \Drupal::time()->getCurrentTime());
} }
/** /**
* Implements hook_theme(). * Implements hook_modules_installed().
*/ */
function automatic_updates_theme(array $existing, $type, $theme, $path) { function automatic_updates_modules_installed() {
return [ // Run the readiness checkers if needed when any modules are installed in
'automatic_updates_psa_notify' => [ // case they provide readiness checker services.
'variables' => [ /** @var \Drupal\automatic_updates\Validation\ReadinessValidationManager $checker_manager */
'messages' => [], $checker_manager = \Drupal::service('automatic_updates.readiness_validation_manager');
], $checker_manager->runIfNoStoredResults();
],
'automatic_updates_post_update' => [
'variables' => [
'success' => NULL,
'metadata' => NULL,
],
],
];
} }
/** /**
* Implements hook_mail(). * Implements hook_modules_uninstalled().
*/ */
function automatic_updates_mail($key, &$message, $params) { function automatic_updates_modules_uninstalled() {
/** @var \Drupal\Core\Render\RendererInterface $renderer */ // Run the readiness checkers if needed when any modules are uninstalled in
$renderer = \Drupal::service('renderer'); // case they provided readiness checker services.
/** @var \Drupal\automatic_updates\Validation\ReadinessValidationManager $checker_manager */
$message['subject'] = $params['subject']; $checker_manager = \Drupal::service('automatic_updates.readiness_validation_manager');
$message['body'][] = $renderer->render($params['body']); $checker_manager->runIfNoStoredResults();
} }
/** /**
* Helper method to execute console command. * Implements hook_form_FORM_ID_alter() for 'update_manager_update_form'.
*
* @param string $command_argument
* The command argument.
*
* @return \Symfony\Component\Process\Process
* The console command process.
*/ */
function automatic_updates_console_command($command_argument) { function automatic_updates_form_update_manager_update_form_alter(&$form, FormStateInterface $form_state, $form_id) {
$module_path = drupal_get_path('module', 'automatic_updates'); // Remove current message that core updates are not supported with a link to
$command = [ // use this modules form.
(new PhpExecutableFinder())->find(), if (isset($form['manual_updates']['#rows']['drupal']['data']['title'])) {
$module_path . '/scripts/automatic_update_tools', $core_updates_message = t(
$command_argument, '<h2>Core updates required</h2>Drupal core updates are supported by the enabled <a href="@url">Automatic Updates module</a>',
'--script-filename', ['@url' => Url::fromRoute('automatic_updates.update_form')->toString()]
\Drupal::request()->server->get('SCRIPT_FILENAME'), );
'--base-url', $form['manual_updates']['#prefix'] = $core_updates_message;
\Drupal::request()->getBaseUrl(), }
'--base-path',
\Drupal::request()->getBasePath(),
];
$process = new Process($command, (string) \Drupal::root(), NULL, NULL, NULL);
$process->run();
return $process;
} }
automatic_updates.settings: automatic_updates.update_readiness:
path: '/admin/config/automatic_updates' path: '/admin/automatic_updates/readiness'
defaults: defaults:
_form: '\Drupal\automatic_updates\Form\SettingsForm' _controller: '\Drupal\automatic_updates\Controller\ReadinessCheckerController::run'
_title: 'Automatic Updates' _title: 'Update readiness checking'
requirements: requirements:
_permission: 'administer software updates' _permission: 'administer software updates'
options: automatic_updates.update_form:
_admin_route: TRUE path: '/admin/automatic-update'
automatic_updates.update_readiness:
path: '/admin/config/automatic_updates/readiness'
defaults: defaults:
_controller: '\Drupal\automatic_updates\Controller\ReadinessCheckerController::run' _form: '\Drupal\automatic_updates\Form\UpdaterForm'
_title: 'Update readiness checking...' _title: 'Automatic Updates'
requirements: requirements:
_permission: 'administer software updates' _permission: 'administer software updates'
options: options:
_admin_route: TRUE _admin_route: TRUE
automatic_updates.inplace-update: automatic_updates.confirmation_page:
path: '/automatic_updates/in-place-update/{project}/{type}/{from}/{to}' path: '/admin/automatic-update-ready'
defaults: defaults:
_title: 'Update' _form: '\Drupal\automatic_updates\Form\UpdateReady'
_controller: '\Drupal\automatic_updates\Controller\InPlaceUpdateController::update' _title: 'Ready to update'
requirements: requirements:
_permission: 'administer software updates' _permission: 'administer software updates'
_csrf_token: 'TRUE' _access_update_manager: 'TRUE'
options:
no_cache: 'TRUE'
services: services:
logger.channel.automatic_updates: automatic_updates.readiness_validation_manager:
parent: logger.channel_base class: Drupal\automatic_updates\Validation\ReadinessValidationManager
arguments: ['automatic_updates'] arguments: ['@keyvalue.expirable', '@datetime.time', 24]
automatic_updates.psa: automatic_updates.recommender:
class: Drupal\automatic_updates\Services\AutomaticUpdatesPsa class: Drupal\automatic_updates\UpdateRecommender
arguments: arguments: [ '@update.manager', '@update.processor' ]
- '@config.factory' automatic_updates.updater:
- '@cache.default' class: Drupal\automatic_updates\Updater
- '@datetime.time' arguments: [ '@state', '@string_translation','@automatic_updates.beginner', '@automatic_updates.stager', '@automatic_updates.cleaner', '@automatic_updates.committer' , '@file_system', '@event_dispatcher']
- '@http_client' automatic_updates.staged_package_validator:
- '@extension.list.module' class: Drupal\automatic_updates\Validation\StagedProjectsValidation
- '@extension.list.profile' arguments: [ '@string_translation', '@automatic_updates.updater' ]
- '@extension.list.theme'
- '@logger.channel.automatic_updates'
automatic_updates.psa_notify:
class: Drupal\automatic_updates\Services\Notify
arguments:
- '@plugin.manager.mail'
- '@automatic_updates.psa'
- '@config.factory'
- '@language_manager'
- '@state'
- '@datetime.time'
- '@entity_type.manager'
- '@string_translation'
automatic_updates.cron_override:
class: Drupal\automatic_updates\EventSubscriber\CronOverride
tags:
- { name: config.factory.override }
automatic_updates.modified_files:
class: Drupal\automatic_updates\Services\ModifiedFiles
arguments:
- '@logger.channel.automatic_updates'
- '@http_client'
- '@config.factory'
automatic_updates.update:
class: Drupal\automatic_updates\Services\InPlaceUpdate
arguments:
- '@logger.channel.automatic_updates'
- '@plugin.manager.archiver'
- '@config.factory'
- '@file_system'
- '@http_client'
- '@app.root'
plugin.manager.database_update_handler:
class: Drupal\automatic_updates\DatabaseUpdateHandlerPluginManager
parent: default_plugin_manager
automatic_updates.post_update_subscriber:
class: Drupal\automatic_updates\EventSubscriber\PostUpdateSubscriber
arguments:
- '@config.factory'
- '@plugin.manager.mail'
- '@language_manager'
- '@entity_type.manager'
tags: tags:
- { name: event_subscriber } - { name: event_subscriber }
automatic_updates.beginner:
automatic_updates.readiness_checker: class: Drupal\automatic_updates\ComposerStager\Beginner
class: Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManager
arguments: arguments:
- '@keyvalue' [ '@automatic_updates.file_copier', '@automatic_updates.file_system' ]
- '@config.factory' automatic_updates.stager:
tags: class: PhpTuf\ComposerStager\Domain\Stager
- { name: service_collector, tag: readiness_checker, call: addChecker }
# Readiness checkers.
automatic_updates.readonly_checker:
class: Drupal\automatic_updates\ReadinessChecker\ReadOnlyFilesystem
arguments: arguments:
- '@app.root' [ '@automatic_updates.composer_runner', '@automatic_updates.file_system' ]
- '@logger.channel.automatic_updates' automatic_updates.cleaner:
- '@file_system' class: PhpTuf\ComposerStager\Domain\Cleaner
tags:
- { name: readiness_checker, priority: 100, category: error }
automatic_updates.disk_space_checker:
class: Drupal\automatic_updates\ReadinessChecker\DiskSpace
arguments: arguments:
- '@app.root' [ '@automatic_updates.file_system' ]
tags: automatic_updates.committer:
- { name: readiness_checker, category: error} class: PhpTuf\ComposerStager\Domain\Committer
automatic_updates.modified_files_checker:
class: Drupal\automatic_updates\ReadinessChecker\ModifiedFiles
arguments: arguments:
- '@automatic_updates.modified_files' [ '@automatic_updates.file_copier', '@automatic_updates.file_system' ]
- '@extension.list.module' automatic_updates.composer_runner:
- '@extension.list.profile' class: PhpTuf\ComposerStager\Infrastructure\Process\Runner\ComposerRunner
- '@extension.list.theme'
tags:
- { name: readiness_checker, category: warning}
automatic_updates.file_ownership:
class: Drupal\automatic_updates\ReadinessChecker\FileOwnership
arguments: arguments:
- '@app.root' [ '@automatic_updates.exec_finder', '@automatic_updates.process_factory' ]
tags: automatic_updates.file_copier.factory:
- { name: readiness_checker, category: warning} class: PhpTuf\ComposerStager\Infrastructure\Process\FileCopier\FileCopierFactory
automatic_updates.pending_db_updates:
class: Drupal\automatic_updates\ReadinessChecker\PendingDbUpdates
arguments: arguments:
- '@update.post_update_registry' - '@automatic_updates.symfony_exec_finder'
tags: - '@automatic_updates.file_copier.rsync'
- { name: readiness_checker, category: error} - '@automatic_updates.file_copier.symfony'
automatic_updates.missing_project_info: automatic_updates.file_copier.rsync:
class: Drupal\automatic_updates\ReadinessChecker\MissingProjectInfo class: PhpTuf\ComposerStager\Infrastructure\Process\FileCopier\RsyncFileCopier
arguments: arguments:
- '@extension.list.module' - '@automatic_updates.file_system'
- '@extension.list.profile' - '@automatic_updates.rsync'
- '@extension.list.theme' automatic_updates.file_copier.symfony:
tags: class: PhpTuf\ComposerStager\Infrastructure\Process\FileCopier\SymfonyFileCopier
- { name: readiness_checker, category: warning}
automatic_updates.opcode_cache:
class: Drupal\automatic_updates\ReadinessChecker\OpcodeCache
tags:
- { name: readiness_checker, category: error}
automatic_updates.php_sapi:
class: Drupal\automatic_updates\ReadinessChecker\PhpSapi
arguments: arguments:
- '@state' - '@automatic_updates.symfony_file_system'
tags: - '@automatic_updates.finder'
- { name: readiness_checker, category: warning} automatic_updates.finder:
automatic_updates.cron_frequency: class: Symfony\Component\Finder\Finder
class: Drupal\automatic_updates\ReadinessChecker\CronFrequency public: false
automatic_updates.file_copier:
class: PhpTuf\ComposerStager\Infrastructure\Process\FileCopier\FileCopierInterface
factory: ['@automatic_updates.file_copier.factory', 'create']
automatic_updates.file_system:
class: PhpTuf\ComposerStager\Infrastructure\Filesystem\Filesystem
arguments: arguments:
- '@config.factory' [ '@automatic_updates.symfony_file_system' ]
- '@module_handler' automatic_updates.symfony_file_system:
tags: class: Symfony\Component\Filesystem\Filesystem
- { name: readiness_checker, category: warning} automatic_updates.symfony_exec_finder:
automatic_updates.vendor: class: Symfony\Component\Process\ExecutableFinder
class: Drupal\automatic_updates\ReadinessChecker\Vendor automatic_updates.rsync:
class: PhpTuf\ComposerStager\Infrastructure\Process\Runner\RsyncRunner
arguments: arguments:
- '@app.root' [ '@automatic_updates.exec_finder', '@automatic_updates.process_factory' ]
tags: automatic_updates.exec_finder:
- { name: readiness_checker, category: error} class: PhpTuf\ComposerStager\Infrastructure\Process\ExecutableFinder
arguments:
[ '@automatic_updates.symfony_exec_finder' ]
automatic_updates.process_factory:
class: Drupal\automatic_updates\ComposerStager\ProcessFactory
name: 'Automatic Updates 9.3.x shim'
type: module
description: 'Shim module to allow using improvements to the Update module in 9.3.x'
core_version_requirement: ~9.2
package: 'Security'
<?php
namespace Drupal\automatic_updates_9_3_shim;
use Symfony\Component\Validator\Constraints\Choice;
use Symfony\Component\Validator\Constraints\Collection;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Optional;
use Symfony\Component\Validator\Constraints\Type;
use Symfony\Component\Validator\Validation;
/**
* Provides a project release value object.
*/
final class ProjectRelease {
/**
* Whether the release is compatible with the site's Drupal core version.
*
* @var bool
*/
private $coreCompatible;
/**
* The core compatibility message or NULL if not set.
*
* @var string|null
*/
private $coreCompatibilityMessage;
/**
* The download URL or NULL if none is available.
*
* @var string|null
*/
private $downloadUrl;
/**
* The URL for the release.
*
* @var string
*/
private $releaseUrl;
/**
* The release types or NULL if not set.
*
* @var string[]|null
*/
private $releaseTypes;
/**
* Whether the release is published.
*
* @var bool
*/
private $published;
/**
* The release version.
*
* @var string
*/
private $version;
/**
* The release date as a Unix timestamp or NULL if no date was set.
*
* @var int|null
*/
private $date;
/**
* Constructs a ProjectRelease object.
*
* @param bool $published
* Whether the release is published.
* @param string $version
* The release version.
* @param string $release_url
* The URL for the release.
* @param string[]|null $release_types
* The release types or NULL if not set.
* @param bool|null $core_compatible
* Whether the release is compatible with the site's version of Drupal core.
* @param string|null $core_compatibility_message
* The core compatibility message or NULL if not set.
* @param string|null $download_url
* The download URL or NULL if not available.
* @param int|null $date
* The release date in Unix timestamp format.
*/
private function __construct(bool $published, string $version, string $release_url, ?array $release_types, ?bool $core_compatible, ?string $core_compatibility_message, ?string $download_url, ?int $date) {
$this->published = $published;
$this->version = $version;
$this->releaseUrl = $release_url;
$this->releaseTypes = $release_types;
$this->coreCompatible = $core_compatible;
$this->coreCompatibilityMessage = $core_compatibility_message;
$this->downloadUrl = $download_url;
$this->date = $date;
}
/**
* Creates a ProjectRelease instance from an array.
*
* @param array $release_data
* The project release data as returned by update_get_available().
*
* @return \Drupal\update\ProjectRelease
* The ProjectRelease instance.
*
* @throws \UnexpectedValueException
* Thrown if project release data is not valid.
*
* @see \update_get_available()
*/
public static function createFromArray(array $release_data): ProjectRelease {
static::validateReleaseData($release_data);
return new ProjectRelease(
$release_data['status'] === 'published',
$release_data['version'],
$release_data['release_link'],
$release_data['terms']['Release type'] ?? NULL,
$release_data['core_compatible'] ?? NULL,
$release_data['core_compatibility_message'] ?? NULL,
$release_data['download_link'] ?? NULL,
$release_data['date'] ?? NULL
);
}
/**
* Validates the project release data.
*
* @param array $data
* The project release data.
*
* @throws \UnexpectedValueException
* Thrown if project release data is not valid.
*/
private static function validateReleaseData(array $data): void {
$not_blank_constraints = [
new Type('string'),
new NotBlank(),
];
$collection_constraint = new Collection([
'fields' => [
'version' => $not_blank_constraints,
'date' => new Optional([new Type('numeric')]),
'core_compatible' => new Optional([new Type('boolean')]),
'core_compatibility_message' => new Optional($not_blank_constraints),
'status' => new Choice(['published', 'unpublished']),
'download_link' => new Optional($not_blank_constraints),
'release_link' => $not_blank_constraints,
'terms' => new Optional([
new Type('array'),
new Collection([
'Release type' => new Optional([
new Type('array'),
]),
]),
]),
],
'allowExtraFields' => TRUE,
]);
$violations = Validation::createValidator()->validate($data, $collection_constraint);
if (count($violations)) {
foreach ($violations as $violation) {
$violation_messages[] = "Field " . $violation->getPropertyPath() . ": " . $violation->getMessage();
}
throw new \UnexpectedValueException('Malformed release data: ' . implode(",\n", $violation_messages));
}
}
/**
* Gets the project version.
*
* @return string
* The project version.
*/
public function getVersion(): string {
return $this->version;
}
/**
* Gets the release date if set.
*
* @return int|null
* The date of the release or null if no date is available.
*/
public function getDate(): ?int {
return $this->date;
}
/**
* Determines if the release is a security release.
*
* @return bool
* TRUE if the release is security release, or FALSE otherwise.
*/
public function isSecurityRelease(): bool {
return $this->isReleaseType('Security update');
}
/**
* Determines if the release is unsupported.
*
* @return bool
* TRUE if the release is unsupported, or FALSE otherwise.
*/
public function isUnsupported(): bool {
return $this->isReleaseType('Unsupported');
}
/**
* Determines if the release is insecure.
*
* @return bool
* TRUE if the release is insecure, or FALSE otherwise.
*/
public function isInsecure(): bool {
return $this->isReleaseType('Insecure');
}
/**
* Determines if the release is matches a type.
*
* @param string $type
* The release type.
*
* @return bool
* TRUE if the release matches the type, or FALSE otherwise.
*/
private function isReleaseType(string $type): bool {
return $this->releaseTypes && in_array($type, $this->releaseTypes, TRUE);
}
/**
* Determines if the release is published.
*
* @return bool
* TRUE if the release is published, or FALSE otherwise.
*/
public function isPublished(): bool {
return $this->published;
}
/**
* Determines whether release is compatible the site's version of Drupal core.
*
* @return bool|null
* Whether the release is compatible or NULL if no data is set.
*/
public function isCoreCompatible(): ?bool {
return $this->coreCompatible;
}
/**
* Gets the core compatibility message for the site's version of Drupal core.
*
* @return string|null
* The core compatibility message or NULL if none is available.
*/
public function getCoreCompatibilityMessage(): ?string {
return $this->coreCompatibilityMessage;
}
/**
* Gets the download URL of the release.
*
* @return string|null
* The download URL or NULL if none is available.
*/
public function getDownloadUrl(): ?string {
return $this->downloadUrl;
}
/**
* Gets the URL of the release.
*
* @return string
* The URL of the release.
*/
public function getReleaseUrl(): string {
return $this->releaseUrl;
}
}
...@@ -11,13 +11,18 @@ ...@@ -11,13 +11,18 @@
"source": "http://cgit.drupalcode.org/automatic_updates" "source": "http://cgit.drupalcode.org/automatic_updates"
}, },
"require": { "require": {
"php": "^7.3",
"ext-json": "*", "ext-json": "*",
"ext-zip": "*", "php-tuf/composer-stager": "0.1.1"
"drupal/php-signify": "^1.0",
"ocramius/package-versions": "^1.5.0"
}, },
"config": { "config": {
"sort-packages": true "platform": {
} "php": "7.3.0"
}
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/php-tuf/composer-stager"
}
]
} }
psa_endpoint: 'https://updates.drupal.org/psa.json'
enable_psa: true
notify: true
check_frequency: 43200
enable_readiness_checks: true
hashes_uri: 'https://updates.drupal.org/release-hashes'
ignored_paths: "modules/*\nthemes/*\nprofiles/*"
download_uri: 'https://www.drupal.org/in-place-updates'
enable_cron_updates: false
enable_cron_security_updates: false
database_update_handling:
- maintenance_mode_activate
- execute_updates
- maintenance_mode_disactivate
langcode: en
status: true
dependencies:
config:
- system.menu.admin
module:
- dblog
- user
id: automatic_updates_log
label: 'Automatic updates log'
module: views
description: 'Lists the files which have been updated by the Automatic updates module.'
tag: ''
base_table: watchdog
base_field: wid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'administer software updates'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: true
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: false
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: full
options:
items_per_page: 100
offset: 0
id: 0
total_pages: null
tags:
previous: ‹‹
next: ››
first: '« First'
last: 'Last »'
expose:
items_per_page: true
items_per_page_label: 'Items per page'
items_per_page_options: '100, 250, 500, 1000'
items_per_page_options_all: true
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
quantity: 9
style:
type: default
options:
grouping:
-
field: timestamp
rendered: true
rendered_strip: false
row_class: ''
default_row_class: false
row:
type: fields
options:
default_field_elements: false
inline: { }
separator: ''
hide_empty: false
fields:
timestamp:
id: timestamp
table: watchdog
field: timestamp
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: true
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
date_format: short
custom_date_format: ''
timezone: ''
plugin_id: date
message:
id: message
table: watchdog
field: message
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
replace_variables: true
plugin_id: dblog_message
filters:
type:
id: type
table: watchdog
field: type
relationship: none
group_type: group
admin_label: ''
operator: in
value:
automatic_updates: automatic_updates
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
reduce: false
operator_limit_selection: false
operator_list: { }
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
plugin_id: dblog_types
severity:
id: severity
table: watchdog
field: severity
relationship: none
group_type: group
admin_label: ''
operator: in
value:
6: '6'
group: 1
exposed: true
expose:
operator_id: severity_op
label: 'Severity level'
description: ''
use_operator: false
operator: severity_op
identifier: severity
required: false
remember: false
multiple: true
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
plugin_id: in_operator
sorts:
wid:
id: wid
table: watchdog
field: wid
relationship: none
group_type: group
admin_label: ''
order: DESC
exposed: false
expose:
label: ''
plugin_id: standard
title: 'Automatic updates log'
header: { }
footer: { }
empty:
area:
id: area
table: views
field: area
relationship: none
group_type: group
admin_label: ''
empty: true
tokenize: false
content:
value: '<p>No automatic update log entries.</p>'
format: basic_html
plugin_id: text
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_interface'
- url
- url.query_args
- user.permissions
tags: { }
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: admin/reports/automatic_updates_log
menu:
type: normal
title: 'Automatic updates log'
description: ''
expanded: false
parent: system.admin_reports
weight: -10
context: '0'
menu_name: admin
cache_metadata:
max-age: -1
contexts:
- 'languages:language_interface'
- url
- url.query_args
- user.permissions
tags: { }
automatic_updates.settings:
type: config_object
label: 'Automatic updates settings'
mapping:
psa_endpoint:
type: string
label: 'Endpoint URI for PSAs'
enable_psa:
type: boolean
label: 'Enable PSA notices'
notify:
type: boolean
label: 'Notify when PSAs are available'
check_frequency:
type: integer
label: 'Frequency to check for PSAs, defaults to 12 hours'
enable_readiness_checks:
type: boolean
label: 'Enable readiness checks'
hashes_uri:
type: string
label: 'Endpoint URI for file hashes'
ignored_paths:
type: string
label: 'List of files paths to ignore when running readiness checks'
download_uri:
type: string
label: 'URI for downloading in-place update assets'
enable_cron_updates:
type: boolean
label: 'Enable automatic updates via cron'
enable_cron_security_updates:
type: boolean
label: 'Enable automatic updates for security releases via cron'
database_update_handling:
type: sequence
label: 'Database update handling'
sequence:
type: string
label: 'Tagged service to handle database updates'
# Learn to make one for your own drupal.org project:
# https://www.drupal.org/drupalorg/docs/drupal-ci/customizing-drupalci-testing
build:
assessment:
validate_codebase:
phplint:
phpcs:
# phpcs will use core's specified version of Coder.
sniff-all-files: true
halt-on-fail: true
testing:
# run_tests task is executed several times in order of performance speeds.
# halt-on-fail can be set on the run_tests tasks in order to fail fast.
# suppress-deprecations is false in order to be alerted to usages of
# deprecated code.
run_tests.standard:
types: 'PHPUnit-Unit,PHPUnit-Kernel,PHPUnit-Functional,PHPUnit-Build'
testgroups: '--all'
suppress-deprecations: false
halt-on-fail: false
<ruleset name="automatic_updates">
<description>Must ignore vendor folder, but otherwise default Drupal code standards.</description>
<file>.</file>
<exclude-pattern>*/vendor/*$</exclude-pattern>
<arg name="extensions" value="php,module,inc,install,test,profile,theme,css,info,txt,md"/>
<rule ref="Drupal"/>
</ruleset>
#!/usr/bin/env php
<?php
/**
* @file
* Provides helper commands for automatic updates.
*
* This must be a separate application so class caches aren't an issue during
* in place updates.
*/
use Drupal\automatic_updates\Command\CacheRebuild;
use Drupal\automatic_updates\Command\DatabaseUpdate;
use Drupal\automatic_updates\Command\DatabaseUpdateStatus;
use Symfony\Component\Console\Application;
if (PHP_SAPI !== 'cli') {
return;
}
// Set up autoloader
$loader = false;
if (file_exists($autoloader = __DIR__ . '/../../../../autoload.php')
|| file_exists($autoloader = __DIR__ . '/../../../../../autoload.php')
|| file_exists($autoloader = __DIR__ . '/../../../autoload.php')
) {
/** @var \Composer\Autoload\ClassLoader $loader */
$loader = require_once $autoloader;
// Drupal's autoloader doesn't bootstrap this module's classes yet. Do so
// manually.
$loader->addPsr4('Drupal\\automatic_updates\\', __DIR__ . '/../src');
}
else {
throw new \RuntimeException('Could not locate autoload.php; __DIR__ is ' . __DIR__);
}
$application = new Application('automatic_update_tools', 'stable');
$application->add(new CacheRebuild($loader));
$application->add(new DatabaseUpdate($loader));
$application->add(new DatabaseUpdateStatus($loader));
$application->run();
<?php
namespace Drupal\automatic_updates\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a DatabaseUpdateHandler annotation object.
*
* Plugin Namespace: Plugin\DatabaseUpdateHandler.
*
* For a working example, see
* \Drupal\automatic_updates\Plugin\DatabaseUpdateHandler\MaintenanceMode.
*
* @see \Drupal\automatic_updates\DatabaseUpdateHandlerInterface
* @see \Drupal\automatic_updates\DatabaseUpdateHandlerPluginBase
* @see \Drupal\automatic_updates\DatabaseUpdateHandlerPluginManager
* @see hook_database_update_handler_plugin_info_alter()
* @see plugin_api
*
* @Annotation
*/
final class DatabaseUpdateHandler extends Plugin {
/**
* The ID of the handler, should match the service name.
*
* @var string
*/
public $id;
/**
* The name of the handler.
*
* @var string
*/
public $label;
}
<?php
namespace Drupal\automatic_updates;
/**
* Defines events for the automatic_updates module.
*
* These events allow listeners to validate updates at various points in the
* update process. Listeners to these events should add validation results via
* \Drupal\automatic_updates\Event\UpdateEvent::addValidationResult() if
* necessary. Only error level validation results will stop an update from
* continuing.
*
* @see \Drupal\automatic_updates\Event\UpdateEvent
* @see \Drupal\automatic_updates\Validation\ValidationResult
*/
final class AutomaticUpdatesEvents {
/**
* Name of the event fired when checking if the site could perform an update.
*
* An update is not actually being started when this event is being fired. It
* should be used to notify site admins if the site is in a state which will
* not allow automatic updates to succeed.
*
* This event should only be dispatched from ReadinessValidationManager to
* allow caching of the results.
*
* @Event
*
* @see \Drupal\automatic_updates\Validation\ReadinessValidationManager
*
* @var string
*/
const READINESS_CHECK = 'automatic_updates.readiness_check';
/**
* Name of the event fired when an automatic update is starting.
*
* This event is fired before any files are staged. Validation results added
* by subscribers are not cached.
*
* @Event
*
* @var string
*/
const PRE_START = 'automatic_updates.pre_start';
/**
* Name of the event fired when an automatic update is about to be committed.
*
* Validation results added by subscribers are not cached.
*
* @Event
*
* @var string
*/
const PRE_COMMIT = 'automatic_updates.pre_commit';
}
<?php
namespace Drupal\automatic_updates;
use Drupal\automatic_updates\Exception\UpdateException;
use Drupal\Core\Url;
use Symfony\Component\HttpFoundation\RedirectResponse;
/**
* A batch processor for updates.
*/
class BatchProcessor {
/**
* Get the updater service.
*
* @return \Drupal\automatic_updates\Updater
* The updater service.
*/
protected static function getUpdater(): Updater {
return \Drupal::service('automatic_updates.updater');
}
/**
* Records messages from a throwable, then re-throws it.
*
* @param \Throwable $error
* The caught exception.
* @param array $context
* The current context of the batch job.
*
* @throws \Throwable
* The caught exception, which will always be re-thrown once its messages
* have been recorded.
*/
protected static function handleException(\Throwable $error, array &$context): void {
$error_messages = [
$error->getMessage(),
];
if ($error instanceof UpdateException) {
foreach ($error->getValidationResults() as $result) {
$messages = $result->getMessages();
if (count($messages) > 1) {
array_unshift($messages, $result->getSummary());
}
$error_messages = array_merge($error_messages, $messages);
}
}
foreach ($error_messages as $error_message) {
$context['results']['errors'][] = $error_message;
}
throw $error;
}
/**
* Calls the updater's begin() method.
*
* @param array $context
* The current context of the batch job.
*
* @see \Drupal\automatic_updates\Updater::begin()
*/
public static function begin(array &$context): void {
try {
static::getUpdater()->begin();
}
catch (\Throwable $e) {
static::handleException($e, $context);
}
}
/**
* Calls the updater's stageVersions() method.
*
* @param string[] $project_versions
* The project versions to be staged in the update, keyed by package name.
* @param array $context
* The current context of the batch job.
*
* @see \Drupal\automatic_updates\Updater::stageVersions()
*/
public static function stageProjectVersions(array $project_versions, array &$context): void {
try {
static::getUpdater()->stageVersions($project_versions);
}
catch (\Throwable $e) {
static::handleException($e, $context);
}
}
/**
* Calls the updater's validateStaged() method.
*
* @see \Drupal\automatic_updates\Updater::validateStaged()
*/
public static function validateStaged() {
static::getUpdater()->validateStaged();
}
/**
* Finishes the batch job.
*
* @param bool $success
* Indicate that the batch API tasks were all completed successfully.
* @param array $results
* An array of all the results.
* @param array $operations
* A list of the operations that had not been completed by the batch API.
*/
public static function finish(bool $success, array $results, array $operations): ?RedirectResponse {
if ($success) {
return new RedirectResponse(Url::fromRoute('automatic_updates.confirmation_page', [], ['absolute' => TRUE])->toString());
}
if (isset($results['errors'])) {
foreach ($results['errors'] as $error) {
\Drupal::messenger()->addError($error);
}
}
else {
\Drupal::messenger()->addError("Update error");
}
return NULL;
}
}
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