Browse Source

Merge branch '3.next' into 4.x

ADmad 7 years ago
parent
commit
ea633b2ee5

+ 3 - 3
src/Console/CommandRunner.php

@@ -152,7 +152,7 @@ class CommandRunner implements EventDispatcherInterface
         [$name, $argv] = $this->longestCommandName($commands, $argv);
         $name = $this->resolveName($commands, $io, $name);
 
-        $result = Shell::CODE_ERROR;
+        $result = Command::CODE_ERROR;
         $shell = $this->getShell($io, $commands, $name);
         if ($shell instanceof Shell) {
             $result = $this->runShell($shell, $argv);
@@ -162,13 +162,13 @@ class CommandRunner implements EventDispatcherInterface
         }
 
         if ($result === null || $result === true) {
-            return Shell::CODE_SUCCESS;
+            return Command::CODE_SUCCESS;
         }
         if (is_int($result)) {
             return $result;
         }
 
-        return Shell::CODE_ERROR;
+        return Command::CODE_ERROR;
     }
 
     /**

+ 3 - 3
src/Controller/Component/SecurityComponent.php

@@ -88,7 +88,7 @@ class SecurityComponent extends Component
      * Component startup. All security checking happens here.
      *
      * @param \Cake\Event\EventInterface $event An Event instance
-     * @return void
+     * @return \Cake\Http\Response|null
      */
     public function startup(EventInterface $event)
     {
@@ -114,10 +114,10 @@ class SecurityComponent extends Component
                 $isNotRequestAction &&
                 $this->_config['validatePost']
             ) {
-                    $this->_validatePost($controller);
+                $this->_validatePost($controller);
             }
         } catch (SecurityException $se) {
-            $this->blackHole($controller, $se->getType(), $se);
+            return $this->blackHole($controller, $se->getType(), $se);
         }
 
         $request = $this->generateToken($request);

+ 3 - 3
src/I18n/Number.php

@@ -130,7 +130,7 @@ class Number
      *
      * - `places` - Minimum number or decimals to use, e.g 0
      * - `precision` - Maximum Number of decimal places to use, e.g. 2
-     * - `pattern` - An ICU number pattern to use for formatting the number. e.g #,###.00
+     * - `pattern` - An ICU number pattern to use for formatting the number. e.g #,##0.00
      * - `locale` - The locale name to use for formatting the number, e.g. fr_FR
      * - `before` - The string to place before whole numbers, e.g. '['
      * - `after` - The string to place after decimal numbers, e.g. ']'
@@ -206,7 +206,7 @@ class Number
      * - `zero` - The text to use for zero values, can be a string or a number. e.g. 0, 'Free!'
      * - `places` - Number of decimal places to use. e.g. 2
      * - `precision` - Maximum Number of decimal places to use, e.g. 2
-     * - `pattern` - An ICU number pattern to use for formatting the number. e.g #,###.00
+     * - `pattern` - An ICU number pattern to use for formatting the number. e.g #,##0.00
      * - `useIntlCode` - Whether or not to replace the currency symbol with the international
      *   currency code.
      *
@@ -280,7 +280,7 @@ class Number
      *    numbers representing money or a NumberFormatter constant.
      * - `places` - Number of decimal places to use. e.g. 2
      * - `precision` - Maximum Number of decimal places to use, e.g. 2
-     * - `pattern` - An ICU number pattern to use for formatting the number. e.g #,###.00
+     * - `pattern` - An ICU number pattern to use for formatting the number. e.g #,##0.00
      * - `useIntlCode` - Whether or not to replace the currency symbol with the international
      *   currency code.
      *

+ 4 - 1
src/Mailer/AbstractTransport.php

@@ -63,7 +63,10 @@ abstract class AbstractTransport
             if ($value === false || $value === null || $value === '') {
                 continue;
             }
-            $out .= $key . ': ' . $value . $eol;
+
+            foreach ((array)$value as $val) {
+                $out .= $key . ': ' . $val . $eol;
+            }
         }
         if (!empty($out)) {
             $out = substr($out, 0, -1 * strlen($eol));

+ 1 - 1
src/Mailer/Message.php

@@ -834,7 +834,7 @@ class Message implements JsonSerializable, Serializable
      */
     public function addHeaders(array $headers)
     {
-        $this->headers = array_merge($this->headers, $headers);
+        $this->headers = Hash::merge($this->headers, $headers);
 
         return $this;
     }

+ 17 - 17
src/ORM/Table.php

@@ -1531,23 +1531,13 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
      */
     protected function _processFindOrCreate($search, ?callable $callback = null, $options = [])
     {
-        if (is_callable($search)) {
-            $query = $this->find();
-            $search($query);
-        } elseif (is_array($search)) {
-            $query = $this->find()->where($search);
-        } elseif ($search instanceof Query) {
-            $query = $search;
-        } else {
-            throw new InvalidArgumentException(sprintf(
-                'Search criteria must be an array, callable or Query. Got "%s"',
-                getTypeName($search)
-            ));
-        }
+        $query = $this->_getFindOrCreateQuery($search);
+
         $row = $query->first();
         if ($row !== null) {
             return $row;
         }
+
         $entity = $this->newEntity();
         if ($options['defaults'] && is_array($search)) {
             $accessibleFields = array_combine(array_keys($search), array_fill(0, count($search), true));
@@ -1570,16 +1560,26 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
     /**
      * Gets the query object for findOrCreate().
      *
-     * @param array|\Cake\ORM\Query|string $search The criteria to find existing records by.
+     * @param array|callable|\Cake\ORM\Query $search The criteria to find existing records by.
      * @return \Cake\ORM\Query
      */
     protected function _getFindOrCreateQuery($search): Query
     {
-        if ($search instanceof Query) {
-            return $search;
+        if (is_callable($search)) {
+            $query = $this->find();
+            $search($query);
+        } elseif (is_array($search)) {
+            $query = $this->find()->where($search);
+        } elseif ($search instanceof Query) {
+            $query = $search;
+        } else {
+            throw new InvalidArgumentException(sprintf(
+                'Search criteria must be an array, callable or Query. Got "%s"',
+                getTypeName($search)
+            ));
         }
 
-        return $this->find()->where($search);
+        return $query;
     }
 
     /**

+ 27 - 1
src/Routing/Router.php

@@ -19,7 +19,12 @@ use Cake\Core\Configure;
 use Cake\Http\ServerRequest;
 use Cake\Routing\Exception\MissingRouteException;
 use Cake\Utility\Inflector;
+use Exception;
 use Psr\Http\Message\ServerRequestInterface;
+use ReflectionFunction;
+use ReflectionMethod;
+use RuntimeException;
+use Throwable;
 
 /**
  * Parses the request URL into controller, action, and parameters. Uses the connected routes
@@ -391,8 +396,29 @@ class Router
     protected static function _applyUrlFilters(array $url): array
     {
         $request = static::getRequest(true);
+        $e = null;
         foreach (static::$_urlFilters as $filter) {
-            $url = $filter($url, $request);
+            try {
+                $url = $filter($url, $request);
+            } catch (Exception $e) {
+                // fall through
+            } catch (Throwable $e) {
+                // fall through
+            }
+            if ($e !== null) {
+                if (is_array($filter)) {
+                    $ref = new ReflectionMethod($filter[0], $filter[1]);
+                } else {
+                    $ref = new ReflectionFunction($filter);
+                }
+                $message = sprintf(
+                    'URL filter defined in %s on line %s could not be applied. The filter failed with: %s',
+                    $ref->getFileName(),
+                    $ref->getStartLine(),
+                    $e->getMessage()
+                );
+                throw new RuntimeException($message, $e->getCode(), $e);
+            }
         }
 
         return $url;

+ 1 - 1
src/View/Helper/NumberHelper.php

@@ -169,7 +169,7 @@ class NumberHelper extends Helper
      * - `zero` - The text to use for zero values, can be a string or a number. e.g. 0, 'Free!'
      * - `places` - Number of decimal places to use. e.g. 2
      * - `precision` - Maximum Number of decimal places to use, e.g. 2
-     * - `pattern` - An ICU number pattern to use for formatting the number. e.g #,###.00
+     * - `pattern` - An ICU number pattern to use for formatting the number. e.g #,##0.00
      * - `useIntlCode` - Whether or not to replace the currency symbol with the international
      *   currency code.
      * - `escape` - Whether or not to escape html in resulting string

+ 29 - 0
tests/TestCase/Controller/Component/SecurityComponentTest.php

@@ -19,6 +19,7 @@ use Cake\Controller\Component\SecurityComponent;
 use Cake\Controller\Exception\SecurityException;
 use Cake\Core\Configure;
 use Cake\Event\Event;
+use Cake\Http\Response;
 use Cake\Http\ServerRequest;
 use Cake\Http\Session;
 use Cake\Routing\Router;
@@ -157,6 +158,34 @@ class SecurityComponentTest extends TestCase
     }
 
     /**
+     * test blackholeCallback returning a response
+     *
+     * @return void
+     */
+    public function testBlackholeReturnResponse()
+    {
+        $request = new ServerRequest([
+            'url' => 'posts/index',
+            'session' => $this->Security->session,
+            'method' => 'POST',
+            'params' => [
+                'controller' => 'posts',
+                'action' => 'index',
+            ],
+            'post' => [
+                'key' => 'value',
+            ],
+        ]);
+        $Controller = new \TestApp\Controller\SomePagesController($request);
+        $event = new Event('Controller.startup', $Controller);
+        $Security = new SecurityComponent($Controller->components());
+        $Security->setConfig('blackHoleCallback', 'responseGenerator');
+
+        $result = $Security->startup($event);
+        $this->assertInstanceOf(Response::class, $result);
+    }
+
+    /**
      * testConstructorSettingProperties method
      *
      * Test that initialize can set properties.

+ 8 - 0
tests/TestCase/I18n/NumberTest.php

@@ -261,6 +261,14 @@ class NumberTest extends TestCase
         $expected = '£0.00';
         $this->assertEquals($expected, $result);
 
+        $result = $this->Number->currency(0, 'GBP', ['pattern' => '¤#,###.00;¤-#,###.00']);
+        $expected = '£.00';
+        $this->assertEquals($expected, $result);
+
+        $result = $this->Number->currency(0, 'GBP', ['pattern' => '¤#,##0.00;¤-#,##0.00']);
+        $expected = '£0.00';
+        $this->assertEquals($expected, $result);
+
         $result = $this->Number->currency(0.00000, 'GBP');
         $expected = '£0.00';
         $this->assertEquals($expected, $result);

+ 5 - 0
tests/TestCase/Mailer/EmailTest.php

@@ -734,6 +734,11 @@ class EmailTest extends TestCase
 
         $result = $this->Email->setHeaders([]);
         $this->assertInstanceOf('Cake\Mailer\Email', $result);
+
+        $this->Email->setHeaders(['o:tag' => ['foo']]);
+        $this->Email->addHeaders(['o:tag' => ['bar']]);
+        $result = $this->Email->getHeaders();
+        $this->assertEquals(['foo', 'bar'], $result['o:tag']);
     }
 
     /**

+ 3 - 1
tests/TestCase/Mailer/Transport/DebugTransportTest.php

@@ -54,13 +54,15 @@ class DebugTransportTest extends TestCase
         $message->setMessageId('<4d9946cf-0a44-4907-88fe-1d0ccbdd56cb@localhost>');
         $message->setSubject('Testing Message');
         $date = date(DATE_RFC2822);
-        $message->setHeaders(['Date' => $date]);
+        $message->setHeaders(['Date' => $date, 'o:tag' => ['foo', 'bar']]);
         $message->expects($this->once())->method('getBody')->will($this->returnValue(['First Line', 'Second Line', '.Third Line', '']));
 
         $headers = "From: CakePHP Test <noreply@cakephp.org>\r\n";
         $headers .= "To: CakePHP <cake@cakephp.org>\r\n";
         $headers .= "Cc: Mark Story <mark@cakephp.org>, Juan Basso <juan@cakephp.org>\r\n";
         $headers .= 'Date: ' . $date . "\r\n";
+        $headers .= 'o:tag: foo' . "\r\n";
+        $headers .= 'o:tag: bar' . "\r\n";
         $headers .= "Message-ID: <4d9946cf-0a44-4907-88fe-1d0ccbdd56cb@localhost>\r\n";
         $headers .= "Subject: Testing Message\r\n";
         $headers .= "MIME-Version: 1.0\r\n";

+ 23 - 0
tests/TestCase/ORM/TableTest.php

@@ -5765,6 +5765,29 @@ class TableTest extends TestCase
     }
 
     /**
+     * Test that findOrCreate cannot accidentally bypass required validation.
+     *
+     * @return void
+     */
+    public function testFindOrCreatePartialValidation()
+    {
+        $articles = $this->getTableLocator()->get('Articles');
+        $articles->setEntityClass(ProtectedEntity::class);
+        $validator = new Validator();
+        $validator->notBlank('title')->requirePresence('title', 'create');
+        $validator->notBlank('body')->requirePresence('body', 'create');
+        $articles->setValidator('default', $validator);
+
+        $this->expectException(PersistenceFailedException::class);
+        $this->expectExceptionMessage(
+            'Entity findOrCreate failure. ' .
+            'Found the following errors (title._required: "This field is required").'
+        );
+
+        $articles->findOrCreate(['body' => 'test']);
+    }
+
+    /**
      * Test that creating a table fires the initialize event.
      *
      * @return void

+ 67 - 0
tests/TestCase/Routing/RouterTest.php

@@ -23,6 +23,7 @@ use Cake\Routing\RouteBuilder;
 use Cake\Routing\RouteCollection;
 use Cake\Routing\Router;
 use Cake\TestSuite\TestCase;
+use RuntimeException;
 
 /**
  * RouterTest class
@@ -1089,6 +1090,72 @@ class RouterTest extends TestCase
     }
 
     /**
+     * Test that url filter failure gives better errors
+     *
+     * @return void
+     */
+    public function testUrlGenerationWithUrlFilterFailureClosure()
+    {
+        $this->expectException(RuntimeException::class);
+        $this->expectExceptionMessageRegExp(
+            '/URL filter defined in .*RouterTest\.php on line \d+ could not be applied\.' .
+            ' The filter failed with: nope/'
+        );
+        Router::connect('/:lang/:controller/:action/*');
+        $request = new ServerRequest([
+            'params' => [
+                'plugin' => null,
+                'lang' => 'en',
+                'controller' => 'posts',
+                'action' => 'index',
+            ],
+        ]);
+        Router::pushRequest($request);
+
+        Router::addUrlFilter(function ($url, $request) {
+            throw new RuntimeException('nope');
+        });
+        Router::url(['controller' => 'posts', 'action' => 'index', 'lang' => 'en']);
+    }
+
+    /**
+     * Test that url filter failure gives better errors
+     *
+     * @return void
+     */
+    public function testUrlGenerationWithUrlFilterFailureMethod()
+    {
+        $this->expectException(RuntimeException::class);
+        $this->expectExceptionMessageRegExp(
+            '/URL filter defined in .*RouterTest\.php on line \d+ could not be applied\.' .
+            ' The filter failed with: /'
+        );
+        Router::connect('/:lang/:controller/:action/*');
+        $request = new ServerRequest([
+            'params' => [
+                'plugin' => null,
+                'lang' => 'en',
+                'controller' => 'posts',
+                'action' => 'index',
+            ],
+        ]);
+        Router::pushRequest($request);
+
+        Router::addUrlFilter([$this, 'badFilter']);
+        Router::url(['controller' => 'posts', 'action' => 'index', 'lang' => 'en']);
+    }
+
+    /**
+     * Testing stub for broken URL filters.
+     *
+     * @throws \RuntimeException
+     */
+    public function badFilter()
+    {
+        throw new RuntimeException('nope');
+    }
+
+    /**
      * Test url param persistence.
      *
      * @return void

+ 1 - 3
tests/test_app/TestApp/Controller/SomePagesController.php

@@ -48,9 +48,7 @@ class SomePagesController extends Controller
      */
     public function responseGenerator()
     {
-        $this->response->body('new response');
-
-        return $this->response;
+        return $this->response->withStringBody('new response');
     }
 
     protected function _fail()