aboutsummaryrefslogtreecommitdiff
path: root/lib/classes/restapi/RouteMap.php
diff options
context:
space:
mode:
authorPhilipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de>2024-09-24 10:53:31 +0200
committerPhilipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de>2024-09-24 10:53:31 +0200
commit4459dd7917f4d1c34f40bb68f0e991e9c3d53e4c (patch)
tree5c07151ae61276d334e88f6309c30d439a85c12e /lib/classes/restapi/RouteMap.php
parentda0022e5c1abbf9825ae76debaabdff7e8623bb4 (diff)
parent97a188592c679890a25c37ab78463add76a52ff7 (diff)
Merge branch 'main' into issue-3911issue-3911
Diffstat (limited to 'lib/classes/restapi/RouteMap.php')
-rw-r--r--lib/classes/restapi/RouteMap.php1060
1 files changed, 0 insertions, 1060 deletions
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;
- }
-}