Browse Source

Add InvalidParameterException for passed parameter and action mismatches

Corey Taylor 4 years ago
parent
commit
5b589ab8a4

+ 35 - 27
src/Controller/ControllerFactory.php

@@ -16,7 +16,7 @@ declare(strict_types=1);
  */
 namespace Cake\Controller;
 
-use Cake\Controller\Exception\MissingActionException;
+use Cake\Controller\Exception\InvalidParameterException;
 use Cake\Core\App;
 use Cake\Core\ContainerInterface;
 use Cake\Http\ControllerFactoryInterface;
@@ -162,12 +162,14 @@ class ControllerFactory implements ControllerFactoryInterface, RequestHandlerInt
             $type = $parameter->getType();
             if ($type && !$type instanceof ReflectionNamedType) {
                 // Only single types are supported
-                throw new MissingActionException(sprintf(
-                    'Action %s::%s() has an unsupported type for parameter `%s`.',
-                    $this->controller->getName(),
-                    $function->getName(),
-                    $parameter->getName()
-                ));
+                throw new InvalidParameterException([
+                    'template' => 'unsupported_type',
+                    'parameter' => $parameter->getName(),
+                    'controller' => $this->controller->getName(),
+                    'action' => $this->controller->getRequest()->getParam('action'),
+                    'prefix' => $this->controller->getRequest()->getParam('prefix'),
+                    'plugin' => $this->controller->getRequest()->getParam('plugin'),
+                ]);
             }
 
             // Check for dependency injection for classes
@@ -184,12 +186,14 @@ class ControllerFactory implements ControllerFactoryInterface, RequestHandlerInt
                     continue;
                 }
 
-                throw new MissingActionException(sprintf(
-                    'Action %s::%s() cannot inject parameter `%s` from service container.',
-                    $this->controller->getName(),
-                    $function->getName(),
-                    $parameter->getName()
-                ));
+                throw new InvalidParameterException([
+                    'template' => 'missing_dependency',
+                    'parameter' => $parameter->getName(),
+                    'controller' => $this->controller->getName(),
+                    'action' => $this->controller->getRequest()->getParam('action'),
+                    'prefix' => $this->controller->getRequest()->getParam('prefix'),
+                    'plugin' => $this->controller->getRequest()->getParam('plugin'),
+                ]);
             }
 
             // Use any passed params as positional arguments
@@ -199,14 +203,16 @@ class ControllerFactory implements ControllerFactoryInterface, RequestHandlerInt
                     $typedArgument = $this->coerceStringToType($argument, $type);
 
                     if ($typedArgument === null) {
-                        throw new MissingActionException(sprintf(
-                            'Action %s::%s() cannot coerce "%s" to `%s` for parameter `%s`.',
-                            $this->controller->getName(),
-                            $function->getName(),
-                            $argument,
-                            $type->getName(),
-                            $parameter->getName()
-                        ));
+                        throw new InvalidParameterException([
+                            'template' => 'failed_coercion',
+                            'passed' => $argument,
+                            'type' => $type->getName(),
+                            'parameter' => $parameter->getName(),
+                            'controller' => $this->controller->getName(),
+                            'action' => $this->controller->getRequest()->getParam('action'),
+                            'prefix' => $this->controller->getRequest()->getParam('prefix'),
+                            'plugin' => $this->controller->getRequest()->getParam('plugin'),
+                        ]);
                     }
                     $argument = $typedArgument;
                 }
@@ -226,12 +232,14 @@ class ControllerFactory implements ControllerFactoryInterface, RequestHandlerInt
                 continue;
             }
 
-            throw new MissingActionException(sprintf(
-                'Action %s::%s() expected passed parameter for `%s`.',
-                $this->controller->getName(),
-                $function->getName(),
-                $parameter->getName()
-            ));
+            throw new InvalidParameterException([
+                'template' => 'missing_parameter',
+                'parameter' => $parameter->getName(),
+                'controller' => $this->controller->getName(),
+                'action' => $this->controller->getRequest()->getParam('action'),
+                'prefix' => $this->controller->getRequest()->getParam('prefix'),
+                'plugin' => $this->controller->getRequest()->getParam('plugin'),
+            ]);
         }
 
         return array_merge($resolved, $passedParams);

+ 51 - 0
src/Controller/Exception/InvalidParameterException.php

@@ -0,0 +1,51 @@
+<?php
+declare(strict_types=1);
+
+/**
+ * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ * @since         4.3.0
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Controller\Exception;
+
+use Cake\Core\Exception\CakeException;
+use Throwable;
+
+/**
+ * Used when a passed parameter or action parameter type declaration is missing or invalid.
+ */
+class InvalidParameterException extends CakeException
+{
+    /**
+     * @var array<string, string>
+     */
+    protected $templates = [
+        'failed_coercion' => 'Unable to coerce "%s" to `%s` for `%s` in action %s::%s().',
+        'missing_dependency' => 'Failed to inject dependency from service container for `%s` in action %s::%s().',
+        'missing_parameter' => 'Missing passed parameter for `%s` in action %s::%s().',
+        'unsupported_type' => 'Type declaration for `%s` in action %s::%s() is unsupported.',
+    ];
+
+    /**
+     * Switches message template based on `template` key in message array.
+     *
+     * @param string|array $message Either the string of the error message, or an array of attributes
+     *   that are made available in the view, and sprintf()'d into Exception::$_messageTemplate
+     * @param int|null $code The error code
+     * @param \Throwable|null $previous the previous exception.
+     */
+    public function __construct($message = '', ?int $code = null, ?Throwable $previous = null)
+    {
+        if (is_array($message)) {
+            $this->_messageTemplate = $this->templates[$message['template']] ?? '';
+            unset($message['template']);
+        }
+        parent::__construct($message, $code, $previous);
+    }
+}

+ 2 - 0
src/Error/ExceptionRenderer.php

@@ -18,6 +18,7 @@ namespace Cake\Error;
 
 use Cake\Controller\Controller;
 use Cake\Controller\ControllerFactory;
+use Cake\Controller\Exception\InvalidParameterException;
 use Cake\Controller\Exception\MissingActionException;
 use Cake\Core\App;
 use Cake\Core\Configure;
@@ -107,6 +108,7 @@ class ExceptionRenderer implements ExceptionRendererInterface
      */
     protected $exceptionHttpCodes = [
         // Controller exceptions
+        InvalidParameterException::class => 404,
         MissingActionException::class => 404,
         // Datasource exceptions
         PageOutOfBoundsException::class => 404,

+ 58 - 0
templates/Error/invalid_parameter.php

@@ -0,0 +1,58 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ * @link          https://cakephp.org CakePHP(tm) Project
+ * @since         4.3.0
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ * @var string $action
+ */
+use Cake\Core\Configure;
+use Cake\Core\Plugin;
+use Cake\Utility\Inflector;
+
+$namespace = Configure::read('App.namespace');
+if (!empty($plugin)) {
+    $namespace = str_replace('/', '\\', $plugin);
+}
+$prefixNs = '';
+$prefix = $prefix ?? '';
+if ($prefix) {
+    $prefix = array_map('Cake\Utility\Inflector::camelize', explode('/', $prefix));
+    $prefixNs = '\\' . implode('\\', $prefix);
+    $prefix = implode(DIRECTORY_SEPARATOR, $prefix) . DIRECTORY_SEPARATOR;
+}
+
+$type = 'Controller';
+$class = Inflector::camelize($controller);
+
+if (empty($plugin)) {
+    $path = APP_DIR . DIRECTORY_SEPARATOR . $type . DIRECTORY_SEPARATOR . $prefix . h($class) . '.php';
+} else {
+    $path = Plugin::classPath($plugin) . $type . DIRECTORY_SEPARATOR . $prefix . h($class) . '.php';
+}
+
+$this->layout = 'dev_error';
+
+$this->assign('title', sprintf('Invalid Parameter', h($class)));
+$this->assign(
+    'subheading',
+    sprintf('<strong>Error</strong> The passed parameter or parameter type is invalid in <em>%s::%s()</em>', h($class), h($action))
+);
+$this->assign('templateName', 'invalid_parameter.php');
+
+$this->start('file');
+?>
+<p class="error">
+    <strong>Error</strong>
+    <?= h($message); ?>
+</p>
+
+<div class="code-dump"><?php highlight_string($code) ?></div>
+<?php $this->end() ?>

+ 15 - 15
tests/TestCase/Controller/ControllerFactoryTest.php

@@ -17,7 +17,7 @@ declare(strict_types=1);
 namespace Cake\Test\TestCase\Controller;
 
 use Cake\Controller\ControllerFactory;
-use Cake\Controller\Exception\MissingActionException;
+use Cake\Controller\Exception\InvalidParameterException;
 use Cake\Core\Container;
 use Cake\Http\Exception\MissingControllerException;
 use Cake\Http\Response;
@@ -502,8 +502,8 @@ class ControllerFactoryTest extends TestCase
         ]);
         $controller = $this->factory->create($request);
 
-        $this->expectException(MissingActionException::class);
-        $this->expectExceptionMessage('Action Dependencies::requiredDep() cannot inject parameter `dep` from service container');
+        $this->expectException(InvalidParameterException::class);
+        $this->expectExceptionMessage('Failed to inject dependency from service container for `dep` in action Dependencies::requiredDep()');
         $this->factory->invoke($controller);
     }
 
@@ -519,8 +519,8 @@ class ControllerFactoryTest extends TestCase
         ]);
         $controller = $this->factory->create($request);
 
-        $this->expectException(MissingActionException::class);
-        $this->expectExceptionMessage('Action Dependencies::requiredParam() expected passed parameter for `one`');
+        $this->expectException(InvalidParameterException::class);
+        $this->expectExceptionMessage('Missing passed parameter for `one` in action Dependencies::requiredParam()');
         $this->factory->invoke($controller);
     }
 
@@ -668,8 +668,8 @@ class ControllerFactoryTest extends TestCase
         ]);
         $controller = $this->factory->create($request);
 
-        $this->expectException(MissingActionException::class);
-        $this->expectExceptionMessage('Action Dependencies::requiredString() expected passed parameter for `str`');
+        $this->expectException(InvalidParameterException::class);
+        $this->expectExceptionMessage('Missing passed parameter for `str` in action Dependencies::requiredString()');
         $this->factory->invoke($controller);
     }
 
@@ -735,8 +735,8 @@ class ControllerFactoryTest extends TestCase
         ]);
         $controller = $this->factory->create($request);
 
-        $this->expectException(MissingActionException::class);
-        $this->expectExceptionMessage('Action Dependencies::requiredTyped() cannot coerce "true" to `float` for parameter `one`');
+        $this->expectException(InvalidParameterException::class);
+        $this->expectExceptionMessage('Unable to coerce "true" to `float` for `one` in action Dependencies::requiredTyped()');
         $this->factory->invoke($controller);
     }
 
@@ -756,8 +756,8 @@ class ControllerFactoryTest extends TestCase
         ]);
         $controller = $this->factory->create($request);
 
-        $this->expectException(MissingActionException::class);
-        $this->expectExceptionMessage('Action Dependencies::requiredTyped() cannot coerce "2.0" to `int` for parameter `two`');
+        $this->expectException(InvalidParameterException::class);
+        $this->expectExceptionMessage('Unable to coerce "2.0" to `int` for `two` in action Dependencies::requiredTyped()');
         $this->factory->invoke($controller);
     }
 
@@ -777,8 +777,8 @@ class ControllerFactoryTest extends TestCase
         ]);
         $controller = $this->factory->create($request);
 
-        $this->expectException(MissingActionException::class);
-        $this->expectExceptionMessage('Action Dependencies::requiredTyped() cannot coerce "true" to `bool` for parameter `three`');
+        $this->expectException(InvalidParameterException::class);
+        $this->expectExceptionMessage('Unable to coerce "true" to `bool` for `three` in action Dependencies::requiredTyped()');
         $this->factory->invoke($controller);
     }
 
@@ -798,8 +798,8 @@ class ControllerFactoryTest extends TestCase
         ]);
         $controller = $this->factory->create($request);
 
-        $this->expectException(MissingActionException::class);
-        $this->expectExceptionMessage('Action Dependencies::unsupportedTyped() cannot coerce "test" to `array` for parameter `one`');
+        $this->expectException(InvalidParameterException::class);
+        $this->expectExceptionMessage('Unable to coerce "test" to `array` for `one` in action Dependencies::unsupportedTyped()');
         $this->factory->invoke($controller);
     }
 

+ 15 - 0
tests/TestCase/Error/ExceptionRendererTest.php

@@ -17,6 +17,7 @@ declare(strict_types=1);
 namespace Cake\Test\TestCase\Error;
 
 use Cake\Controller\Controller;
+use Cake\Controller\Exception\InvalidParameterException;
 use Cake\Controller\Exception\MissingActionException;
 use Cake\Controller\Exception\MissingComponentException;
 use Cake\Core\Configure;
@@ -517,6 +518,20 @@ class ExceptionRendererTest extends TestCase
                 404,
             ],
             [
+                new InvalidParameterException([
+                    'template' => 'failed_coercion',
+                    'passed' => 'test',
+                    'type' => 'float',
+                    'parameter' => 'age',
+                    'controller' => 'TestController',
+                    'action' => 'checkAge',
+                    'prefix' => null,
+                    'plugin' => null,
+                ]),
+                ['/The passed parameter or parameter type is invalid in <em>TestController::checkAge\(\)/'],
+                404,
+            ],
+            [
                 new MissingActionException([
                     'controller' => 'PostsController',
                     'action' => 'index',