2013-08-17 05:16:48 -04:00
< ? php
2019-12-03 13:57:53 -05:00
2018-02-21 02:51:46 -05:00
declare ( strict_types = 1 );
2013-08-17 05:16:48 -04:00
/**
2024-05-23 03:26:56 -04:00
* SPDX - FileCopyrightText : 2016 - 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX - FileCopyrightText : 2016 ownCloud , Inc .
* SPDX - License - Identifier : AGPL - 3.0 - only
2013-08-17 05:16:48 -04:00
*/
namespace OC\AppFramework\Http ;
2019-11-22 14:52:10 -05:00
use OC\AppFramework\Http ;
use OC\AppFramework\Middleware\MiddlewareDispatcher ;
use OC\AppFramework\Utility\ControllerMethodReflector ;
2021-01-03 09:28:31 -05:00
use OC\DB\ConnectionAdapter ;
2013-10-11 04:07:57 -04:00
use OCP\AppFramework\Controller ;
2014-10-28 11:34:04 -04:00
use OCP\AppFramework\Http\DataResponse ;
2023-11-27 11:59:14 -05:00
use OCP\AppFramework\Http\ParameterOutOfRangeException ;
2019-11-22 14:52:10 -05:00
use OCP\AppFramework\Http\Response ;
2022-02-02 03:57:50 -05:00
use OCP\Diagnostics\IEventLogger ;
2020-09-25 08:47:14 -04:00
use OCP\IConfig ;
2014-05-06 10:29:19 -04:00
use OCP\IRequest ;
2022-12-15 04:37:27 -05:00
use Psr\Container\ContainerInterface ;
2020-09-25 08:47:14 -04:00
use Psr\Log\LoggerInterface ;
2013-08-17 05:16:48 -04:00
/**
2013-08-20 11:20:36 -04:00
* Class to dispatch the request to the middleware dispatcher
2013-08-17 05:16:48 -04:00
*/
class Dispatcher {
/**
* @ param Http $protocol the http protocol with contains all status headers
* @ param MiddlewareDispatcher $middlewareDispatcher the dispatcher which
* runs the middleware
*/
2025-06-16 12:00:28 -04:00
public function __construct (
2026-01-12 07:15:47 -05:00
private readonly Http $protocol ,
private readonly MiddlewareDispatcher $middlewareDispatcher ,
private readonly ControllerMethodReflector $reflector ,
private readonly IRequest $request ,
private readonly IConfig $config ,
private readonly ConnectionAdapter $connection ,
private readonly LoggerInterface $logger ,
private readonly IEventLogger $eventLogger ,
private readonly ContainerInterface $appContainer ,
2025-06-16 12:00:28 -04:00
) {
2013-08-17 05:16:48 -04:00
}
/**
* Handles a request and calls the dispatcher on the controller
* @ param Controller $controller the controller which will be called
* @ param string $methodName the method name which will be called on
* the controller
2026-01-12 07:15:47 -05:00
* @ return array { 0 : string , 1 : array , 2 : array , 3 : string , 4 : Response }
* $array [ 0 ] contains the http status header as a string ,
* $array [ 1 ] contains response headers as an array ,
* $array [ 2 ] contains response cookies as an array ,
* $array [ 3 ] contains the response output as a string ,
* $array [ 4 ] contains the response object
2014-11-27 08:19:00 -05:00
* @ throws \Exception
2013-08-17 05:16:48 -04:00
*/
2018-02-21 02:51:46 -05:00
public function dispatch ( Controller $controller , string $methodName ) : array {
2013-08-17 05:16:48 -04:00
try {
2022-07-27 08:51:42 -04:00
// prefill reflector with everything that's needed for the
2014-05-06 10:29:19 -04:00
// middlewares
$this -> reflector -> reflect ( $controller , $methodName );
2013-08-17 05:16:48 -04:00
$this -> middlewareDispatcher -> beforeController ( $controller ,
$methodName );
2020-09-25 08:47:14 -04:00
$databaseStatsBefore = [];
if ( $this -> config -> getSystemValueBool ( 'debug' , false )) {
2021-01-03 09:28:31 -05:00
$databaseStatsBefore = $this -> connection -> getInner () -> getStats ();
2020-09-25 08:47:14 -04:00
}
2014-05-06 10:29:19 -04:00
$response = $this -> executeController ( $controller , $methodName );
2013-08-17 05:16:48 -04:00
2020-09-25 08:47:14 -04:00
if ( ! empty ( $databaseStatsBefore )) {
2021-01-03 09:28:31 -05:00
$databaseStatsAfter = $this -> connection -> getInner () -> getStats ();
2020-09-25 08:47:14 -04:00
$numBuilt = $databaseStatsAfter [ 'built' ] - $databaseStatsBefore [ 'built' ];
$numExecuted = $databaseStatsAfter [ 'executed' ] - $databaseStatsBefore [ 'executed' ];
if ( $numBuilt > 50 ) {
2022-01-12 14:44:38 -05:00
$this -> logger -> debug ( 'Controller {class}::{method} created {count} QueryBuilder objects, please check if they are created inside a loop by accident.' , [
2021-01-11 06:57:03 -05:00
'class' => get_class ( $controller ),
2020-09-25 08:47:14 -04:00
'method' => $methodName ,
'count' => $numBuilt ,
]);
}
if ( $numExecuted > 100 ) {
2022-01-12 14:44:38 -05:00
$this -> logger -> warning ( 'Controller {class}::{method} executed {count} queries.' , [
2021-01-11 06:57:03 -05:00
'class' => get_class ( $controller ),
2020-09-25 08:47:14 -04:00
'method' => $methodName ,
'count' => $numExecuted ,
]);
}
}
2013-08-20 11:20:36 -04:00
// if an exception appears, the middleware checks if it can handle the
// exception and creates a response. If no response is created, it is
2022-07-27 08:51:42 -04:00
// assumed that there's no middleware who can handle it and the error is
2013-08-20 11:20:36 -04:00
// thrown again
2013-08-17 05:16:48 -04:00
} catch ( \Exception $exception ) {
$response = $this -> middlewareDispatcher -> afterException (
$controller , $methodName , $exception );
2019-08-29 11:18:39 -04:00
} catch ( \Throwable $throwable ) {
2021-09-21 04:28:26 -04:00
$exception = new \Exception ( $throwable -> getMessage () . ' in file \'' . $throwable -> getFile () . '\' line ' . $throwable -> getLine (), $throwable -> getCode (), $throwable );
2019-08-29 11:18:39 -04:00
$response = $this -> middlewareDispatcher -> afterException (
2023-01-20 05:45:08 -05:00
$controller , $methodName , $exception );
2013-08-17 05:16:48 -04:00
}
$response = $this -> middlewareDispatcher -> afterController (
$controller , $methodName , $response );
// depending on the cache object the headers need to be changed
2026-01-12 07:15:47 -05:00
return [
$this -> protocol -> getStatusHeader ( $response -> getStatus ()),
array_merge ( $response -> getHeaders ()),
$response -> getCookies (),
$this -> middlewareDispatcher -> beforeOutput (
$controller , $methodName , $response -> render ()
),
$response ,
];
2013-08-17 05:16:48 -04:00
}
2014-05-06 10:29:19 -04:00
/**
* Uses the reflected parameters , types and request parameters to execute
* the controller
2014-05-11 07:59:48 -04:00
* @ param Controller $controller the controller to be executed
* @ param string $methodName the method on the controller that should be executed
2014-05-06 10:29:19 -04:00
* @ return Response
*/
2018-02-21 02:51:46 -05:00
private function executeController ( Controller $controller , string $methodName ) : Response {
$arguments = [];
2014-05-06 10:29:19 -04:00
2023-11-17 08:04:09 -05:00
// valid types that will be cast
2022-12-15 03:33:52 -05:00
$types = [ 'int' , 'integer' , 'bool' , 'boolean' , 'float' , 'double' ];
2014-05-06 10:29:19 -04:00
2014-05-13 04:40:49 -04:00
foreach ( $this -> reflector -> getParameters () as $param => $default ) {
2014-05-06 10:29:19 -04:00
// try to get the parameter from the request object and cast
// it to the type annotated in the @param annotation
2014-05-13 04:40:49 -04:00
$value = $this -> request -> getParam ( $param , $default );
2014-05-06 10:29:19 -04:00
$type = $this -> reflector -> getType ( $param );
2014-06-10 18:54:25 -04:00
2024-11-27 03:26:26 -05:00
// Converted the string `'false'` to false when the controller wants a boolean
if ( $value === 'false' && ( $type === 'bool' || $type === 'boolean' )) {
2014-05-06 10:29:19 -04:00
$value = false ;
2018-02-21 02:51:46 -05:00
} elseif ( $value !== null && \in_array ( $type , $types , true )) {
2014-05-06 10:29:19 -04:00
settype ( $value , $type );
2023-11-17 08:04:09 -05:00
$this -> ensureParameterValueSatisfiesRange ( $param , $value );
2022-12-15 04:37:27 -05:00
} elseif ( $value === null && $type !== null && $this -> appContainer -> has ( $type )) {
$value = $this -> appContainer -> get ( $type );
2014-05-06 10:29:19 -04:00
}
2014-06-10 18:54:25 -04:00
2014-05-06 10:29:19 -04:00
$arguments [] = $value ;
}
2022-02-02 03:57:50 -05:00
$this -> eventLogger -> start ( 'controller:' . get_class ( $controller ) . '::' . $methodName , 'App framework controller execution' );
2025-08-20 07:35:17 -04:00
try {
$response = \call_user_func_array ([ $controller , $methodName ], $arguments );
} catch ( \TypeError $e ) {
2025-10-13 03:52:04 -04:00
// Only intercept TypeErrors occurring on the first line, meaning that the invocation of the controller method failed.
2025-08-20 07:35:17 -04:00
// Any other TypeError happens inside the controller method logic and should be logged as normal.
if ( $e -> getFile () === $this -> reflector -> getFile () && $e -> getLine () === $this -> reflector -> getStartLine ()) {
$this -> logger -> debug ( 'Failed to call controller method: ' . $e -> getMessage (), [ 'exception' => $e ]);
return new Response ( Http :: STATUS_BAD_REQUEST );
}
throw $e ;
}
2022-02-02 03:57:50 -05:00
$this -> eventLogger -> end ( 'controller:' . get_class ( $controller ) . '::' . $methodName );
2014-05-06 10:29:19 -04:00
2024-11-15 04:09:59 -05:00
if ( ! ( $response instanceof Response )) {
$this -> logger -> debug ( $controller :: class . '::' . $methodName . ' returned raw data. Please wrap it in a Response or one of it\'s inheritors.' );
}
2014-10-28 11:34:04 -04:00
// format response
if ( $response instanceof DataResponse || ! ( $response instanceof Response )) {
2025-08-25 09:29:36 -04:00
$format = $this -> request -> getFormat ();
2025-10-08 04:35:44 -04:00
if ( $format !== null && $controller -> isResponderRegistered ( $format )) {
2016-07-20 15:30:39 -04:00
$response = $controller -> buildResponse ( $response , $format );
} else {
$response = $controller -> buildResponse ( $response );
}
2014-05-06 10:29:19 -04:00
}
return $response ;
}
2023-11-17 08:04:09 -05:00
/**
* @ psalm - param mixed $value
2023-11-27 11:59:14 -05:00
* @ throws ParameterOutOfRangeException
2023-11-17 08:04:09 -05:00
*/
private function ensureParameterValueSatisfiesRange ( string $param , $value ) : void {
$rangeInfo = $this -> reflector -> getRange ( $param );
if ( $rangeInfo ) {
if ( $value < $rangeInfo [ 'min' ] || $value > $rangeInfo [ 'max' ]) {
2023-11-27 11:59:14 -05:00
throw new ParameterOutOfRangeException (
2023-11-17 08:04:09 -05:00
$param ,
2023-11-27 11:59:14 -05:00
$value ,
2023-11-17 08:04:09 -05:00
$rangeInfo [ 'min' ],
$rangeInfo [ 'max' ],
2023-11-27 11:59:14 -05:00
);
2023-11-17 08:04:09 -05:00
}
}
}
2013-08-17 05:16:48 -04:00
}