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

Issue #3039120 by heddn, catch, larowlan: Create initial feature to display...

Issue #3039120 by heddn, catch, larowlan: Create initial feature to display relevant PSA data in Drupal
parent 59c35735
No related branches found
No related tags found
No related merge requests found
name: 'Automatic Updates'
type: module
description: 'Drupal Automatic Updates'
core: 8.7
package: 'Maintenance'
core: 8.x
package: 'Security'
<?php
/**
* @file
* Contains automatic_updates.module..
*/
/**
* Implements hook_page_top().
*/
function automatic_updates_page_top(array &$page_top) {
/** @var \Drupal\Core\Routing\AdminContext $admin_context */
$admin_context = \Drupal::service('router.admin_context');
$route_match = \Drupal::routeMatch();
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');
foreach ($psa->getPublicServiceMessages() as $psa) {
\Drupal::messenger()->addError($psa);
}
}
}
services:
logger.channel.automatic_updates:
parent: logger.channel_base
arguments: ['automatic_updates']
automatic_updates.psa:
class: Drupal\automatic_updates\Services\AutomaticUpdatesPsa
arguments:
- '@config.factory'
- '@cache.default'
- '@datetime.time'
- '@http_client'
- '@extension.list.module'
- '@extension.list.profile'
- '@extension.list.theme'
- '@logger.channel.automatic_updates'
# Public service announcement URI endpoint.
# TODO: Update to correct end point once it is available. See
# https://www.drupal.org/project/automatic_updates/issues/3045273
psa_endpoint: 'http://localhost/automatic_updates/test-json'
automatic_updates.settings:
type: config_object
label: 'Automatic updates settings'
mapping:
psa_endpoint:
type: string
label: 'Endpoint URI for PSAs'
<?php
namespace Drupal\automatic_updates\Services;
use Composer\Semver\VersionParser;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Version\Constraint;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Extension\ExtensionList;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\TransferException;
use Psr\Log\LoggerInterface;
/**
* Class AutomaticUpdatesPsa.
*/
class AutomaticUpdatesPsa implements AutomaticUpdatesPsaInterface {
use StringTranslationTrait;
use DependencySerializationTrait;
/**
* Module's configuration.
*
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $config;
/**
* The http client.
*
* @var \GuzzleHttp\Client
*/
protected $httpClient;
/**
* The cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* The time service.
*
* @var \Drupal\Component\Datetime\TimeInterface
*/
protected $time;
/**
* The module extension list.
*
* @var \Drupal\Core\Extension\ExtensionList
*/
protected $module;
/**
* The profile extension list.
*
* @var \Drupal\Core\Extension\ExtensionList
*/
protected $profile;
/**
* The theme extension list.
*
* @var \Drupal\Core\Extension\ExtensionList
*/
protected $theme;
/**
* The logger.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* AutomaticUpdatesPsa constructor.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
* @param \GuzzleHttp\Client $client
* The HTTP client.
* @param \Drupal\Core\Extension\ExtensionList $module
* The module extension list.
* @param \Drupal\Core\Extension\ExtensionList $profile
* The profile extension list.
* @param \Drupal\Core\Extension\ExtensionList $theme
* The theme extension list.
* @param \Psr\Log\LoggerInterface $logger
* The logger.
*/
public function __construct(ConfigFactoryInterface $config_factory, CacheBackendInterface $cache, TimeInterface $time, Client $client, ExtensionList $module, ExtensionList $profile, ExtensionList $theme, LoggerInterface $logger) {
$this->config = $config_factory->get('automatic_updates.settings');
$this->cache = $cache;
$this->time = $time;
$this->httpClient = $client;
$this->module = $module;
$this->profile = $profile;
$this->theme = $theme;
$this->logger = $logger;
}
/**
* {@inheritdoc}
*/
public function getPublicServiceMessages() {
$messages = [];
if ($cache = $this->cache->get('automatic_updates_psa')) {
$response = $cache->data;
}
else {
try {
$psa_endpoint = $this->config->get('psa_endpoint');
$response = $this->httpClient->get($psa_endpoint)
->getBody()
->getContents();
// Set response in cache for 12 hours.
$this->cache->set('automatic_updates_psa', $response, $this->time->getCurrentTime() + 3600 * 12);
}
catch (TransferException $exception) {
$this->logger->error($exception->getMessage());
return [$this->t('Drupal PSA endpoint :url is unreachable.', [':url' => $psa_endpoint])];
}
}
try {
$json_payload = json_decode($response);
foreach ($json_payload as $json) {
if ($json->project === 'core') {
$this->coreParser($messages, $json);
}
else {
$this->contribParser($messages, $json);
}
}
}
catch (\UnexpectedValueException $exception) {
$this->logger->error($exception->getMessage());
$messages[] = $this->t('Drupal PSA endpoint service is malformed.');
}
return $messages;
}
/**
* Parse core project JSON version strings.
*
* @param array $messages
* The messages array.
* @param object $json
* The JSON object.
*/
protected function coreParser(array &$messages, $json) {
$parser = new VersionParser();
array_walk($json->secure_versions, function (&$version) {
$version = '<' . $version;
});
$version_string = implode('||', $json->secure_versions);
$psa_constraint = $parser->parseConstraints($version_string);
$core_constraint = $parser->parseConstraints(\Drupal::VERSION);
if ($psa_constraint->matches($core_constraint)) {
$messages[] = $this->t('Drupal Core PSA: <a href=":url">:message</a>', [
':message' => $json->title,
':url' => $json->link,
]);
}
}
/**
* Parse contrib project JSON version strings.
*
* @param array $messages
* The messages array.
* @param object $json
* The JSON object.
*/
protected function contribParser(array &$messages, $json) {
$extension_list = $json->type;
if (!property_exists($this, $extension_list)) {
$this->logger->error('Extension list of type "%extension" does not exist.', ['%extension' => $extension_list]);
return;
}
foreach ($json->extensions as $extension_name) {
if ($this->{$extension_list}->exists($extension_name)) {
$extension = $this->{$extension_list}->getAllAvailableInfo()[$extension_name];
if (empty($extension['version'])) {
continue;
}
$this->contribMessage($messages, $json, $extension['version']);
}
}
}
/**
* Add a contrib message PSA, if appropriate.
*
* @param array $messages
* The messages array.
* @param object $json
* The JSON object.
* @param string $extension_version
* The extension version.
*/
protected function contribMessage(array &$messages, $json, $extension_version) {
array_walk($json->secure_versions, function (&$version) {
$version = \Drupal::CORE_COMPATIBILITY . '-' . $version;
});
$version_string = implode('||', $json->secure_versions);
$constraint = new Constraint($extension_version, \Drupal::CORE_COMPATIBILITY);
if (!$constraint->isCompatible($version_string)) {
$messages[] = $this->t('Drupal Contrib Project PSA: <a href=":url">:message</a>', [
':message' => $json->title,
':url' => $json->link,
]);
}
}
}
<?php
namespace Drupal\automatic_updates\Services;
/**
* Interface AutomaticUpdatesPsaInterface.
*/
interface AutomaticUpdatesPsaInterface {
/**
* Get public service messages.
*
* @return array
* A return of translatable strings.
*/
public function getPublicServiceMessages();
}
......@@ -20,17 +20,66 @@ class JsonTestController extends ControllerBase {
$feed[] = [
'title' => 'Critical Release - PSA-2019-02-19',
'link' => 'https://www.drupal.org/psa-2019-02-19',
'project' => 'drupal/core',
'modules' => ['forum', 'node'],
'version' => '>=8.0.0 <8.6.10 || >=8.0.0 <8.5.11',
'project' => 'core',
'extensions' => [],
'type' => 'module',
'secure_versions' => [
'7.99',
'8.10.99',
'8.9.99',
'8.8.99',
'8.7.99',
'8.6.99',
'8.5.99',
],
'pubDate' => 'Tue, 19 Feb 2019 14:11:01 +0000',
];
$feed[] = [
'title' => 'Critical Release - PSA-Fictional PSA',
'link' => 'https://www.drupal.org/psa-fictional-psa',
'project' => 'drupal/core',
'modules' => ['system'],
'version' => '>=8.6.10 || >=8.5.11',
'title' => 'Critical Release - PSA-Really Old',
'link' => 'https://www.drupal.org/psa',
'project' => 'core',
'extensions' => [],
'type' => 'module',
'secure_versions' => [
'7.0',
'8.4.0',
],
'pubDate' => 'Tue, 19 Feb 2019 14:11:01 +0000',
];
$feed[] = [
'title' => 'Node - Moderately critical - Access bypass - SA-CONTRIB-2019',
'link' => 'https://www.drupal.org/sa-contrib-2019',
'project' => 'node',
'extensions' => ['node'],
'type' => 'module',
'secure_versions' => ['8.10.99'],
'pubDate' => 'Tue, 19 Mar 2019 12:50:00 +0000',
];
$feed[] = [
'title' => 'Standard - Moderately critical - Access bypass - SA-CONTRIB-2019',
'link' => 'https://www.drupal.org/sa-contrib-2019',
'project' => 'Standard Install Profile',
'extensions' => ['standard'],
'type' => 'profile',
'secure_versions' => ['8.10.99'],
'pubDate' => 'Tue, 19 Mar 2019 12:50:00 +0000',
];
$feed[] = [
'title' => 'Seven - Moderately critical - Access bypass - SA-CONTRIB-2019',
'link' => 'https://www.drupal.org/sa-contrib-2019',
'project' => 'seven',
'extensions' => ['seven'],
'type' => 'theme',
'secure_versions' => ['8.10.99'],
'pubDate' => 'Tue, 19 Mar 2019 12:50:00 +0000',
];
$feed[] = [
'title' => 'Foobar - Moderately critical - Access bypass - SA-CONTRIB-2019',
'link' => 'https://www.drupal.org/sa-contrib-2019',
'project' => 'foobar',
'extensions' => ['foobar'],
'type' => 'foobar',
'secure_versions' => ['8.10.99'],
'pubDate' => 'Tue, 19 Mar 2019 12:50:00 +0000',
];
return new JsonResponse($feed);
......
......@@ -5,3 +5,10 @@ test_automatic_updates.json_test_controller:
_title: 'JSON'
requirements:
_access: 'TRUE'
test_automatic_updates.json_test_denied_controller:
path: '/automatic_updates/test-json-denied'
defaults:
_controller: '\Drupal\test_automatic_updates\Controller\JsonTestController::json'
_title: 'JSON'
requirements:
_access: 'FALSE'
......@@ -2,7 +2,6 @@
namespace Drupal\Tests\automatic_updates\Functional;
use Composer\Semver\VersionParser;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
......@@ -35,24 +34,40 @@ class AutomaticUpdatesTest extends BrowserTestBase {
*/
protected function setUp() {
parent::setUp();
$this->user = $this->drupalCreateUser(['administer site configuration']);
$this->user = $this->drupalCreateUser([
'administer site configuration',
'access administration pages',
]);
$this->drupalLogin($this->user);
}
/**
* Tests that the JSON is parsable.
* Tests that a PSA is displayed.
*/
public function testJson() {
$this->drupalGet(Url::fromRoute('test_automatic_updates.json_test_controller'));
$json = json_decode($this->getSession()->getPage()->getContent(), TRUE);
$this->assertEquals($json[0]['title'], 'Critical Release - PSA-2019-02-19');
$parser = new VersionParser();
$constraint = $parser->parseConstraints($json[0]['version']);
$core_constraint = $parser->parseConstraints(\Drupal::VERSION);
$this->assertFALSE($constraint->matches($core_constraint));
$constraint = $parser->parseConstraints($json[1]['version']);
$core_constraint = $parser->parseConstraints(\Drupal::VERSION);
$this->assertTRUE($constraint->matches($core_constraint));
public function testPsa() {
$end_point = $this->buildUrl(Url::fromRoute('test_automatic_updates.json_test_controller'));
$this->config('automatic_updates.settings')
->set('psa_endpoint', $end_point)
->save();
$this->drupalGet(Url::fromRoute('system.admin'));
$this->assertSession()->pageTextContains('Drupal Core PSA: Critical Release - PSA-2019-02-19');
$this->assertSession()->pageTextNotContains('Drupal Core PSA: Critical Release - PSA-Really Old');
$this->assertSession()->pageTextContains('Drupal Contrib Project PSA: Node - Moderately critical - Access bypass - SA-CONTRIB-2019');
$this->assertSession()->pageTextContains('Drupal Contrib Project PSA: Seven - Moderately critical - Access bypass - SA-CONTRIB-2019');
$this->assertSession()->pageTextContains('Drupal Contrib Project PSA: Standard - Moderately critical - Access bypass - SA-CONTRIB-2019');
// Test cache.
$end_point = $this->buildUrl(Url::fromRoute('test_automatic_updates.json_test_denied_controller'));
$this->config('automatic_updates.settings')
->set('psa_endpoint', $end_point)
->save();
$this->drupalGet(Url::fromRoute('system.admin'));
$this->assertSession()->pageTextContains('Drupal Core PSA: Critical Release - PSA-2019-02-19');
// Test transmit errors with JSON endpoint.
drupal_flush_all_caches();
$this->drupalGet(Url::fromRoute('system.admin'));
$this->assertSession()->pageTextContains('Drupal PSA endpoint http://localhost/automatic_updates/test-json-denied is unreachable.');
}
}
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