aboutsummaryrefslogtreecommitdiff
path: root/lib/classes/restapi/Router.php
diff options
context:
space:
mode:
Diffstat (limited to 'lib/classes/restapi/Router.php')
-rw-r--r--lib/classes/restapi/Router.php665
1 files changed, 0 insertions, 665 deletions
diff --git a/lib/classes/restapi/Router.php b/lib/classes/restapi/Router.php
deleted file mode 100644
index df7a6b9..0000000
--- a/lib/classes/restapi/Router.php
+++ /dev/null
@@ -1,665 +0,0 @@
-<?php
-/** @namespace RESTAPI
- *
- * Im Namensraum RESTAPI sind alle Klassen und Funktionen versammelt,
- * die für die RESTful Web Services von Stud.IP benötigt werden.
- */
-namespace RESTAPI;
-use RESTAPI\Renderer\DefaultRenderer;
-
-/**
- * Die Aufgabe des Routers ist das Anlegen und Auswerten eines
- * Mappings von sogenannten Routen (Tupel aus HTTP-Methode und Pfad)
- * auf Code.
- *
- * Dazu werden zunächst Routen mittels der Funktion
- * Router::registerRoutes registriert.
- *
- * Wenn dann ein HTTP-Request eingeht, kann mithilfe von
- * Router::dispatch und HTTP-Methode bzw. Pfad der zugehörige Code
- * gefunden und ausgeführt werden. Der Router bildet aus dem
- * Rückgabewert des Codes ein Response-Objekt, das er als Ergebnis
- * zurück meldet.
- *
- * @code
- * $router = Router::getInstance();
- *
- * // register a sample Route
- * $router->registerRoutes(new ExampleRoute);
- *
- * // dispatch to therein defined Routes
- * $response = $router->dispatch('/example', 'GET');
- *
- * // render response
- * $response->output();
- *
- * @endcode
- *
- * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
- * @author <mlunzena@uos.de>
- * @license GPL 2 or later
- * @see Inspired by http://blog.sosedoff.com/2009/07/04/simpe-php-url-routing-controller/
- * @since Stud.IP 3.0
- * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0.
- */
-class Router
-{
- // instances are cached here
- protected static $instances = [];
-
- /**
- * Holds the user object of the user that is accessing the API.
- * This is null for nobody users.
- */
- protected $user = null;
-
- /**
- * Returns (and if neccessary, initializes) a (cached) router object for an
- * optional consumer id.
- *
- * @param mixed $consumer_id ID of the consumer (defaults to 'global')
- *
- * @return Router returns the Router instance associated to the
- * consumer ID (or to the 'global' ID)
- */
- public static function getInstance($consumer_id = null)
- {
- $consumer_id = $consumer_id ?: 'global';
-
- if (!isset(self::$instances[$consumer_id])) {
- self::$instances[$consumer_id] = new self($consumer_id);
- }
- return self::$instances[$consumer_id];
- }
-
- // All supported method need to be defined here
- protected static $supported_methods = [
- 'get', 'post', 'put', 'delete', 'patch', 'options', 'head'
- ];
-
- /**
- * Returns a list of all supported methods.
- *
- * @return array of methods as strings
- */
- public static function getSupportedMethods()
- {
- return self::$supported_methods;
- }
-
- // registered routes by method and uri template
- protected $routes = [];
-
- // registered content renderers
- protected $renderers = [];
-
- // identified or forced content renderer
- protected $content_renderer = false;
-
- // default renderer
- protected $default_renderer = false;
-
- // registered conditions
- protected $conditions = [];
-
- // registered descriptions
- protected $descriptions = [];
-
- // registered consumers
- protected $consumers = [];
-
- // associated permissions
- protected $permissions = false;
-
- /**
- * Constructs the router.
- *
- * @param mixed $consumer_id the ID of the consumer this router
- * should associate to
- */
- protected function __construct($consumer_id)
- {
- $this->permissions = ConsumerPermissions::get($consumer_id);
- $this->registerRenderer(new Renderer\DefaultRenderer);
- }
-
- /**
- * Registers a handler for a specific combination of request method
- * and uri template.
- *
- * @param String $request_method expected HTTP request method
- * @param String $uri_template expected URI template, for
- * example: \code "/user/:user_id/events" \endcode
- * @param Array $handler request handler array:
- * \code array($object, "methodName") \endcode
- * @param Array $conditions (optional) an associative
- * array using the name of
- * parameters as keys and regexps
- * as value
- * @param string $source (optional) this denotes the
- * origin of a route. Usually
- * either 'core' or 'plugin', but
- * defaults to 'unknown'.
- * @param bool $allow_nobody Whether the route can be accessed
- * as nobody user (true) or not (false).
- * Defaults to false.
- *
- * @return Router returns itself to allow chaining
- * @throws \Exception if passed HTTP request method is not supported
- */
- public function register($request_method, $uri_template, $handler, $conditions = [], $source = 'unknown', $allow_nobody = false)
- {
- // Normalize method and test whether it's supported
- $request_method = mb_strtolower($request_method);
- if (!in_array($request_method, self::$supported_methods)) {
- throw new \Exception('Method "' . $request_method . '" is not supported.');
- }
-
- // Initialize routes storage for this method if neccessary
- if (!isset($this->routes[$request_method])) {
- $this->routes[$request_method] = [];
- }
-
- // Normalize uri template (always starts with a slash)
- if ($uri_template[0] !== '/') {
- $uri_template = '/' . $uri_template;
- }
-
- // Sanitize conditions
- foreach ($conditions as $var => $pattern) {
- if ($pattern[0] !== $pattern[mb_strlen($pattern) - 1] || ctype_alnum($pattern[0])) {
- $conditions[$var] = '/' . $pattern . '/';
- }
- }
-
- $this->routes[$request_method][$uri_template] = compact(
- 'handler', 'conditions', 'source', 'allow_nobody'
- );
-
- // Return instance to allow chaining
- return $this;
- }
-
- /**
- * Registers the routes defined in a RouteMap instance using
- * docblock annotations (like @get) of its methods.
- *
- * \code
- * $router = \RESTAPI\Router::getInstance();
- *
- * $router->registerRoutes(new ExampleRouteMap());
- * \endcode
- *
- * @param RouteMap $map the RouteMap instance to register
- *
- * @return Router returns itself to allow chaining
- */
- public function registerRoutes(RouteMap $map)
- {
- // Investigate object, define whether it's located in the core system
- // or a plugin, respect any defined class conditions and iterate
- // through it's methods to find any defined route
- $ref = new \ReflectionClass($map);
- $filename = $ref->getFilename();
- $source = mb_strpos($filename, 'plugins_packages') !== false
- ? 'plugin'
- : 'core';
-
- foreach (self::$supported_methods as $http_method) {
- foreach ($map->getRoutes($http_method) as $uri_template => $data) {
- // Register (and describe) route
- $this->register(
- $http_method, $uri_template,
- $data['handler'], $data['conditions'],
- $source,
- $data['allow_nobody']
- );
- if ($data['description']) {
- $this->describe(
- $uri_template,
- $data['description'],
- $http_method
- );
- }
- }
- }
-
- return $this;
- }
-
- /**
- * Describe one or more routes.
- *
- * \code
- * $router = \RESTAPI\Router::getInstance();
- *
- * // describe a single route
- * $router->describe('/foo', 'returns everything about foo', 'get');
- *
- * // describe several routes that use the same path
- * $router->describe('/foo', array(
- * 'get' => 'returns everything about foo',
- * 'put' => 'updates all of foo',
- * 'delete' => 'empty up foo'
- * ));
- *
- * // describe several routes
- * $router->describe(array(
- * '/foo' => array(
- * 'get' => 'returns everything about foo',
- * 'put' => 'updates all of foo',
- * 'delete' => 'empty up foo'),
- * '/bar' => array(...),
- * ));
- * \endcode
- *
- * @param String|Array $uri_template URI template to describe or pass an
- * array to describe multiple routes.
- * @param String|null $description description of the route
- * @param String $method method to describe.
- *
- * @return Router returns instance of itself to allow chaining
- */
- public function describe($uri_template, $description = null, $method = 'get')
- {
- // describe multiple routes at once
- if (func_num_args() === 1 && is_array($uri_template)) {
- foreach ($uri_template as $template => $description) {
- $this->describe($template, $description);
- }
- }
-
- // describe routes that use the same URI template
- elseif (func_num_args() === 2 && is_array($description)) {
- foreach ($description as $method => $desc) {
- $this->describe($uri_template, $desc, $method);
- }
- }
-
- // describe a single route
- else {
- if (!isset($this->descriptions[$uri_template])) {
- $this->descriptions[$uri_template] = [];
- }
- if (isset($this->routes[$method][$uri_template])) {
- $this->descriptions[$uri_template][$method] = $description;
- } else {
- // Try to find route with different method
- foreach ($this->routes as $m => $templates) {
- if (isset($templates[$uri_template])) {
- $this->descriptions[$uri_template][$m] = $description;
- break;
- }
- }
- }
- }
- return $this;
- }
-
- /**
- * Get list of registered routes - optionally with their descriptions.
- *
- * @param bool $describe (optional) include descriptions,
- * defaults to `false`
- * @param bool $check_access (optional) only show methods this router's
- * consumer is authorized to,
- * defaults to `true`
- *
- * @return array list of registered routes
- */
- public function getRoutes($describe = false, $check_access = true)
- {
- $this->setupRoutes();
-
- $result = [];
- foreach ($this->routes as $method => $routes) {
- foreach ($routes as $uri => $route) {
- if ($check_access && !$this->permissions->check($uri, $method)) {
- continue;
- }
- if (!isset($result[$uri])) {
- $result[$uri] = [];
- }
- if ($describe) {
- $result[$uri][$method] = [
- 'description' => $this->descriptions[$uri][$method] ?? null,
- 'source' => $route['source'] ?? 'unknown',
- ];
- } else {
- $result[$uri][] = $method;
- }
- }
- }
- ksort($result);
- if ($describe) {
- $result = array_map(function ($item) {
- ksort($item);
- return $item;
- }, $result);
- }
- return $result;
- }
-
- /**
- * Dispatches an URI across the defined routes and produces a
- * Response object which may then be send back (using #output).
- *
- * @param mixed $uri URI to dispatch (defaults to `$_SERVER['PATH_INFO']`)
- * @param String $method Request method (defaults to the method
- * of the actual HTTP request or "GET")
- *
- * @return Response a Response object containing status, headers
- * and body
- * @throws RouterException may throw such an exception if there
- * is no matching route (404) or if there
- * is one, but the consumer is not
- * authorized to it (403)
- */
- public function dispatch($uri = null, $method = null)
- {
- $this->setupRoutes();
-
- $uri = $this->normalizeDispatchURI($uri);
- $method = $this->normalizeRequestMethod($method);
-
- $content_renderer = $this->negotiateContent($uri);
-
- $match_result = $this->matchRoute($uri, $method, $content_renderer);
- $route = $match_result[0];
- $parameters = $match_result[1];
- $allow_nobody = $match_result[2] ?? false;
- if (!$route) {
- //No route found for the combination of URI and method.
- //We return the allowed methods for the route in the HTTP header:
- $methods = $this->getMethodsForUri($uri);
- if (count($methods) > 0) {
- header('Allow: ' . implode(', ', $methods));
- throw new RouterException(405);
- } else {
- //Route not found.
- throw new RouterException(404);
- }
- }
- //At this point, a route is found.
- //We need to check if it can be used as nobody user or not.
- if (!$route['allow_nobody'] && !$this->user) {
- //Nobody users aren't allowed for this route.
- throw new RouterException(401, 'Unauthorized (no consumer)');
- }
-
- try {
- $response = $this->execute($route, $parameters);
- } catch (RouterHalt $halt) {
- $response = $halt->response;
- }
-
- $response->finish($content_renderer);
-
- return $response;
- }
-
- /**
- * Searches and registers available routes.
- */
- private function setupRoutes()
- {
- // A bit ugly, I confess
- static $was_setup = false;
- if ($was_setup) {
- return;
- }
- $was_setup = true;
-
- // Register default routes
- $routes = [
- 'Activity',
- 'Blubber',
- 'Clipboard',
- 'Contacts',
- 'Course',
- 'Discovery',
- 'Events',
- 'Feedback',
- 'FileSystem',
- 'Forum',
- 'Messages',
- 'News',
- 'ResourceBooking',
- 'Resources',
- 'ResourceCategories',
- 'ResourcePermissions',
- 'ResourceProperties',
- 'ResourceRequest',
- 'RoomClipboard',
- 'Schedule',
- 'Semester',
- 'Studip',
- 'User',
- 'UserConfig',
- 'Wiki'
- ];
-
- foreach ($routes as $route) {
- require_once "app/routes/$route.php";
- $class = "\\RESTAPI\\Routes\\$route";
- $this->registerRoutes(new $class);
- }
-
- // Register plugin routes
- $router = $this;
- $routes = array_flatten(\PluginEngine::sendMessage('RESTAPIPlugin', 'getRouteMaps'));
- array_walk(
- $routes,
- function ($route) use ($router) {
- $router->registerRoutes($route);
- }
- );
- }
-
- /**
- * Takes a route and the parameters out of the requested path and
- * executes the handler of the route.
- *
- * @param array $route the matched route out of
- * Router::matchRoute; an array with keys
- * 'handler', 'conditions' and 'source'
- * @param array $parameters the matched parameters out of
- * Router::matchRoute; something like:
- * `array('user_id' => '23a21d...e78f')`
- * @return Response the resulting Response object which is then
- * polished in Router::dispatch
- */
- protected function execute($route, $parameters)
- {
- $handler = $route['handler'];
-
- if (!is_object($handler[0])) {
- throw new \RuntimeException("Handler is not a method.");
- }
-
- $handler[0]->init($this, $route);
-
- if (method_exists($handler[0], 'before')) {
- $handler[0]->before($this, $handler, $parameters);
- }
-
- $result = call_user_func_array($handler, $parameters);
-
- if (is_object($result) && method_exists($result, 'toArray')) {
- $result = $result->toArray();
- }
-
- // $result is stronger than $response->body
- if (isset($result)) {
- $handler[0]->body($result);
- }
-
- if (method_exists($handler[0], 'after')) {
- $handler[0]->after($this, $parameters);
- }
-
- return $handler[0]->getResponse();
- }
-
- /**
- * Registers a content renderer.
- *
- * @param DefaultRenderer $renderer instance of a content renderer
- * @param boolean $is_default (optional) set this
- * renderer as default?;
- * defaults to `false`
- *
- * @return Router returns itself to allow chaining
- */
- public function registerRenderer($renderer, $is_default = false)
- {
- $this->renderers[$renderer->extension()] = $renderer;
- if ($is_default) {
- $this->default_renderer = $renderer;
- }
-
- return $this;
- }
-
- private function normalizeDispatchURI($uri)
- {
- return $uri ?? \Request::pathInfo();
- }
-
- private function normalizeRequestMethod($method)
- {
- return mb_strtolower($method ?: \Request::method() ?: 'get');
- }
-
- /**
- * Negotiate content using the registered content renderers. The
- * first ContentRenderer that returns `true` when calling
- * ContentRenderer::shouldRespondTo gets the job.
- *
- * @param String $uri the URI to which the content renderers may respond
- *
- * @return ContentRenderer either a ContentRenderer that responds
- * to the URI or the default
- * ContentRenderer of this router.
- */
- protected function negotiateContent($uri)
- {
- $content_renderer = null;
- foreach ($this->renderers as $renderer) {
- if ($renderer->shouldRespondTo($uri)) {
- $content_renderer = $renderer;
- break;
- }
- }
- if (!$content_renderer) {
- $content_renderer = $this->default_renderer ?: reset($this->renderers);
- }
- return $content_renderer;
- }
-
- /**
- * Tries to match a route given a URI and a HTTP request method.
- *
- * @param String $uri the URI to match
- * @param String $method the HTTP request method to match
- * @param DefaultRenderer $content_renderer the used
- * ContentRenderer which
- * is needed to remove
- * a file extension
- *
- * @return array an array containing the matched route and the
- * found parameters
- */
- protected function matchRoute($uri, $method, $content_renderer)
- {
- $matched = null;
- $parameters = [];
- if (isset($this->routes[$method])) {
- if ($content_renderer->extension() && mb_strpos($uri, $content_renderer->extension()) !== false) {
- $uri = mb_substr($uri, 0, -mb_strlen($content_renderer->extension()));
- }
-
- foreach ($this->routes[$method] as $uri_template => $route) {
- if (!isset($route['uri_template'])) {
- $route['uri_template'] = new UriTemplate($uri_template, $route['conditions']);
- }
-
- $prmtrs = null; // Will be filled by a successful match()
- if ($route['uri_template']->match($uri, $prmtrs)) {
- if (!$this->permissions->check($uri_template, $method)) {
- throw new RouterException(403, "Route not activated");
- }
- $matched = $route;
- $parameters = $prmtrs;
- break;
- }
- }
- }
- return [$matched, $parameters];
- }
-
- /**
- * Returns all methods the given uri responds to.
- *
- * @param String $uri the URI to match
- *
- * @return array of all of responding methods
- */
- protected function getMethodsForUri($uri)
- {
- $methods = [];
-
- foreach ($this->routes as $method => $templates) {
- foreach ($templates as $uri_template => $route) {
- if (!isset($route['uri_template'])) {
- $route['uri_template'] = new UriTemplate($uri_template, $route['conditions']);
- }
-
- if ($route['uri_template']->match($uri)
- && $this->permissions->check($uri_template, $method))
- {
- $methods[] = $method;
- }
- }
- }
-
- return array_map('strtoupper', $methods);
- }
-
-
- /**
- * Sets up the authentication for the router.
- */
- public function setupAuth()
- {
- // Detect consumer
- $consumer = Consumer\Base::detectConsumer();
- if (!$consumer) {
- return null;
- }
-
- $this->user = $consumer->getUser();
-
- // Set authentication if present
- if ($this->user) {
- // Skip fake authentication if user is already logged in
- if ($GLOBALS['user']->id !== $this->user->id) {
-
- $GLOBALS['auth'] = new \Seminar_Auth();
- $GLOBALS['auth']->auth = [
- 'uid' => $this->user->user_id,
- 'uname' => $this->user->username,
- 'perm' => $this->user->perms,
- ];
-
- $GLOBALS['user'] = new \Seminar_User($this->user);
-
- $GLOBALS['perm'] = new \Seminar_Perm();
- $GLOBALS['MAIL_VALIDATE_BOX'] = false;
- }
- setTempLanguage($GLOBALS['user']->id);
- }
-
- return $this->user;
- }
-}