From 671e237952abb5f635602938a03eb51354a9255a Mon Sep 17 00:00:00 2001
From: phenaproxima <phenaproxima@205645.no-reply.drupal.org>
Date: Mon, 24 Oct 2022 21:50:24 +0000
Subject: [PATCH] Issue #3317232 by phenaproxima, tedbow: Stages can be owned
 by someone other than their creator

---
 automatic_updates.services.yml                |  2 ++
 package_manager/src/Stage.php                 | 24 +++++++++++++++----
 .../tests/src/Kernel/StageOwnershipTest.php   | 22 +++++++++++++----
 src/CronUpdater.php                           |  6 +----
 src/EventSubscriber/ConfigSubscriber.php      | 21 ++++++++++++++--
 5 files changed, 59 insertions(+), 16 deletions(-)

diff --git a/automatic_updates.services.yml b/automatic_updates.services.yml
index bdd24e6eda..3c3377cab9 100644
--- a/automatic_updates.services.yml
+++ b/automatic_updates.services.yml
@@ -98,6 +98,8 @@ services:
       - { name: event_subscriber }
   automatic_updates.config_subscriber:
     class: Drupal\automatic_updates\EventSubscriber\ConfigSubscriber
+    arguments:
+      - '@automatic_updates.status_checker'
     tags:
       - { name: event_subscriber }
   automatic_updates.validator.scaffold_file_permissions:
diff --git a/package_manager/src/Stage.php b/package_manager/src/Stage.php
index f6e2032c26..7e5188ba00 100644
--- a/package_manager/src/Stage.php
+++ b/package_manager/src/Stage.php
@@ -148,6 +148,13 @@ class Stage implements LoggerAwareInterface {
    */
   protected $eventDispatcher;
 
+  /**
+   * The shared temp store factory.
+   *
+   * @var \Drupal\Core\TempStore\SharedTempStoreFactory
+   */
+  protected $tempStoreFactory;
+
   /**
    * The shared temp store.
    *
@@ -202,7 +209,7 @@ class Stage implements LoggerAwareInterface {
    *   The file system service.
    * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher
    *   The event dispatcher service.
-   * @param \Drupal\Core\TempStore\SharedTempStoreFactory $shared_tempstore
+   * @param \Drupal\Core\TempStore\SharedTempStoreFactory $temp_store_factory
    *   The shared tempstore factory.
    * @param \Drupal\Component\Datetime\TimeInterface $time
    *   The time service.
@@ -211,7 +218,7 @@ class Stage implements LoggerAwareInterface {
    * @param \Drupal\package_manager\FailureMarker $failure_marker
    *   The failure marker service.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, PathLocator $path_locator, BeginnerInterface $beginner, StagerInterface $stager, CommitterInterface $committer, FileSystemInterface $file_system, EventDispatcherInterface $event_dispatcher, SharedTempStoreFactory $shared_tempstore, TimeInterface $time, PathFactoryInterface $path_factory = NULL, FailureMarker $failure_marker = NULL) {
+  public function __construct(ConfigFactoryInterface $config_factory, PathLocator $path_locator, BeginnerInterface $beginner, StagerInterface $stager, CommitterInterface $committer, FileSystemInterface $file_system, EventDispatcherInterface $event_dispatcher, SharedTempStoreFactory $temp_store_factory, TimeInterface $time, PathFactoryInterface $path_factory = NULL, FailureMarker $failure_marker = NULL) {
     $this->configFactory = $config_factory;
     $this->pathLocator = $path_locator;
     $this->beginner = $beginner;
@@ -220,7 +227,8 @@ class Stage implements LoggerAwareInterface {
     $this->fileSystem = $file_system;
     $this->eventDispatcher = $event_dispatcher;
     $this->time = $time;
-    $this->tempStore = $shared_tempstore->get('package_manager_stage');
+    $this->tempStoreFactory = $temp_store_factory;
+    $this->tempStore = $temp_store_factory->get('package_manager_stage');
     if (empty($path_factory)) {
       @trigger_error('Calling ' . __METHOD__ . '() without the $path_factory argument is deprecated in automatic_updates:8.x-2.3 and will be required before automatic_updates:3.0.0. See https://www.drupal.org/node/3310706.', E_USER_DEPRECATED);
       $path_factory = new PathFactory();
@@ -259,7 +267,7 @@ class Stage implements LoggerAwareInterface {
   protected function getMetadata(string $key) {
     $this->checkOwnership();
 
-    $metadata = $this->tempStore->getIfOwner(static::TEMPSTORE_METADATA_KEY) ?: [];
+    $metadata = $this->tempStore->get(static::TEMPSTORE_METADATA_KEY) ?: [];
     return $metadata[$key] ?? NULL;
   }
 
@@ -317,6 +325,12 @@ class Stage implements LoggerAwareInterface {
     // while the event is being processed, the stage is marked as available.
     // @see ::dispatch()
     $id = Crypt::randomBytesBase64();
+    // Re-acquire the tempstore to ensure that the lock is written by whoever is
+    // actually logged in (or not) right now, since it's possible that the stage
+    // was instantiated (i.e., __construct() was called) by a different session,
+    // which would result in the lock having the wrong owner and the stage not
+    // being claimable by whoever is actually creating it.
+    $this->tempStore = $this->tempStoreFactory->get('package_manager_stage');
     $this->tempStore->set(static::TEMPSTORE_LOCK_KEY, [$id, static::class]);
     $this->claim($id);
 
@@ -610,7 +624,7 @@ class Stage implements LoggerAwareInterface {
       throw new StageException('Cannot claim the stage because no stage has been created.');
     }
 
-    $stored_lock = $this->tempStore->getIfOwner(self::TEMPSTORE_LOCK_KEY);
+    $stored_lock = $this->tempStore->getIfOwner(static::TEMPSTORE_LOCK_KEY);
     if (!$stored_lock) {
       throw new StageOwnershipException('Cannot claim the stage because it is not owned by the current user or session.');
     }
diff --git a/package_manager/tests/src/Kernel/StageOwnershipTest.php b/package_manager/tests/src/Kernel/StageOwnershipTest.php
index cfd06a1a6f..b945927b6f 100644
--- a/package_manager/tests/src/Kernel/StageOwnershipTest.php
+++ b/package_manager/tests/src/Kernel/StageOwnershipTest.php
@@ -127,6 +127,20 @@ class StageOwnershipTest extends PackageManagerKernelTestBase {
     }
   }
 
+  /**
+   * Tests that the stage is owned by the person who calls create() on it.
+   */
+  public function testStageOwnedByCreator(): void {
+    // Even if the stage is instantiated before anyone is logged in, it should
+    // still be owned (and claimable) by the user who called create() on it.
+    $stage = $this->createStage();
+
+    $account = $this->createUser([], NULL, FALSE, ['uid' => 2]);
+    $this->setCurrentUser($account);
+    $id = $stage->create();
+    $this->createStage()->claim($id);
+  }
+
   /**
    * Tests behavior of claiming a stage.
    */
@@ -253,8 +267,8 @@ class StageOwnershipTest extends PackageManagerKernelTestBase {
         // Normally, the stage will call this method as it tries to make
         // everything in the staging area writable so it can be deleted. We
         // don't wan't to do that in this test, since we're specifically testing
-        // what happens when we try to delete a staging area with unwritable
-        // files.
+        // what happens when we try to delete a staging area with
+        // write-protected files.
       }
 
     });
@@ -264,8 +278,8 @@ class StageOwnershipTest extends PackageManagerKernelTestBase {
     $stage->create();
     $this->assertFalse($stage->isAvailable());
 
-    // Make the stage directory unwritable, which should prevent files in it
-    // from being deleted.
+    // Write-protect the stage directory, which should prevent files in it from
+    // being deleted.
     $dir = $stage->getStageDirectory();
     chmod($dir, 0400);
     $this->assertDirectoryIsNotWritable($dir);
diff --git a/src/CronUpdater.php b/src/CronUpdater.php
index 5af93a252d..3c00e94829 100644
--- a/src/CronUpdater.php
+++ b/src/CronUpdater.php
@@ -283,11 +283,7 @@ class CronUpdater extends Updater {
     $owner = $this->tempStore->getMetadata(static::TEMPSTORE_LOCK_KEY)
       ->getOwnerId();
     // Reload the tempstore with the correct owner ID so we can claim the stage.
-    // We use \Drupal::service() here because, given what we inherit from the
-    // parent class, there's no clean way to inject the shared tempstore
-    // factory.
-    $this->tempStore = \Drupal::service('tempstore.shared')
-      ->get('package_manager_stage', $owner);
+    $this->tempStore = $this->tempStoreFactory->get('package_manager_stage', $owner);
 
     $this->claim($stage_id);
 
diff --git a/src/EventSubscriber/ConfigSubscriber.php b/src/EventSubscriber/ConfigSubscriber.php
index 439e86ed27..267f4177ff 100644
--- a/src/EventSubscriber/ConfigSubscriber.php
+++ b/src/EventSubscriber/ConfigSubscriber.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\automatic_updates\EventSubscriber;
 
+use Drupal\automatic_updates\Validation\StatusChecker;
 use Drupal\Core\Config\ConfigCrudEvent;
 use Drupal\Core\Config\ConfigEvents;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@@ -19,6 +20,23 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  */
 final class ConfigSubscriber implements EventSubscriberInterface {
 
+  /**
+   * The status checker service.
+   *
+   * @var \Drupal\automatic_updates\Validation\StatusChecker
+   */
+  protected $statusChecker;
+
+  /**
+   * Constructs a ConfigSubscriber object.
+   *
+   * @param \Drupal\automatic_updates\Validation\StatusChecker $status_checker
+   *   The status checker service.
+   */
+  public function __construct(StatusChecker $status_checker) {
+    $this->statusChecker = $status_checker;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -36,8 +54,7 @@ final class ConfigSubscriber implements EventSubscriberInterface {
    */
   public function onConfigSave(ConfigCrudEvent $event): void {
     if ($event->getConfig()->getName() === 'package_manager.settings' && $event->isChanged('executables.composer')) {
-      \Drupal::service('automatic_updates.status_checker')
-        ->clearStoredResults();
+      $this->statusChecker->clearStoredResults();
     }
   }
 
-- 
GitLab