Browse Source

Merge branch '4.next' into 5.x

ADmad 3 years ago
parent
commit
98d656b46d

+ 4 - 0
.github/CONTRIBUTING.md

@@ -86,6 +86,10 @@ After that you can perform the checks via:
 
     composer stan
 
+Note that updating the baselines need to be done with the same PHP version it is run online.
+That is usually the minimum version.
+Make sure to "composer install" and set up the stan tools with it and then also execute them.
+
 ## Reporting a Security Issue
 
 If you've found a security related issue in CakePHP, please don't open an issue in github. Instead, contact us at security@cakephp.org. For more information on how we handle security issues, [see the CakePHP Security Issue Process](https://book.cakephp.org/4/en/contributing/tickets.html#reporting-security-issues).

+ 8 - 0
composer.json

@@ -68,6 +68,14 @@
         "lib-ICU": "The intl PHP library, to use Text::transliterate() or Text::slug()",
         "paragonie/csp-builder": "CSP builder, to use the CSP Middleware"
     },
+    "provide": {
+        "psr/container-implementation": "^1.0 || ^2.0",
+        "psr/http-client-implementation": "^1.0",
+        "psr/http-server-handler-implementation": "^1.0",
+        "psr/http-server-middleware-implementation": "^1.0",
+        "psr/log-implementation": "^1.0 || ^2.0",
+        "psr/simple-cache-implementation": "^1.0 || ^2.0"
+    },
     "config": {
         "process-timeout": 900,
         "sort-packages": true,

+ 3 - 0
src/Core/composer.json

@@ -25,6 +25,9 @@
         "php": ">=8.1",
         "cakephp/utility": "^5.0"
     },
+    "provide": {
+        "psr/container-implementation": "^1.0 || ^2.0"
+    },
     "suggest": {
         "cakephp/event": "To use PluginApplicationInterface or plugin applications.",
         "cakephp/cache": "To use Configure::store() and restore().",

+ 1 - 1
src/Http/Cookie/Cookie.php

@@ -607,7 +607,7 @@ class Cookie implements CookieInterface
     public function withExpired(): static
     {
         $new = clone $this;
-        $new->expiresAt = new DateTimeImmutable('1970-01-01 00:00:01');
+        $new->expiresAt = new DateTimeImmutable('@1');
 
         return $new;
     }

+ 24 - 3
src/Http/Session.php

@@ -18,6 +18,7 @@ namespace Cake\Http;
 
 use Cake\Core\App;
 use Cake\Core\Exception\CakeException;
+use Cake\Error\Debugger;
 use Cake\Utility\Hash;
 use InvalidArgumentException;
 use SessionHandlerInterface;
@@ -67,6 +68,13 @@ class Session
     protected bool $_isCLI = false;
 
     /**
+     * Info about where the headers were sent.
+     *
+     * @var array{filename: string, line: int}|null
+     */
+    protected ?array $headerSentInfo = null;
+
+    /**
      * Returns a new instance of a session after building a configuration bundle for it.
      * This function allows an options array which will be used for configuring the session
      * and the handler to be used. The most important key in the configuration array is
@@ -342,7 +350,10 @@ class Session
             throw new CakeException('Session was already started');
         }
 
-        if (ini_get('session.use_cookies') && headers_sent()) {
+        $filename = $line = null;
+        if (ini_get('session.use_cookies') && headers_sent($filename, $line)) {
+            $this->headerSentInfo = ['filename' => $filename, 'line' => $line];
+
             return false;
         }
 
@@ -491,8 +502,18 @@ class Session
      */
     public function write(array|string $name, mixed $value = null): void
     {
-        if (!$this->started()) {
-            $this->start();
+        $started = $this->started() || $this->start();
+        if (!$started) {
+            $message = 'Could not start the session';
+            if ($this->headerSentInfo !== null) {
+                $message .= sprintf(
+                    ', headers already sent in file `%s` on line `%s`',
+                    Debugger::trimPath($this->headerSentInfo['filename']),
+                    $this->headerSentInfo['line']
+                );
+            }
+
+            throw new CakeException($message);
         }
 
         if (!is_array($name)) {

+ 3 - 1
src/Http/composer.json

@@ -39,7 +39,9 @@
     },
     "provide": {
         "psr/http-client-implementation": "^1.0",
-        "psr/http-factory-implementation": "^1.0"
+        "psr/http-factory-implementation": "^1.0",
+        "psr/http-server-server-implementation": "^1.0",
+        "psr/http-server-middleware-implementation": "^1.0"
     },
     "suggest": {
         "cakephp/cache": "To use cache session storage",

+ 1 - 1
src/TestSuite/TestEmailTransport.php

@@ -42,7 +42,7 @@ class TestEmailTransport extends DebugTransport
      */
     public function send(Message $message): array
     {
-        static::$messages[] = $message;
+        static::$messages[] = clone $message;
 
         return parent::send($message);
     }

+ 14 - 0
tests/TestCase/Http/Cookie/CookieTest.php

@@ -230,6 +230,20 @@ class CookieTest extends TestCase
     }
 
     /**
+     * Test the expired method
+     */
+    public function testWithExpiredNotUtc(): void
+    {
+        date_default_timezone_set('Europe/Paris');
+
+        $cookie = new Cookie('cakephp', 'cakephp-rocks');
+        $new = $cookie->withExpired();
+        date_default_timezone_set('UTC');
+
+        $this->assertStringContainsString('01-Jan-1970 00:00:01 GMT+0000', $new->toHeaderValue());
+    }
+
+    /**
      * Test the withExpiry method
      */
     public function testWithExpiry(): void

+ 30 - 0
tests/TestCase/Http/Middleware/CsrfProtectionMiddlewareTest.php

@@ -106,6 +106,8 @@ class CsrfProtectionMiddlewareTest extends TestCase
         $this->assertNotEmpty($cookie, 'Should set a token.');
         $this->assertMatchesRegularExpression('/^[a-z0-9\/+]+={0,2}$/i', $cookie['value'], 'Should look like base64.');
         $this->assertSame(0, $cookie['expires'], 'session duration.');
+        $this->assertFalse($cookie['secure']);
+        $this->assertFalse($cookie['httponly']);
         $this->assertSame('/dir/', $cookie['path'], 'session path.');
         $requestAttr = $updatedRequest->getAttribute('csrfToken');
 
@@ -114,6 +116,34 @@ class CsrfProtectionMiddlewareTest extends TestCase
         $this->assertMatchesRegularExpression('/^[A-Z0-9\/+]+=*$/i', $requestAttr);
     }
 
+    public function testSettingCookieWithCustomOptions(): void
+    {
+        $request = new ServerRequest([
+            'environment' => ['REQUEST_METHOD' => 'GET'],
+            'webroot' => '/',
+        ]);
+
+        /** @var \Cake\Http\ServerRequest $updatedRequest */
+        $updatedRequest = null;
+        $handler = new TestRequestHandler(function ($request) use (&$updatedRequest) {
+            $updatedRequest = $request;
+
+            return new Response();
+        });
+
+        $middleware = new CsrfProtectionMiddleware([
+            'secure' => true,
+            'httponly' => true,
+            'samesite' => CookieInterface::SAMESITE_LAX,
+        ]);
+        $response = $middleware->process($request, $handler);
+
+        $cookie = $response->getCookie('csrfToken');
+        $this->assertTrue($cookie['secure']);
+        $this->assertTrue($cookie['httponly']);
+        $this->assertSame(CookieInterface::SAMESITE_LAX, $cookie['samesite']);
+    }
+
     /**
      * Test setting request attribute based on old cookie value.
      */

+ 12 - 0
tests/TestCase/Http/ResponseTest.php

@@ -859,6 +859,18 @@ class ResponseTest extends TestCase
         $this->assertSame(1, $new->getCookie('yay')['expires']);
     }
 
+    public function testWithExpiredCookieNotUtc()
+    {
+        date_default_timezone_set('Europe/Paris');
+
+        $response = new Response();
+        $cookie = new Cookie('yay', 'a value');
+        $response = $response->withExpiredCookie($cookie);
+        date_default_timezone_set('UTC');
+
+        $this->assertSame(1, $response->getCookie('yay')['expires']);
+    }
+
     /**
      * Test getCookies() and array data.
      */

+ 36 - 0
tests/TestCase/TestSuite/EmailTraitTest.php

@@ -14,6 +14,7 @@ declare(strict_types=1);
  * @since         3.7.0
  * @license       https://opensource.org/licenses/mit-license.php MIT License
  */
+
 namespace Cake\Test\TestCase\TestSuite;
 
 use Cake\Mailer\Mailer;
@@ -165,6 +166,18 @@ class EmailTraitTest extends TestCase
     }
 
     /**
+     * tests multiple messages sent by same Mailer are captured correctly
+     */
+    public function testAssertMultipleMessages(): void
+    {
+        $this->sendMultipleEmails();
+
+        $this->assertMailSentTo('to@example.com');
+        $this->assertMailSentTo('to2@example.com');
+        $this->assertMailSentFrom('reusable-mailer@example.com');
+    }
+
+    /**
      * Tests asserting using RegExp characters doesn't break the assertion
      */
     public function testAssertUsingRegExpCharacters(): void
@@ -247,4 +260,27 @@ class EmailTraitTest extends TestCase
             ->setTo(['to3@example.com' => null])
             ->deliver('html');
     }
+
+    /**
+     * sends some emails
+     */
+    private function sendMultipleEmails(): void
+    {
+        $reusableMailer = new Mailer();
+        $reusableMailer
+            ->setEmailFormat(Message::MESSAGE_TEXT)
+            ->setFrom('reusable-mailer@example.com');
+
+        $emails = [
+            'to@example.com' => ['title' => 'Title1', 'content' => 'abc'],
+            'to2@example.com' => ['title' => 'Title2', 'content' => 'xyz'],
+        ];
+
+        foreach ($emails as $email => $messageContents) {
+            $reusableMailer->setTo($email)
+                ->setSubject($messageContents['title'])
+                ->setViewVars($messageContents)
+                ->deliver();
+        }
+    }
 }