From 650696ac148ce1be7d07ffeb455aadbdcbed5893 Mon Sep 17 00:00:00 2001
From: tatarbj <tatarbj@649590.no-reply.drupal.org>
Date: Wed, 12 Jun 2019 08:30:37 -0500
Subject: [PATCH] Issue #3045273 by tatarbj, heddn, mlhess, catch: Add real
 endpoint after drupal.org provides a live feed

---
 automatic_updates.info.yml                    |   2 +-
 automatic_updates.install                     |   4 +-
 automatic_updates.links.menu.yml              |   2 +-
 automatic_updates.module                      |   2 +-
 src/Form/SettingsForm.php                     |   4 +-
 src/Services/AutomaticUpdatesPsa.php          | 109 ++++++++++--------
 .../automatic-updates-psa-notify.html.twig    |   3 +-
 .../src/Controller/JsonTestController.php     |  83 ++++++++-----
 tests/src/Functional/AutomaticUpdatesTest.php |  11 +-
 tests/src/Functional/NotifyTest.php           |   6 +-
 10 files changed, 129 insertions(+), 97 deletions(-)

diff --git a/automatic_updates.info.yml b/automatic_updates.info.yml
index fdcd5bb618..79f8401cb5 100644
--- a/automatic_updates.info.yml
+++ b/automatic_updates.info.yml
@@ -1,6 +1,6 @@
 name: 'Automatic Updates'
 type: module
-description: 'Display public service announcements and verify readiness for applying automatic updates to the site.'
+description: 'Display Public service announcements and verify readiness for applying automatic updates to the site.'
 core: 8.x
 package: 'Security'
 configure: automatic_updates.settings
diff --git a/automatic_updates.install b/automatic_updates.install
index 25965a7ca7..3c54d76b64 100644
--- a/automatic_updates.install
+++ b/automatic_updates.install
@@ -76,7 +76,7 @@ function _automatic_updates_checker_requirements(array &$requirements) {
 }
 
 /**
- * Display requirements from public service announcements.
+ * Display requirements from Public service announcements.
  *
  * @param array $requirements
  *   The requirements array.
@@ -89,7 +89,7 @@ function _automatic_updates_psa_requirements(array &$requirements) {
   $psa = \Drupal::service('automatic_updates.psa');
   $messages = $psa->getPublicServiceMessages();
   $requirements['automatic_updates_psa'] = [
-    'title' => t('<a href="@link">Drupal public service announcements</a>', ['@link' => 'https://www.drupal.org/docs/8/update/automatic-updates#psas']),
+    '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.'),
   ];
diff --git a/automatic_updates.links.menu.yml b/automatic_updates.links.menu.yml
index 4ebb874894..bc60db505b 100644
--- a/automatic_updates.links.menu.yml
+++ b/automatic_updates.links.menu.yml
@@ -1,5 +1,5 @@
 automatic_updates.settings:
   title: 'Automatic updates'
   route_name: automatic_updates.settings
-  description: 'Configure public service announcement notifications'
+  description: 'Configure Public service announcement notifications'
   parent: system.admin_config_system
diff --git a/automatic_updates.module b/automatic_updates.module
index c95de6fb40..446d8e99d8 100644
--- a/automatic_updates.module
+++ b/automatic_updates.module
@@ -35,7 +35,7 @@ function automatic_updates_page_top(array &$page_top) {
     $psa = \Drupal::service('automatic_updates.psa');
     $messages = $psa->getPublicServiceMessages();
     if ($messages) {
-      \Drupal::messenger()->addError(t('Drupal public service announcements:'));
+      \Drupal::messenger()->addError(t('Public service announcements:'));
       foreach ($messages as $message) {
         \Drupal::messenger()->addError($message);
       }
diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php
index 9f38e66a95..96b9a082d4 100644
--- a/src/Form/SettingsForm.php
+++ b/src/Form/SettingsForm.php
@@ -71,12 +71,12 @@ class SettingsForm extends ConfigFormBase {
     ];
     $form['enable_psa'] = [
       '#type' => 'checkbox',
-      '#title' => $this->t('Show public service announcements on administrative pages.'),
+      '#title' => $this->t('Show Public service announcements on administrative pages.'),
       '#default_value' => $config->get('enable_psa'),
     ];
     $form['notify'] = [
       '#type' => 'checkbox',
-      '#title' => $this->t('Send email notifications for public service announcements.'),
+      '#title' => $this->t('Send email notifications for Public service announcements.'),
       '#default_value' => $config->get('notify'),
       '#description' => $this->t('The email addresses listed in <a href="@update_manager">update manager settings</a> will be notified.', ['@update_manager' => Url::fromRoute('update.settings')->toString()]),
     ];
diff --git a/src/Services/AutomaticUpdatesPsa.php b/src/Services/AutomaticUpdatesPsa.php
index 820bc9955b..e0e0d12f8b 100644
--- a/src/Services/AutomaticUpdatesPsa.php
+++ b/src/Services/AutomaticUpdatesPsa.php
@@ -5,7 +5,6 @@ namespace Drupal\automatic_updates\Services;
 use Composer\Semver\VersionParser;
 use Drupal\Component\Datetime\TimeInterface;
 use Drupal\Component\Render\FormattableMarkup;
-use Drupal\Component\Version\Constraint;
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
@@ -114,7 +113,6 @@ class AutomaticUpdatesPsa implements AutomaticUpdatesPsaInterface {
    */
   public function getPublicServiceMessages() {
     $messages = [];
-
     if (!$this->config->get('enable_psa')) {
       return $messages;
     }
@@ -140,10 +138,13 @@ class AutomaticUpdatesPsa implements AutomaticUpdatesPsaInterface {
       $json_payload = json_decode($response);
       if ($json_payload) {
         foreach ($json_payload as $json) {
-          if ($json->project === 'core') {
-            $this->coreParser($messages, $json);
+          if ($json->is_psa && ($json->type === 'core' || $this->isValidExtension($json->type, $json->project))) {
+            $messages[] = $this->message($json->title, $json->link);
+          }
+          elseif ($json->type === 'core') {
+            $this->parseConstraints($messages, $json, \Drupal::VERSION);
           }
-          else {
+          elseif ($this->isValidExtension($json->type, $json->project)) {
             $this->contribParser($messages, $json);
           }
         }
@@ -163,27 +164,22 @@ class AutomaticUpdatesPsa implements AutomaticUpdatesPsaInterface {
   }
 
   /**
-   * Parse core project JSON version strings.
+   * Determine if extension exists and has a version string.
    *
-   * @param array $messages
-   *   The messages array.
-   * @param object $json
-   *   The JSON object.
+   * @param string $extension_type
+   *   The extension type i.e. module, theme, profile.
+   * @param string $project_name
+   *   The project.
+   *
+   * @return bool
+   *   TRUE if extension exists, else FALSE.
    */
-  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[] = new FormattableMarkup('<a href=":url">:message</a>', [
-        ':message' => $json->title,
-        ':url' => $json->link,
-      ]);
+  protected function isValidExtension($extension_type, $project_name) {
+    if (!property_exists($this, $extension_type)) {
+      $this->logger->error('Extension list of type "%extension" does not exist.', ['%extension' => $extension_type]);
+      return FALSE;
     }
+    return $this->{$extension_type}->exists($project_name) && !empty($this->{$extension_type}->getAllAvailableInfo()[$project_name]['version']);
   }
 
   /**
@@ -195,46 +191,59 @@ class AutomaticUpdatesPsa implements AutomaticUpdatesPsaInterface {
    *   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;
-    }
-    array_walk($json->secure_versions, function (&$version) {
+    $extension_version = $this->{$json->type}->getAllAvailableInfo()[$json->project]['version'];
+    $json->insecure = array_filter(array_map(function ($version) {
       if (substr($version, 0, 4) === \Drupal::CORE_COMPATIBILITY . '-') {
-        $version = substr($version, 4);
-      }
-    });
-    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']);
+        return substr($version, 4);
       }
+    }, $json->insecure));
+    if (substr($extension_version, 0, 4) === \Drupal::CORE_COMPATIBILITY . '-') {
+      $extension_version = substr($extension_version, 4);
     }
+    $this->parseConstraints($messages, $json, $extension_version);
   }
 
   /**
-   * Add a contrib message PSA, if appropriate.
+   * Compare versions and add a message, if appropriate.
    *
    * @param array $messages
    *   The messages array.
    * @param object $json
    *   The JSON object.
-   * @param string $extension_version
-   *   The extension version.
+   * @param string $current_version
+   *   The current extension version.
+   *
+   * @throws \UnexpectedValueException
    */
-  protected function contribMessage(array &$messages, $json, $extension_version) {
-    $version_string = implode('||', $json->secure_versions);
-    $constraint = new Constraint("<=$extension_version", \Drupal::CORE_COMPATIBILITY);
-    if (!$constraint->isCompatible($version_string)) {
-      $messages[] = new FormattableMarkup('<a href=":url">:message</a>', [
-        ':message' => $json->title,
-        ':url' => $json->link,
-      ]);
+  protected function parseConstraints(array &$messages, $json, $current_version) {
+    $version_string = implode('||', $json->insecure);
+    if (empty($version_string)) {
+      return;
+    }
+    $parser = new VersionParser();
+    $psa_constraint = $parser->parseConstraints($version_string);
+    $contrib_constraint = $parser->parseConstraints($current_version);
+    if ($psa_constraint->matches($contrib_constraint)) {
+      $messages[] = $this->message($json->title, $json->link);
     }
   }
 
+  /**
+   * Return a message.
+   *
+   * @param string $title
+   *   The title.
+   * @param string $link
+   *   The link.
+   *
+   * @return \Drupal\Component\Render\FormattableMarkup
+   *   The PSA or SA message.
+   */
+  protected function message($title, $link) {
+    return new FormattableMarkup('<a href=":url">:message</a>', [
+      ':message' => $title,
+      ':url' => $link,
+    ]);
+  }
+
 }
diff --git a/templates/automatic-updates-psa-notify.html.twig b/templates/automatic-updates-psa-notify.html.twig
index dd49aac63d..853df06fd5 100644
--- a/templates/automatic-updates-psa-notify.html.twig
+++ b/templates/automatic-updates-psa-notify.html.twig
@@ -21,12 +21,13 @@
     See the <a href="{{ status_report }}">site status report page</a> for more information.
   {% endtrans %}
 </p>
-<p>{{ 'Drupal public service announcements:'|t }}</p>
+<p>{{ 'Public service announcements:'|t }}</p>
 <ul>
   {% for message in messages %}
     <li>{{ message }}</li>
   {% endfor %}
 </ul>
+<p>To see all PSAs, visit <a href="https://www.drupal.org/security/psa" target="_blank">https://www.drupal.org/security/psa</a>.</p>
 <p>
   {% set settings_link = path('automatic_updates.settings') %}
   {% trans %}
diff --git a/tests/modules/test_automatic_updates/src/Controller/JsonTestController.php b/tests/modules/test_automatic_updates/src/Controller/JsonTestController.php
index c84d11d9e4..b646680580 100644
--- a/tests/modules/test_automatic_updates/src/Controller/JsonTestController.php
+++ b/tests/modules/test_automatic_updates/src/Controller/JsonTestController.php
@@ -19,77 +19,98 @@ class JsonTestController extends ControllerBase {
   public function json() {
     $feed = [];
     $feed[] = [
-      'title' => 'Critical Release - PSA-2019-02-19',
-      'link' => 'https://www.drupal.org/psa-2019-02-19',
-      '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',
+      'title' => 'Critical Release - SA-2019-02-19',
+      'link' => 'https://www.drupal.org/sa-2019-02-19',
+      'project' => 'drupal',
+      'type' => 'core',
+      'insecure' => [
+        '7.65',
+        '8.5.14',
+        '8.5.14',
+        '8.6.13',
+        '8.7.0-alpha2',
+        '8.7.0-beta1',
+        '8.7.0-beta2',
+        '8.6.14',
+        '8.6.15',
+        '8.6.15',
+        '8.5.15',
+        '8.5.15',
+        '7.66',
+        '8.7.0',
+        \Drupal::VERSION,
       ],
+      'is_psa' => '0',
       'pubDate' => 'Tue, 19 Feb 2019 14:11:01 +0000',
     ];
     $feed[] = [
       'title' => 'Critical Release - PSA-Really Old',
       'link' => 'https://www.drupal.org/psa',
-      'project' => 'core',
-      'extensions' => [],
-      'type' => 'module',
-      'secure_versions' => [
-        '7.0',
-        '8.4.0',
-      ],
+      'project' => 'drupal',
+      'type' => 'core',
+      'is_psa' => '1',
+      'insecure' => [],
       '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' => ['7.x-7.22', '8.x-8.2.0'],
+      'is_psa' => '0',
+      'insecure' => ['7.x-7.22', '8.x-8.2.0'],
       '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'],
+      'project' => 'standard',
       'type' => 'profile',
-      'secure_versions' => ['8.x-8.10.99'],
+      'insecure' => ['8.x-8.6.13', '8.x-' . \Drupal::VERSION],
+      'is_psa' => '0',
       '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.x-8.10.99'],
+      'is_psa' => '0',
+      'insecure' => ['8.x-8.7.0', '8.x-' . \Drupal::VERSION],
       '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.x-1.2'],
+      'is_psa' => '1',
+      'insecure' => [],
       'pubDate' => 'Tue, 19 Mar 2019 12:50:00 +0000',
     ];
     $feed[] = [
       'title' => 'Token - Moderately critical - Access bypass - SA-CONTRIB-2019',
       'link' => 'https://www.drupal.org/sa-contrib-2019',
       'project' => 'token',
-      'extensions' => ['token'],
       'type' => 'module',
-      'secure_versions' => ['7.x-1.7', '8.x-1.5'],
+      'is_psa' => '0',
+      'insecure' => ['7.x-1.7', '8.x-1.4'],
+      'pubDate' => 'Tue, 19 Mar 2019 12:50:00 +0000',
+    ];
+    $feed[] = [
+      'title' => 'Views - Moderately critical - Access bypass - SA-CONTRIB-2019',
+      'link' => 'https://www.drupal.org/sa-contrib-2019',
+      'project' => 'views',
+      'type' => 'module',
+      'insecure' => [
+        '7.x-3.16',
+        '7.x-3.17',
+        '7.x-3.18',
+        '7.x-3.19',
+        '7.x-3.19',
+        '8.x-8.7.0',
+      ],
+      'is_psa' => '0',
       'pubDate' => 'Tue, 19 Mar 2019 12:50:00 +0000',
     ];
     return new JsonResponse($feed);
diff --git a/tests/src/Functional/AutomaticUpdatesTest.php b/tests/src/Functional/AutomaticUpdatesTest.php
index cbd4cb924b..d01fc4c2db 100644
--- a/tests/src/Functional/AutomaticUpdatesTest.php
+++ b/tests/src/Functional/AutomaticUpdatesTest.php
@@ -51,15 +51,16 @@ class AutomaticUpdatesTest extends BrowserTestBase {
       ->set('psa_endpoint', $end_point)
       ->save();
     $this->drupalGet(Url::fromRoute('system.admin'));
-    $this->assertSession()->pageTextContains('Critical Release - PSA-2019-02-19');
-    $this->assertSession()->pageTextNotContains('Critical Release - PSA-Really Old');
-    $this->assertSession()->pageTextNotContains('Node - Moderately critical - Access bypass - SA-CONTRIB-2019');
+    $this->assertSession()->pageTextContains('Critical Release - SA-2019-02-19');
+    $this->assertSession()->pageTextContains('Critical Release - PSA-Really Old');
     $this->assertSession()->pageTextContains('Seven - Moderately critical - Access bypass - SA-CONTRIB-2019');
     $this->assertSession()->pageTextContains('Standard - Moderately critical - Access bypass - SA-CONTRIB-2019');
+    $this->assertSession()->pageTextNotContains('Node - Moderately critical - Access bypass - SA-CONTRIB-2019');
+    $this->assertSession()->pageTextNotContains('Views - Moderately critical - Access bypass - SA-CONTRIB-2019');
 
     // Test site status report.
     $this->drupalGet(Url::fromRoute('system.status'));
-    $this->assertSession()->pageTextContains('3 urgent announcements require your attention:');
+    $this->assertSession()->pageTextContains('4 urgent announcements require your attention:');
 
     // Test cache.
     $end_point = $this->buildUrl(Url::fromRoute('test_automatic_updates.json_test_denied_controller'));
@@ -67,7 +68,7 @@ class AutomaticUpdatesTest extends BrowserTestBase {
       ->set('psa_endpoint', $end_point)
       ->save();
     $this->drupalGet(Url::fromRoute('system.admin'));
-    $this->assertSession()->pageTextContains('Critical Release - PSA-2019-02-19');
+    $this->assertSession()->pageTextContains('Critical Release - SA-2019-02-19');
 
     // Test transmit errors with JSON endpoint.
     drupal_flush_all_caches();
diff --git a/tests/src/Functional/NotifyTest.php b/tests/src/Functional/NotifyTest.php
index dfb394ccb8..77b63a6e49 100644
--- a/tests/src/Functional/NotifyTest.php
+++ b/tests/src/Functional/NotifyTest.php
@@ -60,14 +60,14 @@ class NotifyTest extends BrowserTestBase {
   public function testSendMail() {
     // Test PSAs on admin pages.
     $this->drupalGet(Url::fromRoute('system.admin'));
-    $this->assertSession()->pageTextContains('Critical Release - PSA-2019-02-19');
+    $this->assertSession()->pageTextContains('Critical Release - SA-2019-02-19');
 
     // Email should be sent.
     $notify = $this->container->get('automatic_updates.psa_notify');
     $notify->send();
     $this->assertCount(1, $this->getMails());
-    $this->assertMailString('subject', '3 urgent Drupal announcements require your attention', 1);
-    $this->assertMailString('body', 'Critical Release - PSA-2019-02-19', 1);
+    $this->assertMailString('subject', '4 urgent Drupal announcements require your attention', 1);
+    $this->assertMailString('body', 'Critical Release - SA-2019-02-19', 1);
 
     // No email should be sent if PSA's are disabled.
     $this->container->get('state')->set('system.test_mail_collector', []);
-- 
GitLab