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/RouteMap.php | |
| parent | da0022e5c1abbf9825ae76debaabdff7e8623bb4 (diff) | |
| parent | 97a188592c679890a25c37ab78463add76a52ff7 (diff) | |
Merge branch 'main' into issue-3911issue-3911
Diffstat (limited to 'lib/classes/restapi/RouteMap.php')
| -rw-r--r-- | lib/classes/restapi/RouteMap.php | 1060 |
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; - } -} |
