浏览代码

Merge branch '4.x' into 4.next

ADmad 3 年之前
父节点
当前提交
c33dff19df

+ 4 - 0
.github/CONTRIBUTING.md

@@ -84,6 +84,10 @@ And after that 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).

+ 1 - 1
.phive/phars.xml

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <phive xmlns="https://phar.io/phive">
-  <phar name="phpstan" version="1.9.12" installed="1.9.12" location="./tools/phpstan" copy="false"/>
+  <phar name="phpstan" version="1.10.2" installed="1.10.2" location="./tools/phpstan" copy="false"/>
   <phar name="psalm" version="4.30.0" installed="4.30.0" location="./tools/psalm" copy="false"/>
 </phive>

+ 8 - 0
composer.json

@@ -67,6 +67,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,

+ 0 - 75
phpstan-baseline.neon

@@ -1,11 +1,6 @@
 parameters:
 	ignoreErrors:
 		-
-			message: "#^Strict comparison using \\=\\=\\= between string and false will always evaluate to false\\.$#"
-			count: 1
-			path: src/Auth/DigestAuthenticate.php
-
-		-
 			message: "#^Constructor of class Cake\\\\Auth\\\\Storage\\\\SessionStorage has an unused parameter \\$response\\.$#"
 			count: 1
 			path: src/Auth/Storage/SessionStorage.php
@@ -16,11 +11,6 @@ parameters:
 			path: src/Auth/Storage/StorageInterface.php
 
 		-
-			message: "#^Strict comparison using \\=\\=\\= between class\\-string\\<ArrayObject\\> and 'ArrayIterator' will always evaluate to false\\.$#"
-			count: 1
-			path: src/Collection/Collection.php
-
-		-
 			message: "#^Call to an undefined method Traversable\\:\\:getArrayCopy\\(\\)\\.$#"
 			count: 1
 			path: src/Collection/Iterator/ExtractIterator.php
@@ -41,26 +31,11 @@ parameters:
 			path: src/Collection/Iterator/TreeIterator.php
 
 		-
-			message: "#^Strict comparison using \\=\\=\\= between class\\-string\\<ArrayObject\\> and 'ArrayIterator' will always evaluate to false\\.$#"
-			count: 1
-			path: src/Collection/Iterator/TreeIterator.php
-
-		-
-			message: "#^Strict comparison using \\=\\=\\= between class\\-string\\<ArrayObject\\> and 'ArrayIterator' will always evaluate to false\\.$#"
-			count: 1
-			path: src/Collection/Iterator/TreePrinter.php
-
-		-
 			message: "#^Parameter \\#1 \\$iterator of method MultipleIterator\\:\\:attachIterator\\(\\) expects Iterator, Traversable given\\.$#"
 			count: 1
 			path: src/Collection/Iterator/ZipIterator.php
 
 		-
-			message: "#^Strict comparison using \\=\\=\\= between class\\-string\\<ArrayObject\\> and 'ArrayIterator' will always evaluate to false\\.$#"
-			count: 1
-			path: src/Collection/Iterator/ZipIterator.php
-
-		-
 			message: "#^Strict comparison using \\!\\=\\= between null and null will always evaluate to false\\.$#"
 			count: 2
 			path: src/Command/I18nExtractCommand.php
@@ -116,51 +91,6 @@ parameters:
 			path: src/Database/Driver.php
 
 		-
-			message: "#^Unreachable statement \\- code above always terminates\\.$#"
-			count: 1
-			path: src/Database/Driver.php
-
-		-
-			message: "#^If condition is always true\\.$#"
-			count: 1
-			path: src/Database/Driver/Mysql.php
-
-		-
-			message: "#^Unreachable statement \\- code above always terminates\\.$#"
-			count: 1
-			path: src/Database/Driver/Mysql.php
-
-		-
-			message: "#^If condition is always true\\.$#"
-			count: 1
-			path: src/Database/Driver/Postgres.php
-
-		-
-			message: "#^Unreachable statement \\- code above always terminates\\.$#"
-			count: 1
-			path: src/Database/Driver/Postgres.php
-
-		-
-			message: "#^If condition is always true\\.$#"
-			count: 1
-			path: src/Database/Driver/Sqlite.php
-
-		-
-			message: "#^Unreachable statement \\- code above always terminates\\.$#"
-			count: 1
-			path: src/Database/Driver/Sqlite.php
-
-		-
-			message: "#^If condition is always true\\.$#"
-			count: 1
-			path: src/Database/Driver/Sqlserver.php
-
-		-
-			message: "#^Unreachable statement \\- code above always terminates\\.$#"
-			count: 1
-			path: src/Database/Driver/Sqlserver.php
-
-		-
 			message: "#^Unsafe usage of new static\\(\\)\\.$#"
 			count: 8
 			path: src/Database/Expression/QueryExpression.php
@@ -336,11 +266,6 @@ parameters:
 			path: src/ORM/Query.php
 
 		-
-			message: "#^Strict comparison using \\=\\=\\= between class\\-string\\<ArrayObject\\> and 'ArrayIterator' will always evaluate to false\\.$#"
-			count: 1
-			path: src/ORM/ResultSet.php
-
-		-
 			message: "#^Parameter \\#1 \\$rules of method Cake\\\\ORM\\\\Table\\:\\:buildRules\\(\\) expects Cake\\\\ORM\\\\RulesChecker, Cake\\\\Datasource\\\\RulesChecker given\\.$#"
 			count: 1
 			path: src/ORM/Table.php

+ 1 - 1
src/Cache/composer.json

@@ -27,7 +27,7 @@
         "psr/simple-cache": "^1.0 || ^2.0"
     },
     "provide": {
-        "psr/simple-cache-implementation": "^1.0.0"
+        "psr/simple-cache-implementation": "^1.0 || ^2.0"
     },
     "autoload": {
         "psr-4": {

+ 3 - 0
src/Core/composer.json

@@ -25,6 +25,9 @@
         "php": ">=7.4.0",
         "cakephp/utility": "^4.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/Database/Expression/WhenThenExpression.php

@@ -134,7 +134,7 @@ class WhenThenExpression implements ExpressionInterface
                 'The `$when` argument must be either a non-empty array, a scalar value, an object, ' .
                 'or an instance of `\%s`, `%s` given.',
                 ExpressionInterface::class,
-                is_array($when) ? '[]' : getTypeName($when) // @phpstan-ignore-line
+                is_array($when) ? '[]' : getTypeName($when)
             ));
         }
 

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

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

+ 25 - 3
src/Http/Session.php

@@ -17,6 +17,8 @@ declare(strict_types=1);
 namespace Cake\Http;
 
 use Cake\Core\App;
+use Cake\Core\Exception\CakeException;
+use Cake\Error\Debugger;
 use Cake\Utility\Hash;
 use InvalidArgumentException;
 use RuntimeException;
@@ -66,6 +68,13 @@ class Session
     protected $_isCLI = false;
 
     /**
+     * Info about where the headers were sent.
+     *
+     * @var array{filename: string, line: int}|null
+     */
+    protected $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 +351,10 @@ class Session
             throw new RuntimeException('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 +503,18 @@ class Session
      */
     public function write($name, $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

@@ -35,7 +35,9 @@
         "laminas/laminas-httphandlerrunner": "^1.0"
     },
     "provide": {
-        "psr/http-client-implementation": "^1.0"
+        "psr/http-client-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/Log/composer.json

@@ -28,7 +28,7 @@
         "psr/log": "^1.0 || ^2.0"
     },
     "provide": {
-        "psr/log-implementation": "^1.0.0"
+        "psr/log-implementation": "^1.0 || ^2.0"
     },
     "autoload": {
         "psr-4": {

+ 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);
     }

+ 0 - 1
src/Utility/Hash.php

@@ -1119,7 +1119,6 @@ class Hash
             next($intersection);
         }
 
-        /** @phpstan-ignore-next-line */
         return $data + $compare;
     }
 

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

@@ -237,6 +237,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

@@ -910,6 +910,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();
+        }
+    }
 }

+ 1 - 1
tests/TestCase/View/Helper/FormHelperTest.php

@@ -131,7 +131,7 @@ class FormHelperTest extends TestCase
     public function tearDown(): void
     {
         parent::tearDown();
-        unset($this->Form, $this->Controller, $this->View);
+        unset($this->Form, $this->View);
     }
 
     /**