ControllerFactory.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 3.3.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Controller;
  17. use Cake\Controller\Exception\InvalidParameterException;
  18. use Cake\Core\App;
  19. use Cake\Core\ContainerInterface;
  20. use Cake\Http\ControllerFactoryInterface;
  21. use Cake\Http\Exception\MissingControllerException;
  22. use Cake\Http\MiddlewareQueue;
  23. use Cake\Http\Runner;
  24. use Cake\Http\ServerRequest;
  25. use Closure;
  26. use Psr\Http\Message\ResponseInterface;
  27. use Psr\Http\Message\ServerRequestInterface;
  28. use Psr\Http\Server\RequestHandlerInterface;
  29. use ReflectionClass;
  30. use ReflectionFunction;
  31. use ReflectionNamedType;
  32. /**
  33. * Factory method for building controllers for request.
  34. *
  35. * @implements \Cake\Http\ControllerFactoryInterface<\Cake\Controller\Controller>
  36. */
  37. class ControllerFactory implements ControllerFactoryInterface, RequestHandlerInterface
  38. {
  39. /**
  40. * @var \Cake\Core\ContainerInterface
  41. */
  42. protected ContainerInterface $container;
  43. /**
  44. * @var \Cake\Controller\Controller
  45. */
  46. protected Controller $controller;
  47. /**
  48. * Constructor
  49. *
  50. * @param \Cake\Core\ContainerInterface $container The container to build controllers with.
  51. */
  52. public function __construct(ContainerInterface $container)
  53. {
  54. $this->container = $container;
  55. }
  56. /**
  57. * Create a controller for a given request.
  58. *
  59. * @param \Psr\Http\Message\ServerRequestInterface $request The request to build a controller for.
  60. * @return \Cake\Controller\Controller
  61. * @throws \Cake\Http\Exception\MissingControllerException
  62. */
  63. public function create(ServerRequestInterface $request): Controller
  64. {
  65. $className = $this->getControllerClass($request);
  66. if ($className === null) {
  67. throw $this->missingController($request);
  68. }
  69. $reflection = new ReflectionClass($className);
  70. if ($reflection->isAbstract()) {
  71. throw $this->missingController($request);
  72. }
  73. // Get the controller from the container if defined.
  74. // The request is in the container by default.
  75. if ($this->container->has($className)) {
  76. $controller = $this->container->get($className);
  77. } else {
  78. $controller = $reflection->newInstance($request);
  79. }
  80. return $controller;
  81. }
  82. /**
  83. * Invoke a controller's action and wrapping methods.
  84. *
  85. * @param \Cake\Controller\Controller $controller The controller to invoke.
  86. * @return \Psr\Http\Message\ResponseInterface The response
  87. * @throws \Cake\Controller\Exception\MissingActionException If controller action is not found.
  88. * @throws \UnexpectedValueException If return value of action method is not null or ResponseInterface instance.
  89. */
  90. public function invoke(mixed $controller): ResponseInterface
  91. {
  92. $this->controller = $controller;
  93. $middlewares = $controller->getMiddleware();
  94. if ($middlewares) {
  95. $middlewareQueue = new MiddlewareQueue($middlewares);
  96. $runner = new Runner();
  97. return $runner->run($middlewareQueue, $controller->getRequest(), $this);
  98. }
  99. return $this->handle($controller->getRequest());
  100. }
  101. /**
  102. * Invoke the action.
  103. *
  104. * @param \Psr\Http\Message\ServerRequestInterface $request Request instance.
  105. * @return \Psr\Http\Message\ResponseInterface
  106. */
  107. public function handle(ServerRequestInterface $request): ResponseInterface
  108. {
  109. $controller = $this->controller;
  110. /** @psalm-suppress ArgumentTypeCoercion */
  111. $controller->setRequest($request);
  112. $result = $controller->startupProcess();
  113. if ($result instanceof ResponseInterface) {
  114. return $result;
  115. }
  116. $action = $controller->getAction();
  117. $args = $this->getActionArgs(
  118. $action,
  119. array_values((array)$controller->getRequest()->getParam('pass'))
  120. );
  121. $controller->invokeAction($action, $args);
  122. $result = $controller->shutdownProcess();
  123. if ($result instanceof ResponseInterface) {
  124. return $result;
  125. }
  126. return $controller->getResponse();
  127. }
  128. /**
  129. * Get the arguments for the controller action invocation.
  130. *
  131. * @param \Closure $action Controller action.
  132. * @param array $passedParams Params passed by the router.
  133. * @return array
  134. */
  135. protected function getActionArgs(Closure $action, array $passedParams): array
  136. {
  137. $resolved = [];
  138. $function = new ReflectionFunction($action);
  139. foreach ($function->getParameters() as $parameter) {
  140. $type = $parameter->getType();
  141. if ($type && !$type instanceof ReflectionNamedType) {
  142. // Only single types are supported
  143. throw new InvalidParameterException([
  144. 'template' => 'unsupported_type',
  145. 'parameter' => $parameter->getName(),
  146. 'controller' => $this->controller->getName(),
  147. 'action' => $this->controller->getRequest()->getParam('action'),
  148. 'prefix' => $this->controller->getRequest()->getParam('prefix'),
  149. 'plugin' => $this->controller->getRequest()->getParam('plugin'),
  150. ]);
  151. }
  152. // Check for dependency injection for classes
  153. if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
  154. $typeName = $type->getName();
  155. if ($this->container->has($typeName)) {
  156. $resolved[] = $this->container->get($typeName);
  157. continue;
  158. }
  159. // Use passedParams as a source of typed dependencies.
  160. // The accepted types for passedParams was never defined and userland code relies on that.
  161. if ($passedParams && is_object($passedParams[0]) && $passedParams[0] instanceof $typeName) {
  162. $resolved[] = array_shift($passedParams);
  163. continue;
  164. }
  165. // Add default value if provided
  166. // Do not allow positional arguments for classes
  167. if ($parameter->isDefaultValueAvailable()) {
  168. $resolved[] = $parameter->getDefaultValue();
  169. continue;
  170. }
  171. throw new InvalidParameterException([
  172. 'template' => 'missing_dependency',
  173. 'parameter' => $parameter->getName(),
  174. 'type' => $typeName,
  175. 'controller' => $this->controller->getName(),
  176. 'action' => $this->controller->getRequest()->getParam('action'),
  177. 'prefix' => $this->controller->getRequest()->getParam('prefix'),
  178. 'plugin' => $this->controller->getRequest()->getParam('plugin'),
  179. ]);
  180. }
  181. // Use any passed params as positional arguments
  182. if ($passedParams) {
  183. $argument = array_shift($passedParams);
  184. if (is_string($argument) && $type instanceof ReflectionNamedType) {
  185. $typedArgument = $this->coerceStringToType($argument, $type);
  186. if ($typedArgument === null) {
  187. throw new InvalidParameterException([
  188. 'template' => 'failed_coercion',
  189. 'passed' => $argument,
  190. 'type' => $type->getName(),
  191. 'parameter' => $parameter->getName(),
  192. 'controller' => $this->controller->getName(),
  193. 'action' => $this->controller->getRequest()->getParam('action'),
  194. 'prefix' => $this->controller->getRequest()->getParam('prefix'),
  195. 'plugin' => $this->controller->getRequest()->getParam('plugin'),
  196. ]);
  197. }
  198. $argument = $typedArgument;
  199. }
  200. $resolved[] = $argument;
  201. continue;
  202. }
  203. // Add default value if provided
  204. if ($parameter->isDefaultValueAvailable()) {
  205. $resolved[] = $parameter->getDefaultValue();
  206. continue;
  207. }
  208. // Variadic parameter can have 0 arguments
  209. if ($parameter->isVariadic()) {
  210. continue;
  211. }
  212. throw new InvalidParameterException([
  213. 'template' => 'missing_parameter',
  214. 'parameter' => $parameter->getName(),
  215. 'controller' => $this->controller->getName(),
  216. 'action' => $this->controller->getRequest()->getParam('action'),
  217. 'prefix' => $this->controller->getRequest()->getParam('prefix'),
  218. 'plugin' => $this->controller->getRequest()->getParam('plugin'),
  219. ]);
  220. }
  221. return array_merge($resolved, $passedParams);
  222. }
  223. /**
  224. * Coerces string argument to primitive type.
  225. *
  226. * @param string $argument Argument to coerce
  227. * @param \ReflectionNamedType $type Parameter type
  228. * @return array|string|float|int|bool|null
  229. */
  230. protected function coerceStringToType(string $argument, ReflectionNamedType $type): array|string|float|int|bool|null
  231. {
  232. switch ($type->getName()) {
  233. case 'string':
  234. return $argument;
  235. case 'float':
  236. return is_numeric($argument) ? (float)$argument : null;
  237. case 'int':
  238. return ctype_digit($argument) ? (int)$argument : null;
  239. case 'bool':
  240. return $argument === '0' ? false : ($argument === '1' ? true : null);
  241. case 'array':
  242. return $argument === '' ? [] : explode(',', $argument);
  243. }
  244. return null;
  245. }
  246. /**
  247. * Determine the controller class name based on current request and controller param
  248. *
  249. * @param \Cake\Http\ServerRequest $request The request to build a controller for.
  250. * @return string|null
  251. * @psalm-return class-string<\Cake\Controller\Controller>|null
  252. */
  253. public function getControllerClass(ServerRequest $request): ?string
  254. {
  255. $pluginPath = '';
  256. $namespace = 'Controller';
  257. $controller = $request->getParam('controller', '');
  258. if ($request->getParam('plugin')) {
  259. $pluginPath = $request->getParam('plugin') . '.';
  260. }
  261. if ($request->getParam('prefix')) {
  262. $prefix = $request->getParam('prefix');
  263. $namespace .= '/' . $prefix;
  264. }
  265. $firstChar = substr($controller, 0, 1);
  266. // Disallow plugin short forms, / and \\ from
  267. // controller names as they allow direct references to
  268. // be created.
  269. if (
  270. str_contains($controller, '\\') ||
  271. str_contains($controller, '/') ||
  272. str_contains($controller, '.') ||
  273. $firstChar === strtolower($firstChar)
  274. ) {
  275. throw $this->missingController($request);
  276. }
  277. /** @var class-string<\Cake\Controller\Controller>|null */
  278. return App::className($pluginPath . $controller, $namespace, 'Controller');
  279. }
  280. /**
  281. * Throws an exception when a controller is missing.
  282. *
  283. * @param \Cake\Http\ServerRequest $request The request.
  284. * @return \Cake\Http\Exception\MissingControllerException
  285. */
  286. protected function missingController(ServerRequest $request): MissingControllerException
  287. {
  288. return new MissingControllerException([
  289. 'class' => $request->getParam('controller'),
  290. 'plugin' => $request->getParam('plugin'),
  291. 'prefix' => $request->getParam('prefix'),
  292. '_ext' => $request->getParam('_ext'),
  293. ]);
  294. }
  295. }