From d84e5fb8da58d776989fb7a381031c578256122c Mon Sep 17 00:00:00 2001
From: lucashedding <lucashedding@1463982.no-reply.drupal.org>
Date: Thu, 14 Nov 2019 11:31:55 -0600
Subject: [PATCH] Issue #3093782 by heddn, drumm, David Strauss, mbaynton,
 pwolanin: Use an external csig file, not packaged inside zip archive

---
 automatic_updates.module                      | 36 ++++++-------
 .../ReadinessCheckerManager.php               |  3 +-
 src/Services/InPlaceUpdate.php                | 50 +++++++++----------
 3 files changed, 42 insertions(+), 47 deletions(-)

diff --git a/automatic_updates.module b/automatic_updates.module
index 78de7ab724..98c5b52d2a 100644
--- a/automatic_updates.module
+++ b/automatic_updates.module
@@ -68,46 +68,42 @@ function automatic_updates_page_top(array &$page_top) {
  * Implements hook_cron().
  */
 function automatic_updates_cron() {
-  /** @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.
-  if (strpos(\Drupal::VERSION, '-dev') !== FALSE) {
-    return;
-  }
+  $dev_core = strpos(\Drupal::VERSION, '-dev') !== FALSE;
   /** @var \Drupal\Core\Config\ImmutableConfig $config */
   $config = \Drupal::config('automatic_updates.settings');
-  if ($config->get('enable_cron_updates')) {
+  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;
-    $dev_core = strpos(\Drupal::VERSION, '-dev') !== FALSE;
     $security_update = in_array($projects['drupal']['status'], [UpdateManagerInterface::NOT_SECURE, UpdateManagerInterface::REVOKED], TRUE);
-    $recommended_release = $projects['drupal']['releases'][$projects['drupal']['recommended']];
-    $major_upgrade = $recommended_release['version_major'] !== $projects['drupal']['existing_major'];
+    $recommended_release = isset($projects['drupal']['releases'][$projects['drupal']['recommended']]) ? $projects['drupal']['releases'][$projects['drupal']['recommended']] : NULL;
+    $major_upgrade = isset($recommended_release['version_major']) ? $recommended_release['version_major'] !== $projects['drupal']['existing_major'] : TRUE;
     // Don't automatically update major version bumps or a dev version of core.
-    if ($major_upgrade || $dev_core) {
-      return;
-    }
-    if ($config->get('enable_cron_security_updates')) {
+    if (!$major_upgrade && $config->get('enable_cron_security_updates')) {
       if ($not_recommended_version && $security_update) {
         /** @var \Drupal\automatic_updates\Services\UpdateInterface $updater */
         $updater = \Drupal::service('automatic_updates.update');
         $updater->update('drupal', 'core', \Drupal::VERSION, $recommended_release['version']);
       }
     }
-    elseif ($not_recommended_version) {
+    elseif (!$major_upgrade && $not_recommended_version) {
       /** @var \Drupal\automatic_updates\Services\UpdateInterface $updater */
       $updater = \Drupal::service('automatic_updates.update');
       $updater->update('drupal', 'core', \Drupal::VERSION, $recommended_release['version']);
     }
   }
+
+  /** @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);
+  }
 }
 
 /**
diff --git a/src/ReadinessChecker/ReadinessCheckerManager.php b/src/ReadinessChecker/ReadinessCheckerManager.php
index 443d552b83..e4732dc713 100644
--- a/src/ReadinessChecker/ReadinessCheckerManager.php
+++ b/src/ReadinessChecker/ReadinessCheckerManager.php
@@ -71,8 +71,9 @@ class ReadinessCheckerManager implements ReadinessCheckerManagerInterface {
     }
 
     foreach ($this->getSortedCheckers()[$category] as $checker) {
-      $messages = array_merge($messages, $checker->run());
+      $messages[] = $checker->run();
     }
+    $messages = array_merge(...$messages);
     $this->keyValue->set("readiness_check_results.$category", $messages);
     $this->keyValue->set('readiness_check_timestamp', \Drupal::time()->getRequestTime());
     return $messages;
diff --git a/src/Services/InPlaceUpdate.php b/src/Services/InPlaceUpdate.php
index a9f1b4c076..f1d6daf8ff 100644
--- a/src/Services/InPlaceUpdate.php
+++ b/src/Services/InPlaceUpdate.php
@@ -4,6 +4,7 @@ namespace Drupal\automatic_updates\Services;
 
 use Drupal\automatic_updates\ProjectInfoTrait;
 use Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManagerInterface;
+use Drupal\Component\FileSystem\FileSystem;
 use Drupal\Core\Archiver\ArchiverInterface;
 use Drupal\Core\Archiver\ArchiverManager;
 use Drupal\Core\Config\ConfigFactoryInterface;
@@ -28,11 +29,6 @@ class InPlaceUpdate implements UpdateInterface {
    */
   const DELETION_MANIFEST = 'DELETION_MANIFEST.txt';
 
-  /**
-   * The checksum file with hashes of archive file contents.
-   */
-  const CHECKSUM_LIST = 'checksumlist.csig';
-
   /**
    * The directory inside the archive for file additions and modifications.
    */
@@ -175,10 +171,17 @@ class InPlaceUpdate implements UpdateInterface {
    *   The archive or NULL if download fails.
    */
   protected function getArchive($project_name, $from_version, $to_version) {
-    $url = $this->buildUrl($project_name, $this->getQuasiPatchFileName($project_name, $from_version, $to_version));
-    $destination = $this->fileSystem->realpath($this->fileSystem->getDestinationFilename("temporary://$project_name.zip", FileSystemInterface::EXISTS_RENAME));
-    $this->doGetArchive($url, $destination);
-    /** @var \Drupal\Core\Archiver\ArchiverInterface $archive */
+    $quasi_patch = $this->getQuasiPatchFileName($project_name, $from_version, $to_version);
+    $url = $this->buildUrl($project_name, $quasi_patch);
+    $temp_directory = $this->getTempDirectory();
+    $destination = $this->fileSystem->getDestinationFilename($temp_directory . $quasi_patch, FileSystemInterface::EXISTS_REPLACE);
+    $this->doGetResource($url, $destination);
+    $csig_file = $quasi_patch . '.csig';
+    $csig_url = $this->buildUrl($project_name, $csig_file);
+    $csig_destination = $this->fileSystem->getDestinationFilename(FileSystem::getOsTemporaryDirectory() . DIRECTORY_SEPARATOR . $csig_file, FileSystemInterface::EXISTS_REPLACE);
+    $this->doGetResource($csig_url, $csig_destination);
+    $csig = file_get_contents($csig_destination);
+    $this->validateArchive($temp_directory, $csig);
     return $this->archiveManager->getInstance(['filepath' => $destination]);
   }
 
@@ -215,7 +218,6 @@ class InPlaceUpdate implements UpdateInterface {
     foreach ($archive_files as $index => &$archive_file) {
       $skipped_files = [
         self::DELETION_MANIFEST,
-        self::CHECKSUM_LIST,
       ];
       // Skip certain files and all directories.
       if (in_array($archive_file, $skipped_files, TRUE) || substr($archive_file, -1) === '/') {
@@ -244,7 +246,7 @@ class InPlaceUpdate implements UpdateInterface {
    * @param null|int $delay
    *   The delay, defaults to NULL.
    */
-  protected function doGetArchive($url, $destination, $delay = NULL) {
+  protected function doGetResource($url, $destination, $delay = NULL) {
     try {
       $this->httpClient->get($url, [
         'sink' => $destination,
@@ -253,14 +255,15 @@ class InPlaceUpdate implements UpdateInterface {
     }
     catch (RequestException $exception) {
       $response = $exception->getResponse();
-      if (!$response || ($response->getStatusCode() === 429 && ($retry = $response->getHeader('Retry-After')))) {
-        $this->doGetArchive($url, $destination, $retry[0] ?? 10 * 1000);
+      if (!$response || ($retry = $response->getHeader('Retry-After'))) {
+        $this->doGetResource($url, $destination, !empty($retry[0]) ? $retry[0] : 10 * 1000);
       }
       else {
         $this->logger->error('Retrieval of "@url" failed with: @message', [
-          '@url' => $url,
+          '@url' => $exception->getRequest()->getUri(),
           '@message' => $exception->getMessage(),
         ]);
+        throw $exception;
       }
     }
   }
@@ -278,7 +281,6 @@ class InPlaceUpdate implements UpdateInterface {
    */
   protected function processUpdate(ArchiverInterface $archive, $project_root) {
     $archive->extract($this->getTempDirectory());
-    $this->validateArchive($this->getTempDirectory());
     foreach ($this->getFilesList($this->getTempDirectory()) as $file) {
       $file_real_path = $this->getFileRealPath($file);
       $file_path = substr($file_real_path, strlen($this->getTempDirectory() . self::ARCHIVE_DIRECTORY));
@@ -311,17 +313,14 @@ class InPlaceUpdate implements UpdateInterface {
    *
    * @param string $directory
    *   The location of the downloaded archive.
+   * @param string $csig
+   *   The CSIG contents.
    */
-  protected function validateArchive($directory) {
-    $csig_file = $directory . DIRECTORY_SEPARATOR . self::CHECKSUM_LIST;
-    if (!file_exists($csig_file)) {
-      throw new \RuntimeException('The CSIG file does not exist in the archive.');
-    }
-    $contents = file_get_contents($csig_file);
+  protected function validateArchive($directory, $csig) {
     $module_path = drupal_get_path('module', 'automatic_updates');
     $key = file_get_contents($module_path . '/artifacts/keys/root.pub');
     $verifier = new Verifier($key);
-    $files = $verifier->verifyCsigMessage($contents);
+    $files = $verifier->verifyCsigMessage($csig);
     $checksums = new ChecksumList($files, TRUE);
     $failed_checksums = new FailedCheckumFilter($checksums, $directory);
     if (iterator_count($failed_checksums)) {
@@ -455,7 +454,6 @@ class InPlaceUpdate implements UpdateInterface {
       }
       $skipped_files = [
         self::DELETION_MANIFEST,
-        self::CHECKSUM_LIST,
       ];
       return $file->isFile() && !in_array($file->getFilename(), $skipped_files, TRUE);
     };
@@ -540,9 +538,9 @@ class InPlaceUpdate implements UpdateInterface {
    */
   protected function getTempDirectory() {
     if (!$this->tempDirectory) {
-      $this->tempDirectory = $this->fileSystem->createFilename('automatic_updates-update', 'temporary://');
-      $this->fileSystem->prepareDirectory($this->tempDirectory);
-      $this->tempDirectory = $this->fileSystem->realpath($this->tempDirectory) . DIRECTORY_SEPARATOR;
+      $this->tempDirectory = $this->fileSystem->createFilename('automatic_updates-update', FileSystem::getOsTemporaryDirectory());
+      $this->fileSystem->prepareDirectory($this->tempDirectory, FileSystemInterface::CREATE_DIRECTORY);
+      $this->tempDirectory .= DIRECTORY_SEPARATOR;
     }
     return $this->tempDirectory;
   }
-- 
GitLab