From 5427d50c8f670e7e76c1f846e97b85051001e127 Mon Sep 17 00:00:00 2001
From: Dave Long <dave@longwaveconsulting.com>
Date: Tue, 12 Sep 2023 22:28:44 +0100
Subject: [PATCH] Issue #3371840 by mondrake, Spokje: Time::getRequestTime is
 not immutable when there is no request

---
 core/lib/Drupal/Component/Datetime/Time.php   | 55 +++++++++++++----
 .../Tests/Component/Datetime/TimeTest.php     |  4 +-
 .../Datetime/TimeWithNoRequestTest.php        | 59 +++++++++++++++++++
 3 files changed, 107 insertions(+), 11 deletions(-)
 create mode 100644 core/tests/Drupal/Tests/Component/Datetime/TimeWithNoRequestTest.php

diff --git a/core/lib/Drupal/Component/Datetime/Time.php b/core/lib/Drupal/Component/Datetime/Time.php
index 566debba0494..ec660094fc98 100644
--- a/core/lib/Drupal/Component/Datetime/Time.php
+++ b/core/lib/Drupal/Component/Datetime/Time.php
@@ -6,23 +6,32 @@
 
 /**
  * Provides a class for obtaining system time.
+ *
+ * While the normal use case of this class expects that a Request object is
+ * available from the RequestStack, it is still possible to use it without, for
+ * example for early bootstrap containers or for unit tests. In those cases,
+ * the class will access global variables or set a proxy request time in order
+ * to return the request time.
  */
 class Time implements TimeInterface {
 
   /**
    * The request stack.
-   *
-   * @var \Symfony\Component\HttpFoundation\RequestStack
    */
-  protected $requestStack;
+  protected ?RequestStack $requestStack;
+
+  /**
+   * A proxied request time if the request time is not available.
+   */
+  protected float $proxyRequestTime;
 
   /**
    * Constructs a Time object.
    *
-   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
-   *   The request stack.
+   * @param \Symfony\Component\HttpFoundation\RequestStack|null $request_stack
+   *   (Optional) The request stack.
    */
-  public function __construct(RequestStack $request_stack) {
+  public function __construct(RequestStack $request_stack = NULL) {
     $this->requestStack = $request_stack;
   }
 
@@ -30,26 +39,26 @@ public function __construct(RequestStack $request_stack) {
    * {@inheritdoc}
    */
   public function getRequestTime() {
-    $request = $this->requestStack->getCurrentRequest();
+    $request = $this->requestStack ? $this->requestStack->getCurrentRequest() : NULL;
     if ($request) {
       return $request->server->get('REQUEST_TIME');
     }
     // If this is called prior to the request being pushed to the stack fallback
     // to built-in globals (if available) or the system time.
-    return $_SERVER['REQUEST_TIME'] ?? $this->getCurrentTime();
+    return $_SERVER['REQUEST_TIME'] ?? $this->getProxyRequestTime();
   }
 
   /**
    * {@inheritdoc}
    */
   public function getRequestMicroTime() {
-    $request = $this->requestStack->getCurrentRequest();
+    $request = $this->requestStack ? $this->requestStack->getCurrentRequest() : NULL;
     if ($request) {
       return $request->server->get('REQUEST_TIME_FLOAT');
     }
     // If this is called prior to the request being pushed to the stack fallback
     // to built-in globals (if available) or the system time.
-    return $_SERVER['REQUEST_TIME_FLOAT'] ?? $this->getCurrentMicroTime();
+    return $_SERVER['REQUEST_TIME_FLOAT'] ?? $this->getProxyRequestMicroTime();
   }
 
   /**
@@ -66,4 +75,30 @@ public function getCurrentMicroTime() {
     return microtime(TRUE);
   }
 
+  /**
+   * Returns a mimic of the timestamp of the current request.
+   *
+   * @return int
+   *   A value returned by time().
+   */
+  protected function getProxyRequestTime(): int {
+    if (!isset($this->proxyRequestTime)) {
+      $this->proxyRequestTime = $this->getCurrentMicroTime();
+    }
+    return (int) $this->proxyRequestTime;
+  }
+
+  /**
+   * Returns a mimic of the timestamp of the current request.
+   *
+   * @return float
+   *   A value returned by microtime().
+   */
+  protected function getProxyRequestMicroTime(): float {
+    if (!isset($this->proxyRequestTime)) {
+      $this->proxyRequestTime = $this->getCurrentMicroTime();
+    }
+    return $this->proxyRequestTime;
+  }
+
 }
diff --git a/core/tests/Drupal/Tests/Component/Datetime/TimeTest.php b/core/tests/Drupal/Tests/Component/Datetime/TimeTest.php
index 98dc0e6b53d8..cf6016c7e5c6 100644
--- a/core/tests/Drupal/Tests/Component/Datetime/TimeTest.php
+++ b/core/tests/Drupal/Tests/Component/Datetime/TimeTest.php
@@ -83,7 +83,9 @@ public function testGetRequestMicroTime() {
    * @covers ::getRequestTime
    */
   public function testGetRequestTimeNoRequest() {
-    $expected = 12345678;
+    // With no request, and no global variable, we expect to get the int part
+    // of the microtime.
+    $expected = 1234567;
     unset($_SERVER['REQUEST_TIME']);
     $this->assertEquals($expected, $this->time->getRequestTime());
     $_SERVER['REQUEST_TIME'] = 23456789;
diff --git a/core/tests/Drupal/Tests/Component/Datetime/TimeWithNoRequestTest.php b/core/tests/Drupal/Tests/Component/Datetime/TimeWithNoRequestTest.php
new file mode 100644
index 000000000000..c66e1bb44007
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Datetime/TimeWithNoRequestTest.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Drupal\Tests\Component\Datetime;
+
+use Drupal\Component\Datetime\Time;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * Tests that getRequest(Micro)Time works when no underlying request exists.
+ *
+ * @coversDefaultClass \Drupal\Component\Datetime\Time
+ * @group Datetime
+ * @runTestsInSeparateProcesses
+ * @preserveGlobalState disabled
+ */
+class TimeWithNoRequestTest extends TestCase {
+
+  /**
+   * The time class for testing.
+   */
+  protected Time $time;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    // We need to explicitly unset the $_SERVER variables, so that Time is
+    // forced to look for current time.
+    unset($_SERVER['REQUEST_TIME']);
+    unset($_SERVER['REQUEST_TIME_FLOAT']);
+
+    $this->time = new Time();
+  }
+
+  /**
+   * Tests the getRequestTime method.
+   *
+   * @covers ::getRequestTime
+   */
+  public function testGetRequestTimeImmutable(): void {
+    $requestTime = $this->time->getRequestTime();
+    sleep(2);
+    $this->assertSame($requestTime, $this->time->getRequestTime());
+  }
+
+  /**
+   * Tests the getRequestMicroTime method.
+   *
+   * @covers ::getRequestMicroTime
+   */
+  public function testGetRequestMicroTimeImmutable() {
+    $requestTime = $this->time->getRequestMicroTime();
+    usleep(20000);
+    $this->assertSame($requestTime, $this->time->getRequestMicroTime());
+  }
+
+}
-- 
GitLab