From 80648da43197c91ed52f36cee8bc818038b88eb6 Mon Sep 17 00:00:00 2001 From: Bernhard Posselt Date: Tue, 6 May 2014 16:29:19 +0200 Subject: implement most of the basic stuff that was suggested in #8290 --- .../dependencyinjection/dicontainer.php | 16 +++- lib/private/appframework/http/dispatcher.php | 88 +++++++++++++++++++- .../middleware/security/securitymiddleware.php | 16 ++-- .../utility/controllermethodreflector.php | 97 ++++++++++++++++++++++ .../utility/methodannotationreader.php | 61 -------------- lib/public/appframework/controller.php | 65 +++++++++++++++ .../appframework/http/iresponseserializer.php | 27 ++++++ lib/public/appframework/http/templateresponse.php | 10 ++- 8 files changed, 303 insertions(+), 77 deletions(-) create mode 100644 lib/private/appframework/utility/controllermethodreflector.php delete mode 100644 lib/private/appframework/utility/methodannotationreader.php create mode 100644 lib/public/appframework/http/iresponseserializer.php (limited to 'lib') diff --git a/lib/private/appframework/dependencyinjection/dicontainer.php b/lib/private/appframework/dependencyinjection/dicontainer.php index becd755bda7..c6139df2382 100644 --- a/lib/private/appframework/dependencyinjection/dicontainer.php +++ b/lib/private/appframework/dependencyinjection/dicontainer.php @@ -33,6 +33,7 @@ use OC\AppFramework\Middleware\Security\SecurityMiddleware; use OC\AppFramework\Middleware\Security\CORSMiddleware; use OC\AppFramework\Utility\SimpleContainer; use OC\AppFramework\Utility\TimeFactory; +use OC\AppFramework\Utility\ControllerMethodReflector; use OCP\AppFramework\IApi; use OCP\AppFramework\IAppContainer; use OCP\AppFramework\Middleware; @@ -81,7 +82,11 @@ class DIContainer extends SimpleContainer implements IAppContainer{ }); $this['Dispatcher'] = $this->share(function($c) { - return new Dispatcher($c['Protocol'], $c['MiddlewareDispatcher']); + return new Dispatcher( + $c['Protocol'], + $c['MiddlewareDispatcher'], + $c['ControllerMethodReflector'] + ); }); @@ -90,7 +95,11 @@ class DIContainer extends SimpleContainer implements IAppContainer{ */ $app = $this; $this['SecurityMiddleware'] = $this->share(function($c) use ($app){ - return new SecurityMiddleware($app, $c['Request']); + return new SecurityMiddleware( + $app, + $c['Request'], + $c['ControllerMethodReflector'] + ); }); $this['CORSMiddleware'] = $this->share(function($c) { @@ -118,6 +127,9 @@ class DIContainer extends SimpleContainer implements IAppContainer{ return new TimeFactory(); }); + $this['ControllerMethodReflector'] = $this->share(function($c) { + return new ControllerMethodReflector(); + }); } diff --git a/lib/private/appframework/http/dispatcher.php b/lib/private/appframework/http/dispatcher.php index a2afb53f0fa..532e49540b1 100644 --- a/lib/private/appframework/http/dispatcher.php +++ b/lib/private/appframework/http/dispatcher.php @@ -26,7 +26,11 @@ namespace OC\AppFramework\Http; use \OC\AppFramework\Middleware\MiddlewareDispatcher; use \OC\AppFramework\Http; +use \OC\AppFramework\Utility\ControllerMethodReflector; + use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Response; +use OCP\IRequest; /** @@ -36,17 +40,25 @@ class Dispatcher { private $middlewareDispatcher; private $protocol; - + private $reflector; + private $request; /** * @param Http $protocol the http protocol with contains all status headers * @param MiddlewareDispatcher $middlewareDispatcher the dispatcher which * runs the middleware + * @param ControllerMethodReflector the reflector that is used to inject + * the arguments for the controller + * @param IRequest $request the incoming request */ public function __construct(Http $protocol, - MiddlewareDispatcher $middlewareDispatcher) { + MiddlewareDispatcher $middlewareDispatcher, + ControllerMethodReflector $reflector, + IRequest $request) { $this->protocol = $protocol; $this->middlewareDispatcher = $middlewareDispatcher; + $this->reflector = $reflector; + $this->request = $request; } @@ -63,10 +75,13 @@ class Dispatcher { $out = array(null, array(), null); try { + // prefill reflector with everything thats needed for the + // middlewares + $this->reflector->reflect($controller, $methodName); $this->middlewareDispatcher->beforeController($controller, $methodName); - $response = $controller->$methodName(); + $response = $this->executeController($controller, $methodName); // if an exception appears, the middleware checks if it can handle the // exception and creates a response. If no response is created, it is @@ -98,4 +113,71 @@ class Dispatcher { } + /** + * Uses the reflected parameters, types and request parameters to execute + * the controller + * @return Response + */ + private function executeController($controller, $methodName) { + $arguments = array(); + + // valid types that will be casted + $types = array('int', 'integer', 'bool', 'boolean', 'float'); + + foreach($this->reflector->getParameters() as $param) { + + // try to get the parameter from the request object and cast + // it to the type annotated in the @param annotation + $value = $this->request->getParam($param); + $type = $this->reflector->getType($param); + + // if this is submitted using GET or a POST form, 'false' should be + // converted to false + if(($type === 'bool' || $type === 'boolean') && + $value === 'false' && + ( + $this->request->method === 'GET' || + ( + $this->request->method === 'POST' && + strpos($this->request->getHeader('Content-Type'), + 'application/x-www-form-urlencoded') !== false + ) + ) + ) { + $value = false; + + } elseif(in_array($type, $types)) { + settype($value, $type); + } + + $arguments[] = $value; + } + + $response = call_user_func_array(array($controller, $methodName), $arguments); + + // format response if not of type response + if(!($response instanceof Response)) { + + // get format from the url format or request format parameter + $format = $this->request->getParam('format'); + + // if none is given try the first Accept header + if($format === null) { + $header = $this->request->getHeader('Accept'); + $formats = explode(',', $header); + + if($header !== null && count($formats) > 0) { + $accept = strtolower(trim($formats[0])); + $format = str_replace('application/', '', $accept); + } else { + $format = 'json'; + } + } + + $response = $controller->formatResponse($response, $format); + } + + return $response; + } + } diff --git a/lib/private/appframework/middleware/security/securitymiddleware.php b/lib/private/appframework/middleware/security/securitymiddleware.php index 0f160d224ad..b4ace5d0e90 100644 --- a/lib/private/appframework/middleware/security/securitymiddleware.php +++ b/lib/private/appframework/middleware/security/securitymiddleware.php @@ -25,7 +25,7 @@ namespace OC\AppFramework\Middleware\Security; use OC\AppFramework\Http; -use OC\AppFramework\Utility\MethodAnnotationReader; +use OC\AppFramework\Utility\ControllerMethodReflector; use OCP\AppFramework\Http\RedirectResponse; use OCP\AppFramework\Middleware; use OCP\AppFramework\Http\Response; @@ -55,10 +55,13 @@ class SecurityMiddleware extends Middleware { /** * @param IAppContainer $app * @param IRequest $request + * @param ControllerMethodReflector $reflector */ - public function __construct(IAppContainer $app, IRequest $request){ + public function __construct(IAppContainer $app, IRequest $request, + ControllerMethodReflector $reflector){ $this->app = $app; $this->request = $request; + $this->reflector = $reflector; } @@ -72,28 +75,25 @@ class SecurityMiddleware extends Middleware { */ public function beforeController($controller, $methodName){ - // get annotations from comments - $annotationReader = new MethodAnnotationReader($controller, $methodName); - // this will set the current navigation entry of the app, use this only // for normal HTML requests and not for AJAX requests $this->app->getServer()->getNavigationManager()->setActiveEntry($this->app->getAppName()); // security checks - $isPublicPage = $annotationReader->hasAnnotation('PublicPage'); + $isPublicPage = $this->reflector->hasAnnotation('PublicPage'); if(!$isPublicPage) { if(!$this->app->isLoggedIn()) { throw new SecurityException('Current user is not logged in', Http::STATUS_UNAUTHORIZED); } - if(!$annotationReader->hasAnnotation('NoAdminRequired')) { + if(!$this->reflector->hasAnnotation('NoAdminRequired')) { if(!$this->app->isAdminUser()) { throw new SecurityException('Logged in user must be an admin', Http::STATUS_FORBIDDEN); } } } - if(!$annotationReader->hasAnnotation('NoCSRFRequired')) { + if(!$this->reflector->hasAnnotation('NoCSRFRequired')) { if(!$this->request->passesCSRFCheck()) { throw new SecurityException('CSRF check failed', Http::STATUS_PRECONDITION_FAILED); } diff --git a/lib/private/appframework/utility/controllermethodreflector.php b/lib/private/appframework/utility/controllermethodreflector.php new file mode 100644 index 00000000000..d6209cae2f2 --- /dev/null +++ b/lib/private/appframework/utility/controllermethodreflector.php @@ -0,0 +1,97 @@ +. + * + */ + + +namespace OC\AppFramework\Utility; + + +/** + * Reads and parses annotations from doc comments + */ +class ControllerMethodReflector { + + private $annotations; + private $types; + private $parameters; + + public function __construct() { + $this->types = array(); + $this->parameters = array(); + $this->annotations = array(); + } + + + /** + * @param object $object an object or classname + * @param string $method the method which we want to inspect + */ + public function reflect($object, $method){ + $reflection = new \ReflectionMethod($object, $method); + $docs = $reflection->getDocComment(); + + // extract everything prefixed by @ and first letter uppercase + preg_match_all('/@([A-Z]\w+)/', $docs, $matches); + $this->annotations = $matches[1]; + + // extract type parameter information + preg_match_all('/@param (?\w+) \$(?\w+)/', $docs, $matches); + $this->types = array_combine($matches['var'], $matches['type']); + + // get method parameters + foreach ($reflection->getParameters() as $param) { + $this->parameters[] = $param->name; + } + } + + + /** + * Inspects the PHPDoc parameters for types + * @param strint $parameter the parameter whose type comments should be + * parsed + * @return string type in the type parameters (@param int $something) would + * return int + */ + public function getType($parameter) { + return $this->types[$parameter]; + } + + + /** + * @return array the arguments of the method + */ + public function getParameters() { + return $this->parameters; + } + + + /** + * Check if a method contains an annotation + * @param string $name the name of the annotation + * @return bool true if the annotation is found + */ + public function hasAnnotation($name){ + return in_array($name, $this->annotations); + } + + +} diff --git a/lib/private/appframework/utility/methodannotationreader.php b/lib/private/appframework/utility/methodannotationreader.php deleted file mode 100644 index 42060a08529..00000000000 --- a/lib/private/appframework/utility/methodannotationreader.php +++ /dev/null @@ -1,61 +0,0 @@ -. - * - */ - - -namespace OC\AppFramework\Utility; - - -/** - * Reads and parses annotations from doc comments - */ -class MethodAnnotationReader { - - private $annotations; - - /** - * @param object $object an object or classname - * @param string $method the method which we want to inspect for annotations - */ - public function __construct($object, $method){ - $this->annotations = array(); - - $reflection = new \ReflectionMethod($object, $method); - $docs = $reflection->getDocComment(); - - // extract everything prefixed by @ and first letter uppercase - preg_match_all('/@([A-Z]\w+)/', $docs, $matches); - $this->annotations = $matches[1]; - } - - - /** - * Check if a method contains an annotation - * @param string $name the name of the annotation - * @return bool true if the annotation is found - */ - public function hasAnnotation($name){ - return in_array($name, $this->annotations); - } - - -} diff --git a/lib/public/appframework/controller.php b/lib/public/appframework/controller.php index f42eba172c7..f28a1d83a63 100644 --- a/lib/public/appframework/controller.php +++ b/lib/public/appframework/controller.php @@ -28,6 +28,8 @@ namespace OCP\AppFramework; use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Http\IResponseSerializer; use OCP\IRequest; @@ -48,6 +50,8 @@ abstract class Controller { */ protected $request; + private $serializer; + private $formatters; /** * constructor of the controller @@ -66,11 +70,66 @@ abstract class Controller { IRequest $request){ $this->appName = $appName; $this->request = $request; + + // default formatters + $this->formatters = array( + 'json' => function ($response) { + return new JSONResponse($response); + } + ); + } + + + /** + * Registers a serializer that is executed before a formatter is being + * called, useful for turning any data into PHP arrays that can be used + * by a JSONResponse for instance + * @param IResponseSerializer $serializer + */ + protected function registerSerializer(IResponseSerializer $serializer) { + $this->serializer = $serializer; + } + + + /** + * Registers a formatter for a type + * @param string $format + * @param \Closure $closure + */ + protected function registerFormatter($format, \Closure $formatter) { + $this->formatters[$format] = $formatter; + } + + + /** + * Serializes and formats a response + * @param mixed response the value that was returned from a controller and + * is not a Response instance + * @param string $format the format for which a formatter has been registered + * @throws \DomainException if format does not match a registered formatter + * @return Response + */ + public function formatResponse($response, $format='json') { + if(array_key_exists($format, $this->formatters)) { + + if ($this->serializer) { + $response = $this->serializer->serialize($response); + } + + $formatter = $this->formatters[$format]; + + return $formatter($response); + + } else { + throw new \DomainException('No formatter registered for format ' . + $format . '!'); + } } /** * Lets you access post and get parameters by the index + * @deprecated write your parameters as method arguments instead * @param string $key the key which you want to access in the URL Parameter * placeholder, $_POST or $_GET array. * The priority how they're returned is the following: @@ -88,6 +147,7 @@ abstract class Controller { /** * Returns all params that were received, be it from the request * (as GET or POST) or throuh the URL by the route + * @deprecated use $this->request instead * @return array the array with all parameters */ public function getParams() { @@ -97,6 +157,7 @@ abstract class Controller { /** * Returns the method of the request + * @deprecated use $this->request instead * @return string the method of the request (POST, GET, etc) */ public function method() { @@ -106,6 +167,7 @@ abstract class Controller { /** * Shortcut for accessing an uploaded file through the $_FILES array + * @deprecated use $this->request instead * @param string $key the key that will be taken from the $_FILES array * @return array the file in the $_FILES element */ @@ -116,6 +178,7 @@ abstract class Controller { /** * Shortcut for getting env variables + * @deprecated use $this->request instead * @param string $key the key that will be taken from the $_ENV array * @return array the value in the $_ENV element */ @@ -126,6 +189,7 @@ abstract class Controller { /** * Shortcut for getting cookie variables + * @deprecated use $this->request instead * @param string $key the key that will be taken from the $_COOKIE array * @return array the value in the $_COOKIE element */ @@ -136,6 +200,7 @@ abstract class Controller { /** * Shortcut for rendering a template + * @deprecated return a template response instead * @param string $templateName the name of the template * @param array $params the template parameters in key => value structure * @param string $renderAs user renders a full page, blank only your template diff --git a/lib/public/appframework/http/iresponseserializer.php b/lib/public/appframework/http/iresponseserializer.php new file mode 100644 index 00000000000..8ffdd451020 --- /dev/null +++ b/lib/public/appframework/http/iresponseserializer.php @@ -0,0 +1,27 @@ +. + * + */ + +namespace OCP\AppFramework\Http; + +interface IResponseSerializer { + function serialize($response); +} \ No newline at end of file diff --git a/lib/public/appframework/http/templateresponse.php b/lib/public/appframework/http/templateresponse.php index f5baf788ada..52355f93cdd 100644 --- a/lib/public/appframework/http/templateresponse.php +++ b/lib/public/appframework/http/templateresponse.php @@ -61,12 +61,16 @@ class TemplateResponse extends Response { * constructor of TemplateResponse * @param string $appName the name of the app to load the template from * @param string $templateName the name of the template + * @param array $params an array of parameters which should be passed to the + * template + * @param string $renderAs how the page should be rendered, defaults to user */ - public function __construct($appName, $templateName) { + public function __construct($appName, $templateName, array $params=array(), + $renderAs='user') { $this->templateName = $templateName; $this->appName = $appName; - $this->params = array(); - $this->renderAs = 'user'; + $this->params = $params; + $this->renderAs = $renderAs; } -- cgit v1.2.3