diff options
| author | Philipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de> | 2024-09-24 10:53:31 +0200 |
|---|---|---|
| committer | Philipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de> | 2024-09-24 10:53:31 +0200 |
| commit | 4459dd7917f4d1c34f40bb68f0e991e9c3d53e4c (patch) | |
| tree | 5c07151ae61276d334e88f6309c30d439a85c12e /lib/classes/restapi | |
| parent | da0022e5c1abbf9825ae76debaabdff7e8623bb4 (diff) | |
| parent | 97a188592c679890a25c37ab78463add76a52ff7 (diff) | |
Merge branch 'main' into issue-3911issue-3911
Diffstat (limited to 'lib/classes/restapi')
| -rw-r--r-- | lib/classes/restapi/ConsumerPermissions.php | 212 | ||||
| -rw-r--r-- | lib/classes/restapi/Response.php | 164 | ||||
| -rw-r--r-- | lib/classes/restapi/RouteMap.php | 1060 | ||||
| -rw-r--r-- | lib/classes/restapi/Router.php | 665 | ||||
| -rw-r--r-- | lib/classes/restapi/RouterException.php | 31 | ||||
| -rw-r--r-- | lib/classes/restapi/RouterHalt.php | 19 | ||||
| -rw-r--r-- | lib/classes/restapi/UriTemplate.php | 115 | ||||
| -rw-r--r-- | lib/classes/restapi/UserPermissions.php | 144 | ||||
| -rw-r--r-- | lib/classes/restapi/consumer/Base.php | 226 | ||||
| -rw-r--r-- | lib/classes/restapi/consumer/HTTP.php | 50 | ||||
| -rw-r--r-- | lib/classes/restapi/consumer/OAuth.php | 231 | ||||
| -rw-r--r-- | lib/classes/restapi/consumer/Studip.php | 36 | ||||
| -rw-r--r-- | lib/classes/restapi/renderer/DebugRenderer.php | 57 | ||||
| -rw-r--r-- | lib/classes/restapi/renderer/DefaultRenderer.php | 74 | ||||
| -rw-r--r-- | lib/classes/restapi/renderer/JSONRenderer.php | 35 |
15 files changed, 0 insertions, 3119 deletions
diff --git a/lib/classes/restapi/ConsumerPermissions.php b/lib/classes/restapi/ConsumerPermissions.php deleted file mode 100644 index 8fc2252..0000000 --- a/lib/classes/restapi/ConsumerPermissions.php +++ /dev/null @@ -1,212 +0,0 @@ -<?php -namespace RESTAPI; -use DBManager, PDO; - -/** - * REST API routing permissions - * - * @author Jan-Hendrik Willms <tleilax+studip@gmail.com> - * @license GPL 2 or later - * @since Stud.IP 3.0 - * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0. - */ -class ConsumerPermissions -{ - /** - * Create a permission object (for a certain consumer). - * Permissions object will be cached for each consumer. - * - * @param mixed $consumer_id Id of consumer (optional, defaults to global) - * @return ConsumerPermissions Returns permissions object - */ - public static function get($consumer_id = null) - { - static $cache = []; - if (!isset($cache[$consumer_id])) { - $cache[$consumer_id] = new self($consumer_id); - } - - return $cache[$consumer_id]; - } - - private $consumer_id; - private $permissions = []; - - /** - * Creates the actual permission object (for a certain consumer). - * - * @param mixed $consumer_id Id of consumer (optional, defaults to global) - */ - private function __construct($consumer_id = null) - { - $this->consumer_id = $consumer_id; - - // Init with global permissions - $this->loadPermissions('global', true); - - // Specific consumers permissions? - if ($consumer_id) { - $this->loadPermissions($consumer_id, false); - } - } - - /** - * Defines whether access if allowed for the current consumer to the - * passed route via the passed method. - * - * @param String $route_id Route template (hash) - * @param String $method HTTP method - * @param mixed $granted Granted state (PHP'ish boolean) - * @param bool $overwrite May values be overwritten - * @return bool Indicates if value could be changed. - */ - public function set($route_id, $method, $granted, $overwrite = false) - { - // If route_id is not an md5 hash, convert it - if (!preg_match('/^[0-9a-f]{32}$/', $route_id)) { - $route_id = md5($route_id); - } - - if (!isset($this->permissions[$route_id])) { - // Skip if not globally set and not allowed to overwrite - if (!$overwrite) { - return false; - } - $this->permissions[$route_id] = []; - } - - // overwrite only if globally allowed - if (!$overwrite && empty($this->permissions[$route_id][$method])) { - return false; - } - - $this->permissions[$route_id][$method] = (bool) $granted; - - return true; - } - - /** - * Convenience method for activating all routes in a route map. - * - * @param \RESTAPI\RouteMap $routemap RouteMap to activate - */ - public function activateRouteMap(RouteMap $routemap) - { - foreach ($routemap->getRoutes() as $method => $routes) { - foreach (array_keys($routes) as $route) { - $this->set($route, $method, true, true); - } - } - - $this->store(); - } - - /** - * Removes stored permissions for a given route and method. - * - * @param String $route_id Route template - * @param String $method HTTP method - * @return bool - */ - public function remove($route_id, $method) - { - if (!isset($this->permissions[$route_id][$method])) { - return false; - } - - unset($this->permissions[$route_id][$method]); - - if (count($this->permissions[$route_id]) === 0) { - unset($this->permissions[$route_id]); - } - - return true; - } - - /** - * Convenience method for deactivating all routes in a route map. - * - * @param \RESTAPI\RouteMap $routemap RouteMap to activate - */ - public function deactivateRouteMap(RouteMap $routemap) - { - foreach ($routemap->getRoutes() as $method => $routes) { - foreach (array_keys($routes) as $route) { - $this->remove($route, $method); - } - } - - $this->store(); - } - - /** - * Loads permissions for passed consumer. - * - * @param String $consumer_id Id of the consumer in question - * @param bool $overwrite May values be overwritten - * @return ConsumerPermissions Returns instance of self to allow chaining - */ - protected function loadPermissions($consumer_id, $overwrite = false) - { - $query = "SELECT route_id, method, granted - FROM api_consumer_permissions - WHERE consumer_id = IFNULL(:consumer_id, 'global')"; - $statement = DBManager::get()->prepare($query); - $statement->bindValue(':consumer_id', $consumer_id); - $statement->execute(); - $permissions = $statement->fetchAll(PDO::FETCH_ASSOC); - - // Init with global permissions - foreach ($permissions as $permission) { - extract($permission); - - $this->set($route_id, $method, $granted, $overwrite); - } - - return $this; - } - - /** - * Checks if access to passed route via passed method is allowed for - * the current consumer. - * - * @param String $route Route template - * @param String $method HTTP method - * @return bool Indicates whether access is allowed - */ - public function check($route, $method) - { - $route_id = md5($route); - - return isset($this->permissions[$route_id][$method]) - && $this->permissions[$route_id][$method]; - } - - /** - * Stores the set permissions. - * - * @return bool Returns true if permissions were stored successfully - */ - public function store() - { - $result = true; - - $query = "INSERT INTO api_consumer_permissions (route_id, consumer_id, method, granted) - VALUES (:route, IFNULL(:consumer_id, 'global'), :method, :granted) - ON DUPLICATE KEY UPDATE granted = VALUES(granted)"; - $statement = DBManager::get()->prepare($query); - $statement->bindValue(':consumer_id', $this->consumer_id); - - foreach ($this->permissions as $route_id => $methods) { - $statement->bindParam(':route', $route_id); - foreach ($methods as $method => $granted) { - $statement->bindParam(':method', $method); - $granted = (int) !empty($granted); - $statement->bindParam(':granted', $granted); - $result = $result && $statement->execute(); - } - } - - return $result; - } -} diff --git a/lib/classes/restapi/Response.php b/lib/classes/restapi/Response.php deleted file mode 100644 index 4417979..0000000 --- a/lib/classes/restapi/Response.php +++ /dev/null @@ -1,164 +0,0 @@ -<?php -namespace RESTAPI; - -/** - * Response class for the rest api - * - * @author <mlunzena@uos.de> - * @license GPL 2 or later - * @since Stud.IP 3.0 - * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0. - */ -class Response implements \ArrayAccess -{ - public $body, $status, $headers; - - /** - * Constructor, sets vital information if provided. - * - * @param String $body Body contents of the response, optional, - * defaults to empty string - * @param int $status HTTP status code, optional, defaults to 200 - * @param Array $headers HTTP headers, optional, defaults to no headers - */ - public function __construct($body = '', $status = 200, $headers = []) - { - $this->body = $body; - $this->status = (int) $status; - $this->headers = (array) $headers; - } - - /** - * Detects whether the response status is of success type (HTTP status 2xx) - * - * @return bool True if status is of success type, false otherwise - */ - public function isSuccess() - { - return 200 <= $this->status && $this->status <= 299; - } - - /** - * Finishes the response with the given response renderer. - * - * @param Renderer\DefaultRenderer $content_renderer Used response renderer, - * only applied if body is - * not a callable closure - */ - public function finish($content_renderer) - { - if (!is_callable($this->body)) { - $content_renderer->render($this); - } - } - - /** - * Sends the response. - */ - public function output() - { - if (isset($this->status)) { - if (mb_strpos(PHP_SAPI, 'cgi') === 0) { - $this->sendHeader(sprintf('Status: %d %s', $this->status, $this->reason())); - } else { - $this->sendHeader(sprintf('HTTP/1.1 %d %s', $this->status, $this->reason())); - } - } - - foreach ($this->headers as $k => $v) { - $this->sendHeader("$k: $v", false, $this->status); - } - - if (is_callable($this->body)) { - call_user_func($this->body); - } else { - echo $this->body; - } - } - - /** - * Internally used function to actually send headers - * - * @param string the HTTP header - * @param bool optional; TRUE if previously sent header should be - * replaced - FALSE otherwise (default) - * @param integer optional; the HTTP response code - * - * @return void - */ - public function sendHeader($header, $replace = FALSE, $status = NULL) { - if (isset($status)) { - header($header, $replace, $status); - } - else { - header($header, $replace); - } - } - - /** - * Returns the reason phrase of this response according to RFC2616. - * - * @return string the reason phrase for this response's status - */ - public function reason() { - $reason = [ - 100 => 'Continue', 'Switching Protocols', - 200 => 'OK', 'Created', 'Accepted', 'Non-Authoritative Information', - 'No Content', 'Reset Content', 'Partial Content', - 300 => 'Multiple Choices', 'Moved Permanently', 'Found', 'See Other', - 'Not Modified', 'Use Proxy', '(Unused)', 'Temporary Redirect', - 400 => 'Bad Request', 'Unauthorized', 'Payment Required','Forbidden', - 'Not Found', 'Method Not Allowed', 'Not Acceptable', - 'Proxy Authentication Required', 'Request Timeout', 'Conflict', - 'Gone', 'Length Required', 'Precondition Failed', - 'Request Entity Too Large', 'Request-URI Too Long', - 'Unsupported Media Type', 'Requested Range Not Satisfiable', - 'Expectation Failed', - 500 => 'Internal Server Error', 'Not Implemented', 'Bad Gateway', - 'Service Unavailable', 'Gateway Timeout', - 'HTTP Version Not Supported']; - - return isset($reason[$this->status]) ? $reason[$this->status] : ''; - } - - // array access methods for headers - - /** - * @todo Add bool return type when Stud.IP requires PHP8 minimal - */ - #[ReturnTypeWillChange] - public function offsetExists($offset) - { - return isset($this->headers[$offset]); - } - - /** - * @param $offset - * @return mixed - * - * @todo Add mixed return type when Stud.IP requires PHP8 minimal - */ - #[\ReturnTypeWillChange] - public function offsetGet($offset) - { - return @$this->headers[$offset]; - } - - /** - * @todo Add void return type when Stud.IP requires PHP8 minimal - */ - #[ReturnTypeWillChange] - public function offsetSet($offset, $value) - { - $this->headers[$offset] = $value; - } - - /** - * @todo Add void return type when Stud.IP requires PHP8 minimal - */ - #[ReturnTypeWillChange] - public function offsetUnset($offset) - { - unset($this->headers[$offset]); - } -} diff --git a/lib/classes/restapi/RouteMap.php b/lib/classes/restapi/RouteMap.php deleted file mode 100644 index b8ad2f4..0000000 --- a/lib/classes/restapi/RouteMap.php +++ /dev/null @@ -1,1060 +0,0 @@ -<?php -namespace RESTAPI; - -use Config; -use Request; -use gossi\docblock\Docblock; - -/** - * RouteMaps define and group routes to resources. - * - * Instances of RouteMaps are registered with the RESTAPI\Router to - * participate in the routing business. - * - * A RouteMap defines at least one handler method which has to be - * annotated with one of these annotations correlating to HTTP request - * methods: - * - * @code - * / * * - * * An example handler method - * * - * * @get /foo - * * @post /bar/:id - * * @put /baz/:id/:other_id - * * @delete / - * * / - * public function anyMethodName($id, $other_id = null) {} - * @endcode - * - * By default, all API routes are unaccessible for nobody users. - * To explicitly allow access for nobody users, add the allow_nobody - * tag to the handler method's doc block. Example: - * - * @code - * / * * - * * Another example handler method - * * - * * @get /foo - * * - * * @allow_nobody - * * / - * @endcode - * - * As soon as the Router matches a HTTP request to a handler defined - * in a RouteMap, it calls RouteMap::init to initialize it and - * especially the instance field `$this->response` of type - * RESTAPI\Response. You do not call RouteMap::init on your own. - * - * After the router has initialized this RouteMap, the router tries to - * call a method `before` of this signature: - * - * @code - * public function before(Router $router, Array $handler, Array $parameters); - * @endcode - * - * The parameter `$handler` is a callable (as in function is_callable) - * consisting of the instance of this RouteMap and the name of a - * method of this instance. You may change the values of this array to - * redirect to another handler. - * - * The parameter `$parameters` is an associative array whose keys - * correlate to the placeholders in the matched URI template. The - * values are the actual values of that placeholders in regard to the - * HTTP request. - * - * - * After calling RouteMap::before control is transfered to the actual - * handler method. The values of the placeholders in the URI template - * of the annotation are send as arguments to the handler. - * - * Example: We have got this handler method defined: - * - * @code - * / * * - * * @get /foo/:id/bar/:other_id - * * / - * public function fooHandler($id, $other_id) { - * } - * @endcode - * - * The router receives a request like this: `http://[..]/foo/1/bar/2` - * and matches it to our `fooHandler` which is then called something - * like that: - * - * @code - * $result = $routeMap->fooHandler(1, 2); - * @endcode - * - * In your handler methods you have to process the input and return - * some output data, which is then rendered in an appropriate way - * after negotiating the content format in the Router. - * - * Thus the return value of your handler method becomes the body of - * the HTTP response. - * - * - * The RouteMap class defines several methods to ease up your work - * with the HTTP specifica. - * - * The methods RouteMap::status, RouteMap::headers and RouteMap::body - * correlate to the components of a HTTP response. - * - * There are helpers for returning paginated collections, see - * RouteMap::paginated. - * - * If you encounter an error or have to stop further processing, see - * methods RouteMap::halt, RouteMap::error and RouteMap::notFound. - * - * These methods are \a DISRUPTIVE as they immediately stop the control - * flow in your handler: - * - * @code - * public function fooHandler($id) - * { - * // do something - * - * $this->halt(); - * - * // this line will never be reached - * } - * @endcode - * - * If you want to simply send a redirection response (HTTP status code - * of 302 or 303), you may find calling RouteMap::redirect helpful. - * - * To generate a URL to a handler, use RouteMap::url - * - * When you find the need to return the content of a file, please see - * RouteMap::sendFile which will help you with streaming it to the - * client. For custom streaming just return a Closure from your - * handler method. - * - * There are several other methods which you may find useful each - * matching a HTTP header: - * - * - RouteMap::contentType - * - RouteMap::etag - * - RouteMap::expires - * - RouteMap::cacheControl - * - RouteMap::lastModified - * - * You can access the data sent in the body of the current HTTP - * request using the `$this->data` instance variable. - * - * - If the request was of Content-Type `application/json`, the - * body of the request is decoded using `json_decode`. - * - If the request was of Content-Type - * `application/x-www-form-urlencoded`, the body of the request is - * decoded using `parse_str`. - * - Otherwise the request will not be parsed and `$this->data` will - * just contain the raw string. - * - * NOTE: The result of the described parsing will always contain - * strings encoded in windows-1252. If the original body - * was UTF-8 encoded, it is automatically re-encoded to windows-1252. - * - * @author Jan-Hendrik Willms <tleilax+studip@gmail.com> - * @author <mlunzena@uos.de> - * @license GPL 2 or later - * @since Stud.IP 3.0 - * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0. - */ -abstract class RouteMap -{ - protected $router; - protected $route; - protected $data = null; - protected $response; - - /** - * Internal property which is used by RouteMap::paginated and - * contains everything about a paginated collection. - */ - protected $pagination = false; - - /** - * The offset into a RouteMap::paginated collection as requested - * by the client. - */ - protected $offset; - - /** - * The limit of a RouteMap::paginated collection as requested - * by the client. - */ - protected $limit; - - /** - * Constructor of the route map. Initializes neccessary offset and limit - * parameters for pagination. - */ - public function __construct() - { - $this->offset = Request::int('offset', 0); - $this->limit = Request::int('limit', Config::get()->ENTRIES_PER_PAGE); - } - - /** - * Initializes the route map by binding it to a router and passing in - * the current route. - * - * @param Router $router Router to bind this route map to - * @param array $route The matched route out of Router::matchRoute; - * an array with keys 'handler', 'conditions' and - * 'source' - */ - public function init($router, $route) - { - $this->router = $router; - $this->route = $route; - $this->response = new Response(); - - if ($mediaType = $this->getRequestMediaType()) { - $this->data = $this->parseRequestBody($mediaType); - } - } - - /** - * Marks this chunk of data as a slice of a larger data set with - * a sum of "total" entries. - * - * @param mixed $data Chunk of data (should be sliced according - * to current offset and limit parameters). - * @param int $total The total number of data entries in the - * according set. - * @param array $uri_params Neccessary parameters when generating uris - * for the current route. - * @param array $query_params Optional query parameters. - */ - public function paginated($data, $total, $uri_params = [], $query_params = []) - { - $uri = $this->url($this->route['uri_template']->inject($uri_params), $query_params); - - $this->paginate($uri, $total); - return $this->collect($data); - } - - - /** - * Low level method for paginating collections. You better use - * RouteMap::paginated instead of this. - * - * Set the pagination data used by the RouteMap::collect. - * - * @param String $uri_format - * @param int $total - * @param mixed $offset - * @param mixed $limit - * - * @return Routemap Returns instance of self to allow chaining - */ - public function paginate($uri_format, $total, $offset = null, $limit = null) - { - $total = (int)$total; - $offset = (int)($offset ?: $this->offset ?: 0); - $limit = (int)($limit ?: $this->limit); - - $this->pagination = compact('uri_format', 'total', 'offset', 'limit'); - - return $this; - } - - /** - * Low level method for paginating collections. You better use - * RouteMap::paginated instead of this. - * - * Adjusts the result set to return a collection. A collection consists - * of the passed data array and the associated pagination information - * if available. - * - * Be aware that the passed data has to be already sliced according to - * the pagination information. - * - * @param array $data Actual dataset - * @return array Collection "object" - */ - public function collect($data) - { - $collection = [ - 'collection' => $data - ]; - if (is_array($this->pagination)) { - extract($this->pagination); - - $offset = $offset - $offset % $limit; - $max = ($total % $limit) - ? $total - $total % $limit - : $total - $limit; - - $pagination = compact('total', 'offset', 'limit'); - if ($total > $limit) { - $links = []; - - foreach ([ - 'first' => 0, - 'previous' => max(0, $offset - $limit), - 'next' => min($max, $offset + $limit), - 'last' => $max] - as $key => $offset) - { - $links[$key] = \URLHelper::getURL($uri_format, compact('offset', 'limit')); - } - - $pagination['links'] = $links; - } - $collection['pagination'] = $pagination; - } - return $collection; - } - - /************************/ - /* REQUEST BODY METHODS */ - /************************/ - - // find the requested media type - private function getRequestMediaType() - { - if (!empty($_SERVER['CONTENT_TYPE'])) { - $contentTypeParts = preg_split('/\s*[;,]\s*/', $_SERVER['CONTENT_TYPE']); - return mb_strtolower($contentTypeParts[0]); - } - } - - // media-types that we know how to process - private static $mediaTypes = [ - 'application/json' => 'parseJson', - 'application/x-www-form-urlencoded' => 'parseFormEncoded', - 'multipart/form-data' => 'parseMultipartFormdata' - ]; - - // cache the request body - private static $_request_body; - - // reads the HTTP request body - private function parseRequestBody($mediaType) - { - // read it only once - if (!isset(self::$_request_body)) { - self::$_request_body = file_get_contents('php://input'); - } - - if (isset(self::$mediaTypes[$mediaType])) { - $result = call_user_func([__CLASS__, self::$mediaTypes[$mediaType]], self::$_request_body); - if ($result) { - return $result; - } - } - return self::$_request_body; - } - - // strategy to decode JSON strings - private static function parseJson($input) - { - return json_decode($input, true); - } - - // strategy to decode form encoded strings - private static function parseFormEncoded($input) - { - parse_str($input, $result); - return $result; - } - - // strategy to decode a multipart message. Used for file-uploads. - private static function parseMultipartFormdata($input) - { - - $data = []; - if (Request::isPost()) { - foreach ($_POST as $key => $value) { - $data[$key] = $value; - } - $data['_FILES'] = $_FILES; - return $data; - } - $boundary = self::getMultipartBoundary(); - if (!$boundary) { - return $data; - } - $input = explode("--".$boundary, $input); - - array_pop($input); - array_shift($input); - - foreach ($input as $part) { - $part = ltrim($part, "\r\n"); - [$head, $body] = explode("\r\n\r\n", $part, 2); - - $tmpheaders = $headers = []; - foreach (explode("\r\n", $head) as $headline) { - if (preg_match('/^[^\s]/', $headline)) { - $lineIsHeader = preg_match('/([^:]+):\s*(.*)$/', $headline, $matches); - if ($lineIsHeader) { - $tmpheaders[] = ['index' => mb_strtolower(trim($matches[1])), 'value' => trim($matches[2])]; - } - } else { - //noch zur letzten Zeile hinzuzählen - end($tmpheaders); - $lastkey = key($tmpheaders); - $tmpheaders[$lastkey]['value'] .= " ".mb_substr($headline, 1); - } - } - foreach ($tmpheaders as $header) { - $headers[$header['index']] = $header['value']; - } - - $contentType = ""; - if (isset($headers['content-type'])) { - preg_match("/^([^;\s]*)/", $headers['content-type'], $matches); - $contentType = mb_strtolower($matches[1]); - } - switch ($headers["transfer-encoding"]) { - case "quoted-printable": - $body = quoted_printable_decode($body); - break; - case "base64": - $body = base64_decode(preg_replace("/(\r?\n|\r)/", "", trim($body))); - break; - case "7bit": - case "8bit": - default: - //nothing to do - } - $matches = []; - preg_match("/name=([^;\s]*)/i", $headers['content-disposition'], $matches); - $name = str_replace(["'", '"'], '', $matches[1]); - if (!$contentType) { - $data[$name] = mb_substr($body, 0, mb_strlen($body) - 2); - } else { - switch ($contentType) { - case 'application/json': - $data = array_merge($data, self::parseJson($body)); - break; - case 'application/x-www-form-urlencoded': - $data = array_merge($data, self::parseFormEncoded($body)); - break; - default: - $matches = []; - preg_match("/filename=([^;\s]*)/i", $headers['content-disposition'], $matches); - if (!$matches[1]) { - preg_match('/filename=([^;\s]*)/i', $headers['content-type'], $matches); - } - $filename = str_replace(["'", '"'], '', $matches[1]); - $tmp_name = $GLOBALS['TMP_PATH']."/uploadfile_".md5(uniqid()); - $handle = fopen($tmp_name, 'wb'); - $filesize = fwrite($handle, $body, (mb_strlen($body) - 2)); - fclose($handle); - $data['_FILES'][$name] = [ - 'name' => $filename, - 'type' => $contentType, - 'tmp_name' => $tmp_name, - 'size' => $filesize - ]; - } - } - } - return $data; - } - - private static function getMultipartBoundary() - { - if ($contentType = $_SERVER['CONTENT_TYPE']) { - foreach (preg_split('/\s*[;,]\s*/', $contentType) as $part) { - if (mb_strtolower(mb_substr($part, 0, 8)) === "boundary") { - $part = explode("=", $part); - return $part[1]; - } - } - } - return null; - } - - - /** - * Set the HTTP status of the current response. - * - * @param integer $status the HTTP status of the response - */ - public function status($status) - { - $this->response->status = $status; - } - - /** - * Set multiple response headers of the current response by - * merging them with already set ones. - * - * @code - * $routemap->headers(array('X-example' => "yep")); - * @endcode - * - * @param array $headers the headers to set - * - * @return array the headers of the current response - */ - public function headers($headers = []) - { - if (sizeof($headers)) { - $this->response->headers = array_merge($this->response->headers, $headers); - } - return $this->response->headers; - } - - /** - * Set the HTTP body of the current response. - * - * @param string $body the body to send back - */ - public function body($body) - { - $this->response->body = $body; - } - - - /** - * Set the Content-Type of the HTTP response given a mime type and - * optionally further parameters as discusses in RFC 2616 14.17. - * - * If no charset is given, it defaults to Stud.IP's 'windows-1252'. - * - * Examples: - * - * @code - * // results in "Content-Type: image/gif" - * $this->contentType('image/gif); - * - * // results in "Content-Type: text/html;charset=ISO-8859-4" - * $this->contentType('text/html;charset=ISO-8859-4'); - * - * // results in "Content-Type: text/html;charset=ISO-8859-4" - * $this->contentType('text/html', array('charset' => 'ISO-8859-4')); - * - * // results in "Content-type: multipart/byteranges; boundary=THIS_STRING_SEPARATES" - * $this->contentType('multipart/byteranges', array('boundary' => 'THIS_STRING_SEPARATES')); - * - * @endcode - * - * @param string $mime_type a string describing a MIME type like 'application/json' - * @param array $params optional parameters as described above - */ - public function contentType($mime_type, $params = []) - { - if (!isset($params['charset'])) { - $params['charset'] = 'utf-8'; - } - - if (mb_strpos($mime_type, 'charset') !== FALSE) { - unset($params['charset']); - } - - if (sizeof($params)) { - $mime_type .= mb_strpos($mime_type, ';') !== FALSE ? ', ' : ';'; - $ps = []; - foreach ($params as $k => $v) { - $ps[] = $k . '=' . $v; - } - $mime_type .= join(', ', $ps); - } - - $this->response['Content-Type'] = $mime_type; - } - - /** - * (Nice) sugar for calling RouteMap::halt and therefore - * as \a DISRUPTIVE. Code after calling RouteMap::error will not - * be evaluated. - * - * @see RouteMap::halt - * - * @param integer $status a number indicating the HTTP status - * code; probably something 4xx or 5xx-ish - * @param string $body optional; the body of the HTTP response - * - */ - public function error($status, $body = null) - { - $this->halt($status, [], $body); - } - - - /** - * Sets the HTTP response's Etag header and halts, if the incoming - * HTTP request was a matching conditional GET using an - * 'If-None-Match' header. Thus it is a possibly \a DISRUPTIVE - * method as it will stop evaluation in that case and send a '304 - * Not Modified'. - * - * Detail: If the request contains an If-Match or If-None-Match - * header set to `*`, a RouteMap assumes a match on safe - * (e.g. GET) and idempotent (e.g. PUT) requests. (In those cases - * it thinks that the resource already exists and therefore - * matches a wildcard.). This can be changed by passing an - * appropriate value for the `$new_resource` parameter. - - * Details of this can be found in RFC 2616 14.24 and 14.26 - * - * @param string $value an identifier uniquely identifying the - * current state of a resource - * @param bool $strong_etag optional; indicates whether the etag - * is a weak or strong (which is the - * default) cache validator. Have a look - * at the RFC for details. - * @param bool $new_resource optional; a way to tell the RouteMap - * that this is a new or existing - * resource. See above. - */ - - public function etag($value, $strong_etag = true, $new_resource = null) - { - // Before touching this code, please double check RFC 2616 - // 14.24 and 14.26. - - if (!isset($new_resource)) { - $new_resource = Request::isPost(); - } - - $value = '"' . $value . '"'; - if (!$strong_etag) { - $value = 'W/' . $value; - } - $this->response['ETag'] = $value; - - if ($this->response->isSuccess() || $this->response->status === 304) { - if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $this->etagMatches($_SERVER['HTTP_IF_NONE_MATCH'], $new_resource)) { - $this->halt($this->isRequestSafe() ? 304 : 412); - } - if (isset($_SERVER['HTTP_IF_MATCH']) - && !$this->etagMatches($_SERVER['HTTP_IF_MATCH'], $new_resource)) { - $this->halt(412); - } - } - } - - // Helper method checking if a ETag value list includes the current ETag. - private function etagMatches($list, $new_resource) - { - if ($list === '*') { - return !$new_resource; - } - - return in_array($this->response['ETag'], - preg_split('/\s*,\s*/', $list)); - } - - // Helper method checking if the request is safe - private function isRequestSafe() - { - $method = Request::method(); - return $method === 'GET' or $method === 'HEAD' or $method === 'OPTIONS' or $method === 'TRACE'; - } - - /** - * This sets the `Expires` header and the `Cache-Control` - * directive `max-age`. - * - * Amount is an integer number of seconds in the future indicating - * when the response should be considered "stale". The - * `$cache_control` parameter is passed to RouteMap#cacheControl - * along with the automatically generated `max_age` directive. - * - * @param int $amount an integer specifying the number of seconds - * this resource will go stale. - * @param array $cache_control optional; more directives for - * RouteMap::cacheControl which is always - * automatically called using the computed max_age - */ - public function expires($amount, $cache_control = []) - { - $time = time() + $amount; - $max_age = $amount; - - $cache_control[] = "max-age=$max_age"; - $this->cacheControl($cache_control); - - $this->response['Expires'] = $this->httpDate($time); - } - - /** - * This sets the Cache-Control header of the HTTP response. - * - * Example: - * - * @code - * $this->cacheControl(array('public', 'must-revalidate')); - * @endcode - * - * @param array $values an array containing Cache-Control - * directives. - */ - public function cacheControl($values) - { - if (is_array($values) && sizeof($values)) { - $this->response['Cache-Control'] = join(', ', $values); - } - } - - /** - * This very important method stops further execution of your - * code. You may specify a status code, headers and the body of - * the resulting response. As the name implies, this method is \a - * DISRUPTIVE and will not return. - * - * @code - * // stops any further code of a route - * $this->halt(); - * - * // you may specify an HTTP status - * $this->halt(409): - * - * // you may specify the HTTP response's body - * $this->halt('my ethereal body') - * - * // or even both - * $this->halt(100, 'Yes, pleazze!') - * - * // giving headers - * $this->halt(417, array('Content-Type' => 'x-not-a-cat'), 'Cats only!') - * @endcode - * - * This method is called by every single \a DISRUPTIVE method. - * - * @param integer $status optional; the response's status code - * @param array $headers optional; (additional) header lines - * which get merged with already set headers - * @param string $body optional; the response's body - */ - public function halt(/* [status], [headers], [body] */) - { - $args = func_get_args(); - $result = []; - - $constraints = [ - 'status' => 'is_int', - 'headers' => 'is_array', - 'body' => function ($i) { return isset($i); } // #existy - ]; - foreach ($constraints as $state => $constraint) { - if ($constraint(current($args))) { - call_user_func([$this, $state], array_shift($args)); - } - } - - throw new RouterHalt($this->response); - } - - /** - * This method sets the Last-Modified header of the HTTP response - * and halts on matching conditional GET requests. Thus this - * method is \a DISRUPTIVE in certain circumstances. - * - * You have to give an integer typed timestamp (in seconds since - * epoch) to specify the data of the last modification to the - * requested resource. - * - * If the current HTTP request contains an `If-Modified-Since` - * header, its value is compared to the specified `$time` - * parameter. Unless the header's value is sooner than the given - * `$time`, further execution is precluded and the RouteMap - * returns with a '304 Not Modified'. - * - * @param integer $time a timestamp described in seconds since epoch - */ - public function lastModified($time) - { - - $this->response['Last-Modified'] = $this->httpDate($time); - - if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) { - return; - } - - if ($this->response->status === 200 - && isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { - // compare based on seconds since epoch - $since = $this->httpdate($_SERVER['HTTP_IF_MODIFIED_SINCE']); - if ($since >= (int) $time) { - $this->halt(304); - } - } - - if (($this->response->isSuccess() || $this->response->status === 412) - && isset($_SERVER['HTTP_IF_UNMODIFIED_SINCE'])) { - - // compare based on seconds since epoch - $since = $this->httpdate($_SERVER['HTTP_IF_UNMODIFIED_SINCE']); - - if ($since < (int) $time) { - $this->halt(412); - } - } - } - - private function httpDate($timestamp) - { - return gmdate('D, d M Y H:i:s \G\M\T', (int) $timestamp); - } - - /** - * Halts execution and returns a '404 Not Found' response. - * - * Sugar for calling RouteMap::error(404) and therefore - * \a DISRUPTIVE. Code after calling RouteMap::notFound will - * not be evaluated. - * - * @see RouteMap::error - * @see RouteMap::halt - * - * @param string $body optional; the body of the HTTP response - */ - public function notFound($body = null) - { - $this->halt(404, $body); - } - - /** - * Stops your code and redirects to the URL provided. This method - * is \a DISRUPTIVE like RouteMap#halt - * - * In addition to the URL you may provide the status code, - * (additional) headers and a request body as you would when - * calling RouteMap#halt. - * - * @code - * $this->redirect('/foo', 201, array('X-Some-Header' => 1234), 'and even a body'); - * @endcode - * - * @see RouteMap::halt - * - * @param string $url the URL to redirect to; it will be filtered - * using RouteMap#url, so you may call it with - * those nice and small strings used in the - * annotations - * @param mixed $args optional; any combinations of the three - * parameters as in RouteMap::halt - */ - public function redirect($url, $args = null) - { - $this->status($_SERVER["SERVER_PROTOCOL"] === 'HTTP/1.1' && !Request::isGet() ? 303 : 302); - $this->response['Location'] = $this->url($url); - - $args = array_slice(func_get_args(), 1); - call_user_func_array([$this, 'halt'], $args); - } - - - /** - * Stops execution of your code and starts sending the specified - * file. This method is \a DISRUPTIVE. - * - * Using the `$opts` parameter you may specify the file's mime - * content type, sending an appropriate 'Content-Type' header, and - * you may specify the 'Content-Disposition' of the file transfer. - * - * Example: - * - * @code - * $this->sendFile('/tmp/c29tZSB0ZXh0', array( - * 'type' => 'image/png', - * 'disposition' => 'inline', - * 'filename' => 'cutecats.png')); - * @endcode - * - * @param string $_path the filesystem path to the file to send - * @param array $opts optional; specify the content type, - * disposition and filename - */ - public function sendFile($_path, $opts = []) - { - $path = realpath($_path); - - if (!file_exists($path)) { - $this->notFound('File to send does not exist'); - } - - if (isset($opts['type'])) { - $this->contentType($opts['type']); - } else if (!isset($this->response['Content-Type'])) { - $this->contentType(get_mime_type($path)); - } - - if ($opts['disposition'] === 'attachment' || isset($opts['filename'])) { - $this->response['Content-Disposition'] = 'attachment; '; - $filename = $opts['filename'] ?: $path; - $this->response['Content-Disposition'] .= encode_header_parameter('filename', basename($filename)); - } - - elseif ($opts['disposition'] === 'inline') { - $this->response['Content-Disposition'] = 'inline'; - } - - // TODO add HTTP 'Range' support - - $size = filesize($path); - $this->response['Content-Length'] = $size; - - // End all potential output buffers - while (ob_get_level() > 0) { - ob_end_clean(); - } - - // Send file - $this->halt(200, $this->response->headers, function () use ($path) { - readfile($path); - }); - } - - - /** - * Generate a URL to a given handler using a URL fragment and URL - * parameters. - * - * Example: - * @code - * // result in something like "/some/path/api.php/course/123/members?status=student" - * $this->url('course/123/members', array('status' => 'student')); - * @endcode - * - * @param string $addr a URL fragment to a handler - * @param array $url_params optional; URL parameters to add to - * the generated URL - * - * @return string the resulting URL - */ - public function url($addr, $url_params = null) - { - $addr = ltrim($addr, '/'); - return \URLHelper::getURL("api.php/$addr", $url_params, true); - } - - /** - * A `vsprintf` like variant to the RouteMap::url method. - * - * Example: - * @code - * // results in "[...]/api.php/foo/some_id?status=student" - * $this->urlf("foo/%s", array("some_id"), array('status' => 'student')); - * @endcode - * - * @param string $addr_f a URL fragment to a handler - * containing sprintf-ish format sequences - * @param array $format_params values to fill into the format markers - * @param array $url_params optional; URL parameters to add to - * the generated URL - * - * @return string the resulting URL - */ - - public function urlf($addr_f, $format_params, $url_params = null) - { - if (!is_array($format_params)) { - $format_params = [$format_params]; - } - return $this->url(vsprintf($addr_f, $format_params), $url_params); - } - - /** - * Returns a list of all the routes this routemap provides. - * - * @param string $http_method Return only the routes for this specific - * http method (optional) - * - * @return array of all routes grouped by method - */ - public function getRoutes($http_method = null) - { - $ref = new \ReflectionClass($this); - - if ($ref->getDocComment()) { - $docblock = new Docblock($ref); - $class_conditions = $this->extractConditions($docblock); - } else { - $class_conditions = []; - } - - - // Create result array by creating an associative array from all - // supported methods as keys - $routes = array_fill_keys(Router::getSupportedMethods(), []); - - // Restrict routes to given http method (if given) - if ($http_method !== null) { - $routes = [$http_method => []]; - } - - // Iterate through all methods of the routemap - foreach ($ref->getMethods( \ReflectionMethod::IS_PUBLIC) as $ref_method) { - // No docblock? Not an api route! - if (!$ref_method->getDocComment()) { - continue; - } - - // Parse docblock - $docblock = new Docblock($ref_method); - - // No docblock tags? Not an api route! - if ($docblock->getTags()->isEmpty()) { - continue; - } - - // Any specific condition to consider? - $conditions = $this->extractConditions($docblock, $class_conditions); - - // Iterate through all possible methods in order to identify - // any according docblock tags - $allow_nobody = $docblock->hasTag('allow_nobody'); - foreach (array_keys($routes) as $http_method) { - if (!$docblock->hasTag($http_method)) { - //The tag for the current HTTP method cannot be found - //in the route's DocBlock tags. - continue; - } - - // Route all defined method and uri template combinations to - // the according methods of the object. - foreach ($docblock->getTags($http_method) as $tag) { - $uri_template = trim($tag->getDescription()); - $routes[$http_method][$uri_template] = [ - 'handler' => [$this, $ref_method->name], - 'conditions' => $conditions, - 'description' => trim($docblock->getShortDescription()) ?: false, - 'allow_nobody' => $allow_nobody - ]; - } - } - } - - // Return all routes grouped or just the routes for the wanted method - return func_num_args() === 1 - ? reset($routes) - : $routes; - } - - /** - * Extracts defined conditions from a given docblock. - * - * @param Docblock $docblock DocBlock to examine - * @param array $conditions Optional array of already defined - * conditions to extend - * @return array of all extracted conditions with the variable name - * as key and pattern to match as value - */ - protected function extractConditions($docblock, $conditions = []) - { - foreach ($docblock->getTags('condition') as $condition) { - [$var, $pattern] = explode(' ', $condition->getDescription(), 2); - $conditions[$var] = $pattern; - } - - return $conditions; - } - - /** - * Returns the response object - * @return Response - */ - public function getResponse(): Response - { - return $this->response; - } -} 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; - } -} diff --git a/lib/classes/restapi/RouterException.php b/lib/classes/restapi/RouterException.php deleted file mode 100644 index 1ce2afc..0000000 --- a/lib/classes/restapi/RouterException.php +++ /dev/null @@ -1,31 +0,0 @@ -<?php -namespace RESTAPI; -use \Exception; - -/** - * Router exception. - * - * @author Jan-Hendrik Willms <tleilax+studip@gmail.com> - * @author <mlunzena@uos.de> - * @license GPL 2 or later - * @since Stud.IP 3.0 - * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0. - */ -class RouterException extends Exception -{ - protected static $error_messages = [ - 400 => 'Bad Request', - 401 => 'Unauthorized', - 403 => 'Forbidden', - 404 => 'Not Found', - 405 => 'Method Not Allowed', - 500 => 'Internal Server Error', - 501 => 'Not implemented', - ]; - - public function __construct($code = 500, $message = '', $previous = null) - { - $message = $message ?: self::$error_messages[$code] ?: ''; - parent::__construct($message, $code, $previous); - } -} diff --git a/lib/classes/restapi/RouterHalt.php b/lib/classes/restapi/RouterHalt.php deleted file mode 100644 index 55a2ca1..0000000 --- a/lib/classes/restapi/RouterHalt.php +++ /dev/null @@ -1,19 +0,0 @@ -<?php -namespace RESTAPI; - -/** - * @author <mlunzena@uos.de> - * @license GPL 2 or later - * @since Stud.IP 3.0 - * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0. - */ -class RouterHalt extends \Exception -{ - public $response; - - public function __construct($response) - { - parent::__construct(); - $this->response = $response; - } -} diff --git a/lib/classes/restapi/UriTemplate.php b/lib/classes/restapi/UriTemplate.php deleted file mode 100644 index 67161de..0000000 --- a/lib/classes/restapi/UriTemplate.php +++ /dev/null @@ -1,115 +0,0 @@ -<?php -namespace RESTAPI; - -/** - * @author Jan-Hendrik Willms <tleilax+studip@gmail.com> - * @author <mlunzena@uos.de> - * @license GPL 2 or later - * @since Stud.IP 3.0 - * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0. - */ -class UriTemplate -{ - public $uri_template; - public $conditions; - - public function __construct($uri_template, $conditions = []) - { - $this->uri_template = $uri_template; - $this->conditions = $conditions; - } - - /** - * Tests whether an uri matches a template. - * - * The template may contain placeholders by prefixing an appropriate, - * unique placeholder name with a colon (:). - * - * <code>$template = '/hello/:name';</code> - * - * If the uri matches the template, all evaluated placeholders will - * be stored in the parameters array. - * - * @param String $uri The uri to test - * @param array $parameters Stores evaluated parameters on match (optional) - * - * @return bool Returns true if the uri matches the template - */ - public function match($uri, &$parameters = null) - { - // Initialize parameters array - $parameters = []; - - // Split and normalize uri and template - $given = array_filter(explode('/', $uri), 'mb_strlen'); - $rules = array_filter(explode('/', $this->uri_template)); - - // Leave if uri and template do not contain the same number of - // elements - if (count($given) !== count($rules)) { - return false; - } - - // Combine uri and template element-wise (simplifies iteration) - $combined = array_combine($rules, $given); - - // Iterate over uri and template and compare element by element - foreach ($combined as $rule => $actual) { - if ($rule[0] === ':') { - // Rule is a placeholder - $parameter_name = mb_substr($rule, 1); - - if (isset($this->conditions[$parameter_name]) - && !preg_match($this->conditions[$parameter_name], $actual)) { - return false; - } - - $parameters[$parameter_name] = $actual; - - } elseif ($actual !== $rule) { - // Elements do not match - $parameters = []; - return false; - } - } - - return true; - } - - - public function inject($params) - { - // Initialize parameters array - $parameters = []; - - // Split and normalize template - $rules = array_filter(explode('/', $this->uri_template)); - - foreach ($rules as &$rule) { - - // Rule is a placeholder - if ($rule[0] === ':') { - $parameter_name = mb_substr($rule, 1); - - if (!isset($params[$parameter_name])) { - $reason = sprintf('UriTemplate parameter :%s missing.', - htmlReady($parameter_name)); - throw new \RuntimeException($reason); - } - - $actual = $params[$parameter_name]; - - if (isset($this->conditions[$parameter_name]) - && !preg_match($this->conditions[$parameter_name], $actual)) { - $reason = sprintf('UriTemplate parameter :%s did not satisfy its condition.', - htmlReady($parameter_name)); - throw new \RuntimeException($reason); - } - - $rule = htmlReady($actual); - } - } - - return join('/', $rules); - } -} diff --git a/lib/classes/restapi/UserPermissions.php b/lib/classes/restapi/UserPermissions.php deleted file mode 100644 index dcf1601..0000000 --- a/lib/classes/restapi/UserPermissions.php +++ /dev/null @@ -1,144 +0,0 @@ -<?php -namespace RESTAPI; -use DBManager, PDO; - -/** - * REST API routing permissions - * - * @author Jan-Hendrik Willms <tleilax+studip@gmail.com> - * @license GPL 2 or later - * @since Stud.IP 2.6 - * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0. - */ -class UserPermissions -{ - /** - * Create a permission object (for a certain user). - * Permissions object will be cached for each user. - * - * @param mixed $user_id Id of user (optional, defaults to global) - * @return UserPermissions Returns permissions object - */ - public static function get($user_id = null) - { - $user_id = $user_id ?: $GLOBALS['user']->id; - - static $cache = []; - if (!isset($cache[$user_id])) { - $cache[$user_id] = new self($user_id); - } - - return $cache[$user_id]; - } - - private $user_id; - private $permissions = []; - - /** - * Creates the actual permission object (for a certain user). - * - * @param mixed $user_id Id of user (optional, defaults to global) - */ - private function __construct($user_id = null) - { - $this->user_id = $user_id; - - // Init with global permissions - $this->loadPermissions(); - } - - /** - * Defines whether access is allowed for the current user to the - * passed route via the passed method. - * - * @param String $user_id Id of the user - * @param mixed $granted Granted state (PHP'ish boolean) - * @return UserPermissions Returns instance of self to allow chaining - */ - public function set($user_id, $granted = true) - { - $this->permissions[$user_id] = (bool)$granted; - - return $this; - } - - /** - * Loads permissions for passed user. - * - * @return UserPermissions Returns instance of self to allow chaining - */ - protected function loadPermissions() - { - $query = "SELECT consumer_id, granted - FROM api_user_permissions - WHERE user_id = :user_id"; - $statement = DBManager::get()->prepare($query); - $statement->bindValue(':user_id', $this->user_id); - $statement->execute(); - $permissions = $statement->fetchAll(PDO::FETCH_ASSOC); - - // Init with global permissions - foreach ($permissions as $permission) { - extract($permission); - - $this->set($permission['consumer_id'], $permission['granted']); - } - - return $this; - } - - /** - * Checks if access to consumer is allowed for the current user. - * - * @param String $consumer_id Id of the consumer - * @return bool Indicates whether access is allowed - */ - public function check($consumer_id) - { - return isset($this->permissions[$consumer_id]) - && $this->permissions[$consumer_id]; - } - - /** - * Stores the set permissions. - * - * @return bool Returns true if permissions were stored successfully - */ - public function store() - { - $result = true; - - $query = "INSERT INTO api_user_permissions (user_id, consumer_id, granted, mkdate, chdate) - VALUES (:user_id, :consumer_id, :granted, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()) - ON DUPLICATE KEY UPDATE granted = VALUES(granted), - chdate = UNIX_TIMESTAMP()"; - $statement = DBManager::get()->prepare($query); - $statement->bindValue(':user_id', $this->user_id); - - foreach ($this->permissions as $consumer_id => $granted) { - $statement->bindValue(':consumer_id', $consumer_id); - $statement->bindValue(':granted', (int) !empty($granted)); - - $result = $result && $statement->execute(); - } - - return $result; - } - - /** - * Get a list of all consumer the user has granted acces to. - * - * @return array List of consumer objects - */ - public function getConsumers() - { - $result = []; - foreach ($this->permissions as $consumer_id => $granted) { - if (!$granted) { - continue; - } - $result[$consumer_id] = Consumer\Base::find($consumer_id); - } - return $result; - } -} diff --git a/lib/classes/restapi/consumer/Base.php b/lib/classes/restapi/consumer/Base.php deleted file mode 100644 index 50f3150..0000000 --- a/lib/classes/restapi/consumer/Base.php +++ /dev/null @@ -1,226 +0,0 @@ -<?php -namespace RESTAPI\Consumer; - -use AuthUserMd5; -use DBManager; -use DBManagerException; -use PDO; - -/** - * Base consumer class for the rest api - * - * Consumers provide means for authenticating a user and the access - * permissions for routes are bound to specific consumers. - * - * @author Jan-Hendrik Willms <tleilax+studip@gmail.com> - * @license GPL 2 or later - * @since Stud.IP 3.0 - * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0. - */ -abstract class Base extends \SimpleORMap -{ - /** - * Each consumer type has to implement a detect feature which - * should extract crucial information from the request and return - * an instance of itself if the consumer detects a valid signature - * it can respond to. - * - * @param mixed $request_type Type of request (optional; defaults to any) - * @return mixed Detected consumer object or false - */ - abstract public static function detect($request_type = null); - - /* Concrete */ - - /** - * Configures the model. - * - * @param array $config Configuration array - */ - protected static function configure($config = []) - { - $config['db_table'] = 'api_consumers'; - - parent::configure($config); - } - - /** - * Stores all known consumer types - */ - protected static $known_types = []; - - /** - * Add a consumer type to the list of consumer types - * - * @param String $type Name of the type - * @param String $class Associated consumer class - */ - public static function addType($type, $class) - { - self::$known_types[$type] = $class; - } - - /** - * Removes a consumer type from the list of consumer types - * - * @param String $type Name of the type - */ - public static function removeType($type) - { - unset(self::$known_types[$type]); - } - - /** - * Overloaded find method. Will return a concrete specialized consumer - * object of the associated type. - * - * @param String $id Id of the consumer - * @return \RESTAPI\Consumer\Base Associated consumer object (derived - * from consumer base type) - * @throws \Exception if either consumer id or consumer type is invalid - */ - public static function find($id) - { - $query = "SELECT consumer_type - FROM api_consumers - WHERE consumer_id = :id"; - $statement = DBManager::get()->prepare($query); - $statement->bindValue(':id', $id); - $statement->execute(); - $type = $statement->fetchColumn(); - - if (!isset(self::$known_types[$type])) { - throw new \Exception('Consumer #' . $id . ' is of unknown type "' . $type . '"'); - } - - return new self::$known_types[$type]($id); - } - - /** - * Returns a list of all known consumers. - * - * @return array List of all known consumers (as specialized consumer - * objects) - */ - public static function findAll() - { - $query = "SELECT consumer_id FROM api_consumers"; - $statement = DBManager::get()->query($query); - $ids = $statement->fetchAll(PDO::FETCH_COLUMN); - - return array_map([self::class, 'find'], $ids); - } - - /** - * Creates a new consumer of the given type. - * - * @param String $type Name of the type - * @return \RESTAPI\Consumer\Base Consumer object of the given (derived - * from consumer base type) - * @throws \Exception if type is invalid - */ - public static function create($type) - { - if (!isset(self::$known_types[$type])) { - throw new \Exception('Consumer is of unknown type "' . $type . '"'); - } - - return new self::$known_types[$type]; - } - - /** - * This method is used to detect a consumer (of a specific type) by - * executing the detect method on all known consumer types. - * - * @param mixed $type Name of the type (optional; defaults to all types) - * @param mixed $request_type Type of request (optional; defaults to any) - * @return mixed Either the detected consumer or false if no consumer - * was detected - * @throws \Exception if type is invalid - */ - public static function detectConsumer($type = null, $request_type = null) - { - $needles = $type === null - ? array_keys(self::$known_types) - : [$type]; - foreach ($needles as $needle) { - if (!isset(self::$known_types)) { - throw new \Exception('Trying to detect consumer of unkown type "' . $needle . '"'); - } - $consumer_class = self::$known_types[$needle]; - if ($consumer = $consumer_class::detect($request_type)) { - return $consumer; - } - } - return false; - } - - /** - * Contains user information - */ - protected $user = null; - - /** - * Extended SimpleORMap constructor. A certain user can be injected upon - * creation. - * - * @param mixed $id Id of the consumer or null to create a new one - * @param mixed $user Either a user object or id to inject to the consumer - * or null if no user should be injected - */ - public function __construct($id = null, $user = null) - { - parent::__construct($id); - - if ($user !== null) { - $this->setUser($user); - } - } - - /** - * Retrieve the api permissions associated with this consumer. - * - * @return \RESTAPI\ConsumerPermissions Permission object for this consumer - */ - public function getPermissions() - { - return \RESTAPI\ConsumerPermissions::get($this->id); - } - - /** - * Inject a user to this consumer. Injecting in this context refers to - * "having a user authenticated by this consumer". - * - * @param mixed $user Either a user object or a user id - * @return \RESTAPI\Consumer\Base Returns instance of self to allow - * chaining - */ - public function setUser($user) - { - if (!is_object($user)) { - $user = \User::findFull($user); - } - $this->user = $user; - return $this; - } - - /** - * Returns whether the consumer has an injected user or not. - * - * @return bool True if a valid user is found, false otherwise - */ - public function hasUser() - { - return $this->user !== null && $this->user->id && $this->user->id !== 'nobody'; - } - - /** - * Return the injected user. - * - * @param mixed User object or false if no user was injected - */ - public function getUser() - { - return $this->user; - } -} diff --git a/lib/classes/restapi/consumer/HTTP.php b/lib/classes/restapi/consumer/HTTP.php deleted file mode 100644 index 97b0657..0000000 --- a/lib/classes/restapi/consumer/HTTP.php +++ /dev/null @@ -1,50 +0,0 @@ -<?php -namespace RESTAPI\Consumer; -use StudipAuthAbstract, RESTAPI\RouterException; - -/** - * Basic HTTP Authentication consumer for the rest api - * - * @author Jan-Hendrik Willms <tleilax+studip@gmail.com> - * @license GPL 2 or later - * @since Stud.IP 3.0 - * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0. - */ -class HTTP extends Base -{ - /** - * Detects if a user is authenticated via basic http authentication. - * The only supported authentication for now is via the url: - * - * http://username:password@host/path?query - * - * @param mixed $request_type Type of request (optional; defaults to any) - * @return mixed Instance of self if authentication was detected, false - * otherwise - * @throws RouterException if authentication fails - * @todo Integrate and test HTTP_AUTHORIZATION header authentication - */ - public static function detect($request_type = null) - { - if ( - isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) - || isset($_SERVER['HTTP_AUTHORIZATION']) - ) { - $user_id = false; - - if (isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) { - $username = $_SERVER['PHP_AUTH_USER']; - $password = $_SERVER['PHP_AUTH_PW']; - } elseif (isset($_SERVER['HTTP_AUTHORIZATION'])) { - list($username, $password) = explode(':', base64_decode(mb_substr($_SERVER['HTTP_AUTHORIZATION'], 6))); - } - - $check = StudipAuthAbstract::CheckAuthentication($username, $password); - if ($check['uid'] && $check['uid'] !== 'nobody') { - return new self(null, $check['uid']); - } - - } - return false; - } -} diff --git a/lib/classes/restapi/consumer/OAuth.php b/lib/classes/restapi/consumer/OAuth.php deleted file mode 100644 index caf51c2..0000000 --- a/lib/classes/restapi/consumer/OAuth.php +++ /dev/null @@ -1,231 +0,0 @@ -<?php -namespace RESTAPI\Consumer; -use StudipAutoloader, DBManager, OAuthRequestVerifier, OAuthStore, OAuthServer, Exception; -use \RESTAPI\UserPermissions; - -StudipAutoloader::addAutoloadPath($GLOBALS['STUDIP_BASE_PATH'] . DIRECTORY_SEPARATOR . 'vendor/oauth-php/library/'); - -/** - * OAuth consumer for the rest api - * - * @author Jan-Hendrik Willms <tleilax+studip@gmail.com> - * @license GPL 2 or later - * @since Stud.IP 3.0 - * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0. - */ -class OAuth extends Base -{ - /** - * Configures the model. - * - * @param array $config Configuration array - */ - protected static function configure($config = []) - { - $config['default_values']['consumer_type'] = 'oauth'; - - $config['registered_callbacks']['before_store'][] = 'before_store'; - - parent::configure($config); - } - - /** - * Detects whether the request is authenticated via OAuth. - * - * @param mixed $request_type Type of request (optional; defaults to any) - * @return mixed Instance of self if authentication was detected, false - * otherwise - */ - public static function detect($request_type = null) - { - if (OAuthRequestVerifier::requestIsSigned() && $request_type !== 'request') { - $user_id = false; - - $parameters = (in_array($_SERVER['REQUEST_METHOD'], ['GET', 'POST'])) - ? null - : $GLOBALS['_' . $_SERVER['REQUEST_METHOD']]; - - $req = new OAuthRequestVerifier(null, null, $parameters); - - // Check oauth timestamp and deny access if timestamp is outdated - if ($req->getParam('oauth_timestamp') < strtotime('-6 hours')) { - return false; - } - $result = $req->verifyExtended('access'); - - // @todo - # self::$consumer_key = $result['consumer_key']; - - $query = "SELECT user_id FROM api_oauth_user_mapping WHERE oauth_id = :oauth_id"; - $statement = DBManager::get()->prepare($query); - $statement->bindValue(':oauth_id', $result['user_id']); - $statement->execute(); - $user_id = $statement->fetchColumn(); - - if (!$user_id) { - return false; - } - - $consumer = reset(self::findByAuth_Key($result['consumer_key'])); - $consumer->setUser($user_id); - return $consumer; - } else { - try { - // Check if there is a valid request token in the current request - // Returns an array with the consumer key, consumer secret, token, token secret and token type. - $rs = self::getServer()->authorizeVerify(); - - $query = "SELECT consumer_id - FROM api_consumers - WHERE auth_key = :key"; - $statement = DBManager::get()->prepare($query); - $statement->bindValue(':key', $rs['consumer_key']); - $statement->execute(); - $id = $statement->fetchColumn(); - - if ($id) { - return new self($id); - } - } catch (Exception $e) { - } - } - return false; - } - - /** - * Returns a singleton instance of the oauth server. - * - * @return OAuthServer The server object - */ - public static function getServer() - { - static $server = null; - if ($server === null) { - $server = new OAuthServer(null, null, null, 'SESSION', [], [ - 'allowed_uri_schemes' => [] - ]); - } - return $server; - } - - /** - * "Before store" trigger. Creates a clone of the consumer in the - * tables for the vendor oauth library. - */ - protected function before_store() - { - static $mapping = [ - 'auth_key' => 'consumer_key', - 'auth_secret' => 'consumer_secret', - 'active' => 'enabled', - 'contact' => 'requester_name', - 'email' => 'requester_email', - 'callback' => 'callback_uri', - 'url' => 'application_uri', - 'title' => 'application_title', - 'description' => 'application_descr', - 'notes' => 'application_notes', - 'type' => 'application_type', - 'commercial' => 'application_commercial', - ]; - - $consumer = []; - foreach ($mapping as $from => $to) { - $consumer[$to] = $this->$from; - } - - $query = "SELECT osr_id - FROM oauth_server_registry - WHERE osr_consumer_key = :key AND osr_consumer_secret = :secret"; - $statement = DBManager::get()->prepare($query); - $statement->bindValue(':key', $this->auth_key); - $statement->bindValue(':secret', $this->auth_secret); - $statement->execute(); - $consumer['id'] = $statement->fetchColumn(); - - $consumer_key = OAuthStore::instance('PDO')->updateConsumer($consumer, null, true); - - if ($this->isNew()) { - $consumer = OAuthStore::instance('PDO')->getConsumer($consumer_key, null, true); - $this->auth_key = $consumer['consumer_key']; - $this->auth_secret = $consumer['consumer_secret']; - } - } - - /** - * Grant oauth access for a user. - * - * @param mixed $user_id Specific user id or null to default to the - * injected user - * @throws Exception If no valid user is present - */ - public function grantAccess($user_id = null) - { - if ($user_id === null && $this->hasUser()) { - $user_id = $this->user->id; - } - if (!$user_id) { - throw new Exception('Can not grant access to unknown user'); - } - - UserPermissions::get($GLOBALS['user']->id)->set($this->id, true)->store(); - return self::getServer()->authorizeFinish(true, self::getOAuthId($user_id)); - } - - /** - * Revoke oauth access from a user. - * - * @param mixed $user_id Specific user id or null to default to the - * injected user - * @throws Exception If no valid user is present - */ - public function revokeAccess($user_id = null) - { - if ($user_id === null && $this->hasUser()) { - $user_id = $this->user->id; - } - if (!$user_id) { - throw new Exception('Can not revoke access from unknown user'); - } - - $query = "DELETE oauth_server_token - FROM oauth_server_token - JOIN oauth_server_registry - WHERE ost_usa_id_ref = :id AND osr_consumer_key = :key AND osr_consumer_secret = :secret"; - $statement = DBManager::get()->prepare($query); - $statement->bindValue(':id', self::getOAuthId($user_id)); - $statement->bindValue(':key', $this->auth_key); - $statement->bindValue(':secret', $this->auth_secret); - $statement->execute(); - - UserPermissions::get($GLOBALS['user']->id)->set($this->id, false)->store(); - return self::getServer()->authorizeFinish(false, self::getOAuthId($user_id)); - } - - /** - * Maps a user to an oauth id. This is neccessary due to the fact that - * the oauth lib works with different ids than Stud.IP. - * - * @param String $user_id Id of the user to get an oauth id for - * @return String The mapped oauth id - */ - public static function getOAuthId($user_id) - { - $query = "SELECT oauth_id FROM api_oauth_user_mapping WHERE user_id = :id"; - $statement = DBManager::get()->prepare($query); - $statement->bindValue(':id', $user_id); - $statement->execute(); - $oauth_id = $statement->fetchColumn(); - - if (!$oauth_id) { - $query = "INSERT INTO api_oauth_user_mapping (user_id, mkdate) - VALUES (:id, UNIX_TIMESTAMP())"; - $statement = DBManager::get()->prepare($query); - $statement->bindValue(':id', $user_id); - $statement->execute(); - $oauth_id = DBManager::get()->lastInsertId(); - } - - return $oauth_id; - } -} diff --git a/lib/classes/restapi/consumer/Studip.php b/lib/classes/restapi/consumer/Studip.php deleted file mode 100644 index 738dd75..0000000 --- a/lib/classes/restapi/consumer/Studip.php +++ /dev/null @@ -1,36 +0,0 @@ -<?php -namespace RESTAPI\Consumer; - -/** - * Stud.IP Session Consumer for the rest api - * - * @author Jan-Hendrik Willms <tleilax+studip@gmail.com> - * @license GPL 2 or later - * @since Stud.IP 3.0 - * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0. - */ -class Studip extends Base -{ - /** - * Detects a user via the Stud.IP session. If a session is present and - * valid, the auth and user object have already been set up by stud.ip - * functions, so we just need to check if these are present. - * - * @param mixed $request_type Type of request (optional; defaults to any) - * @return mixed Instance of self if authentication was detected, false - * otherwise - */ - public static function detect($request_type = null) - { - if ( - !isset($GLOBALS['auth']) - || !$GLOBALS['auth']->is_authenticated() - || $GLOBALS['user']->id === 'nobody' - || !\CSRFProtection::verifyRequest() - ) { - return false; - } - - return new self(null, $GLOBALS['user']->id); - } -} diff --git a/lib/classes/restapi/renderer/DebugRenderer.php b/lib/classes/restapi/renderer/DebugRenderer.php deleted file mode 100644 index afd56f6..0000000 --- a/lib/classes/restapi/renderer/DebugRenderer.php +++ /dev/null @@ -1,57 +0,0 @@ -<?php -namespace RESTAPI\Renderer; - -/** - * Debug content renderer. - * - * @author Jan-Hendrik Willms <tleilax+studip@gmail.com> - * @author <mlunzena@uos.de> - * @license GPL 2 or later - * @since Stud.IP 3.0 - * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0. - */ -class DebugRenderer extends DefaultRenderer -{ - /** - * Returns an associated content type. - */ - public function contentType() - { - return 'text/plain'; - } - - /** - * Returns an associated extension. - */ - public function extension() - { - return '.debug'; - } - - /** - * Response transformation function. - * - * @param \RESTAPI\Response $response the response to transform - */ - public function render($response) - { - if (!isset($response['Content-Type'])) { - $response['Content-Type'] = $this->contentType() . ';charset=utf-8'; - } - - $debug = function ($label, $data) { - echo str_pad('', 78, '=') . PHP_EOL; - echo str_pad('- ' . $label, 77, ' ') . '-' . PHP_EOL; - echo str_pad('', 78, '=') . PHP_EOL; - var_export($data); - echo PHP_EOL; - }; - - ob_start(); - $debug('Response Status', $response->status); - $debug('Response Header', $response->headers); - $debug('Response Body', $response->body); - $debug('Request', $GLOBALS['_' . $_SERVER['REQUEST_METHOD']]); - $response->body = ob_get_clean(); - } -} diff --git a/lib/classes/restapi/renderer/DefaultRenderer.php b/lib/classes/restapi/renderer/DefaultRenderer.php deleted file mode 100644 index 836ba36..0000000 --- a/lib/classes/restapi/renderer/DefaultRenderer.php +++ /dev/null @@ -1,74 +0,0 @@ -<?php -namespace RESTAPI\Renderer; - -/** - * Default base content renderer class (outputs text/plain). - * - * Content renderers are output filters that can reshape data before it - * is sent to the client. - * Each content renderer is associated with a certain content type and a - * certain file extension. This is neccessary for content negotiation. - * - * @author Jan-Hendrik Willms <tleilax+studip@gmail.com> - * @author <mlunzena@uos.de> - * @license GPL 2 or later - * @since Stud.IP 3.0 - * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0. - */ -class DefaultRenderer -{ - /** - * Returns an associated content type. - * - * @return String Content/mime type for this renderer - */ - public function contentType() - { - return 'text/plain'; - } - - /** - * Returns an associated extension. - * - * @return String Associated extension for this renderer. - */ - public function extension() - { - return ''; - } - - /** - * Response transformation function. - * - * @param \RESTAPI\Response $response the response to transform - */ - public function render($response) - { - if (!isset($response['Content-Type'])) { - $response['Content-Type'] = $this->contentType() . ';charset=utf-8'; - } - } - - /** - * Detects whether the renderer should respond to either a certain - * filename (tests by extension) or to a certain media range. - * - * @param String $filename Filename to test against - * @param mixed $media_range Media range to test against (optional, - * defaults to request's accept header if set) - * @return bool Returns whether the renderer should respond - */ - public function shouldRespondTo($filename, $media_range = null) - { - // If no media range is passed, evalute http header "Accept" - if ($media_range === null && isset($_SERVER['ACCEPT'])) { - $media_ranges = explode(';', $_SERVER['ACCEPT']); - $media_range = reset($media_ranges); - } - - // Test if either the filename has the appropriate extension or - // if the client accepts the content type - return ($this->extension() && fnmatch('*' . $this->extension(), $filename)) - || ($media_range && fnmatch($media_range, $this->contentType())); - } -} diff --git a/lib/classes/restapi/renderer/JSONRenderer.php b/lib/classes/restapi/renderer/JSONRenderer.php deleted file mode 100644 index 9c6e449..0000000 --- a/lib/classes/restapi/renderer/JSONRenderer.php +++ /dev/null @@ -1,35 +0,0 @@ -<?php -namespace RESTAPI\Renderer; - -/** - * Content renderer for json content. - * - * @author Jan-Hendrik Willms <tleilax+studip@gmail.com> - * @author <mlunzena@uos.de> - * @license GPL 2 or later - * @since Stud.IP 3.0 - * @deprecated Since Stud.IP 5.0. Will be removed in Stud.IP 6.0. - */ -class JSONRenderer extends DefaultRenderer -{ - public function contentType() - { - return 'application/json'; - } - - public function extension() - { - return '.json'; - } - - public function render($response) - { - if (!isset($response['Content-Type'])) { - $response['Content-Type'] = $this->contentType() . ';charset=utf-8'; - } - - if (isset($response->body)) { - $response->body = json_encode($response->body); - } - } -} |
