summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorArnold Daniels <arnold@jasny.net>2014-10-08 12:40:22 +0200
committerArnold Daniels <arnold@jasny.net>2014-10-08 12:40:22 +0200
commit47c0dd632bb05496f16448141f3a77dae639f84d (patch)
tree32b9c5bbf119cd9cb944137a3194b759d3a28edd
parent48af26269f1b3b7e55faee49e0a06572d8edfe50 (diff)
downloadrouter-47c0dd632bb05496f16448141f3a77dae639f84d.zip
router-47c0dd632bb05496f16448141f3a77dae639f84d.tar.gz
router-47c0dd632bb05496f16448141f3a77dae639f84d.tar.bz2
Added `Request::supportInputType()` and `Request::acceptOrigin`
Router no longer exits Added Success response functions to Controller Added response checks to Controller
-rw-r--r--src/Controller.php134
-rw-r--r--src/Request.php361
-rw-r--r--src/Router.php126
3 files changed, 462 insertions, 159 deletions
diff --git a/src/Controller.php b/src/Controller.php
index 8a2381f..b3c8d46 100644
--- a/src/Controller.php
+++ b/src/Controller.php
@@ -32,6 +32,8 @@ abstract class Controller
public function __construct($router=null)
{
$this->router = $router;
+
+ // Static classes are instantiated to make it easier to use custom versions
$this->request = new Request();
$this->flash = new Flash();
}
@@ -48,6 +50,51 @@ abstract class Controller
/**
+ * Get the request input data, decoded based on Content-Type header.
+ *
+ * @param string|array $supportedFormats Supported input formats (mime or short format)
+ * @return mixed
+ */
+ protected function getInput($supportedFormats = null)
+ {
+ if (isset($supportedFormats)) {
+ $failed = $this->router ? function($message) {
+ $this->router->badRequest($message, 415);
+ } : null;
+
+ $this->request->supportInputFormat($supportedFormats, $failed);
+ }
+
+ return $this->request->getInput();
+ }
+
+
+ /**
+ * Set the headers with HTTP status code and content type.
+ *
+ * @param int $httpCode HTTP status code (may be omitted)
+ * @param string $format Mime or simple format
+ * @return $this
+ */
+ protected function respondWith($httpCode, $format=null)
+ {
+ $this->request->respondWith($httpCode, $format);
+ return $this;
+ }
+
+ /**
+ * Output data
+ *
+ * @param array $data
+ * @param string $format Mime or content format
+ * @return $this
+ */
+ protected function output($data, $format = null)
+ {
+ $this->request->output($data, $format);
+ }
+
+ /**
* Show a view.
*
* @param string $name Filename of Twig template
@@ -65,42 +112,44 @@ abstract class Controller
/**
- * Get the request input data, decoded based on Content-Type header.
+ * Respond with 200 Ok.
+ * This is the default state, so you usually don't have to set it explicitly.
*
- * @return mixed
+ * @return $this;
*/
- protected function getInput()
+ protected function ok()
{
- return $this->request->getInput();
+ $this->request->respondWith(200);
}
/**
- * Set the headers with HTTP status code and content type.
+ * Respond with 201 Created
*
- * @param int $httpCode HTTP status code (may be omitted)
- * @param string $format Mime or simple format
- * @return Controller $this
+ * @param string $location Location of the created resource
+ * @return $this;
*/
- protected function respondWith($httpCode, $format=null)
+ protected function created($location = null)
{
- $this->request->respondWith($httpCode, $format);
- return $this;
+ $this->request->respondWith(201);
+ if (isset($location)) header("Location: $location");
}
/**
- * Output data
+ * Respond with 204 No Content
*
- * @param array $data
+ * @return $this;
*/
- protected function output($data)
+ protected function noContent()
{
- $this->request->output($data);
+ $this->request->respondWith(204);
}
/**
* Redirect to previous page.
* Must be on this website, otherwise redirect to home.
+ *
+ * @return $this;
*/
protected function back()
{
@@ -122,8 +171,6 @@ abstract class Controller
header("Location: $url");
echo 'You are being redirected to <a href="' . $url . '">' . $url . '</a>';
}
-
- exit();
}
@@ -140,8 +187,6 @@ abstract class Controller
} else {
$this->request->outputError($httpCode, $message);
}
-
- exit();
}
/**
@@ -169,8 +214,6 @@ abstract class Controller
if (!isset($message)) $message = "Sorry, you are not allowed to view this page";
$this->request->outputError($httpCode, $message);
}
-
- exit();
}
/**
@@ -187,8 +230,6 @@ abstract class Controller
if (!isset($message)) $message = "Sorry, this page does not exist";
$this->request->outputError($httpCode, $message);
}
-
- exit();
}
/**
@@ -205,7 +246,50 @@ abstract class Controller
} else {
$this->request->outputError($httpCode, $message);
}
-
- exit();
+ }
+
+
+ /**
+ * Check if response is 2xx succesful
+ *
+ * @return boolean
+ */
+ protected function isSuccessful()
+ {
+ $code = http_response_code();
+ return $code >= 200 && $code < 300;
+ }
+
+ /**
+ * Check if response is a 3xx redirect
+ *
+ * @return boolean
+ */
+ protected function isRedirection()
+ {
+ $code = http_response_code();
+ return $code >= 300 && $code < 400;
+ }
+
+ /**
+ * Check if response is a 4xx client error
+ *
+ * @return boolean
+ */
+ protected function isClientError()
+ {
+ $code = http_response_code();
+ return $code >= 400 && $code < 500;
+ }
+
+ /**
+ * Check if response is a 5xx redirect
+ *
+ * @return boolean
+ */
+ protected function isServerError()
+ {
+ $code = http_response_code();
+ return $code >= 500;
}
}
diff --git a/src/Request.php b/src/Request.php
index 2fa0b12..2c759a0 100644
--- a/src/Request.php
+++ b/src/Request.php
@@ -12,39 +12,39 @@ class Request
* @var array
*/
static public $httpStatusCodes = [
- 200 => '200 OK',
- 201 => '201 Created',
- 202 => '202 Accepted',
- 204 => '204 No Content',
- 205 => '205 Reset Content',
- 206 => '206 Partial Content',
- 301 => '301 Moved Permanently',
- 302 => '302 Found',
- 303 => '303 See Other',
- 304 => '304 Not Modified',
- 307 => '307 Temporary Redirect',
- 308 => '308 Permanent Redirect',
- 400 => "400 Bad Request",
- 401 => "401 Unauthorized",
- 402 => "402 Payment Required",
- 403 => "403 Forbidden",
- 404 => "404 Not Found",
- 405 => "405 Method Not Allowed",
- 406 => "406 Not Acceptable",
- 409 => "409 Conflict",
- 410 => "410 Gone",
- 415 => "415 Unsupported Media Type",
- 429 => "429 Too Many Requests",
- 500 => "500 Internal server error",
- 501 => "501 Not Implemented",
- 503 => "503 Service unavailable"
+ 200 => 'OK',
+ 201 => 'Created',
+ 202 => 'Accepted',
+ 204 => 'No Content',
+ 205 => 'Reset Content',
+ 206 => 'Partial Content',
+ 301 => 'Moved Permanently',
+ 302 => 'Found',
+ 303 => 'See Other',
+ 304 => 'Not Modified',
+ 307 => 'Temporary Redirect',
+ 308 => 'Permanent Redirect',
+ 400 => 'Bad Request',
+ 401 => 'Unauthorized',
+ 402 => 'Payment Required',
+ 403 => 'Forbidden',
+ 404 => 'Not Found',
+ 405 => 'Method Not Allowed',
+ 406 => 'Not Acceptable',
+ 409 => 'Conflict',
+ 410 => 'Gone',
+ 415 => 'Unsupported Media Type',
+ 429 => 'Too Many Requests',
+ 500 => 'Internal server error',
+ 501 => 'Not Implemented',
+ 503 => 'Service unavailable'
];
/**
* Common input and output formats with associated MIME
* @var array
*/
- static public $contentFormats = [
+ public static $contentFormats = [
'text/html' => 'html',
'application/json' => 'json',
'application/xml' => 'xml',
@@ -64,34 +64,49 @@ class Request
* File extensions to format mapping
* @var array
*/
- static public $fileExtension = [
+ public static $fileExtension = [
'jpg' => 'jpeg',
'txt' => 'text'
];
/**
+ * Allow the use $_POST['_method'] as request method.
+ * @var boolean
+ */
+ public static $allowMethodOverride = false;
+
+ /**
+ * Always set 'Content-Type' to 'text/plain' with a 4xx or 5xx response.
+ * This is useful when handing jQuery AJAX requests, since jQuery doesn't deserialize errors.
+ *
+ * @var boolean
+ */
+ public static $forceTextErrors = false;
+
+
+ /**
* Get the HTTP protocol
*
* @return string;
*/
protected static function getProtocol()
{
- return isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1';
+ return isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0';
}
/**
* Return the request method.
*
- * Usually REQUEST_METHOD, but this can be overwritten by $_POST['_method'].
- * Method is alway uppercase.
- *
* @return string
*/
public static function getMethod()
{
- return strtoupper(!empty($_POST['_method']) ? $_POST['_method'] : $_SERVER['REQUEST_METHOD']);
+ return static::$allowMethodOverride && !empty($_POST['_method']) ?
+ strtoupper(!empty($_POST['_method'])) :
+ strtoupper($_SERVER['REQUEST_METHOD']);
}
+
/**
* Get the input format.
* Uses the 'Content-Type' request header.
@@ -99,17 +114,62 @@ class Request
* @param string $as 'short' or 'mime'
* @return string
*/
- public static function getInputFormat($as='short')
+ public static function getInputFormat($as)
{
- if (empty($_SERVER['CONTENT_TYPE'])) {
- $mime = trim(explode(';', $_SERVER['CONTENT_TYPE'])[0]);
- }
+ if (empty($_SERVER['CONTENT_TYPE'])) return null;
+
+ $mime = trim(explode(';', $_SERVER['CONTENT_TYPE'])[0]);
return $as !== 'mime' && isset(static::$contentFormats[$mime]) ?
static::$contentFormats[$mime] :
$mime;
}
+ /**
+ * Check `Content-Type` request header to see if input format is supported.
+ * Respond with "415 Unsupported Media Type" if the format isn't supported.
+ *
+ * @param string|array $support Supported formats (short or mime)
+ * @param callback $failed Callback when format is not supported
+ */
+ public static function supportInputFormat($support, $failed = null)
+ {
+ $mime = static::getInputFormat('mime');
+
+ if (!isset($mime)) {
+ if (file_get_contents('php://input', false, null, -1, 1) === '') return;
+ } else {
+ if (static::matchMime($mime, $support)) return;
+ }
+
+ // Not supported
+ $message = isset($mime) ?
+ "The request body is in an unsupported format" :
+ "The 'Content-Type' request header isn't set";
+
+ if (isset($failed)) call_user_func($failed, $message, $support);
+
+ static::outputError("415 Unsupported Media Type", $message);
+ exit();
+ }
+
+ /**
+ * Check the Content-Type of the request.
+ *
+ * @param string $mime
+ * @param string|array $formats Short format or MIME, may contain wildcard
+ * @return mixed
+ */
+ protected static function matchMime($mime, $formats)
+ {
+ $fnWildcardMatch = function($ret, $pattern) use ($mime) {
+ return $ret || fnmatch($pattern, $mime);
+ };
+
+ return
+ isset(static::$contentFormats[$mime]) && in_array(static::$contentFormats[$mime], $formats) ||
+ array_reduce($formats, $fnWildcardMatch, false);
+ }
/**
* Get the request input data, decoded based on Content-Type header.
@@ -118,22 +178,81 @@ class Request
*/
public static function getInput()
{
- switch (static::getInputFormat()) {
- case 'post': return $_FILES + $_POST;
- case 'json': return json_decode(file_get_contents('php://input'));
+ switch (static::getInputFormat('short')) {
+ case 'post': $_POST + static::getPostedFiles();
+ case 'json': return json_decode(file_get_contents('php://input'), true);
case 'xml': return simplexml_load_string(file_get_contents('php://input'));
default: return file_get_contents('php://input');
}
}
/**
+ * Get $_FILES properly grouped.
+ *
+ * @return array
+ */
+ protected static function getPostedFiles()
+ {
+ $files = $_FILES;
+
+ foreach ($files as &$file) {
+ if (!is_array($file['error'])) continue;
+
+ $group = [];
+ foreach (array_keys($file['error']) as $key) {
+ foreach (array_keys($file) as $elem) {
+ $group[$key][$elem] = $file[$elem][$key];
+ }
+ }
+ $file = $group;
+ }
+
+ return $files;
+ }
+
+
+ /**
+ * Check if request is an AJAX request.
+ *
+ * @return boolean
+ */
+ public static function isAjax()
+ {
+ return !empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
+ strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
+ }
+
+ /**
+ * Check if requested to wrap JSON as JSONP response.
+ * Assumes that the output format is json.
+ *
+ * @return boolean
+ */
+ public static function isJsonp()
+ {
+ return !empty($_GET['callback']);
+ }
+
+ /**
+ * Returns the HTTP referer if it is on the current host.
+ *
+ * @return string
+ */
+ public static function getLocalReferer()
+ {
+ return !empty($_SERVER['HTTP_REFERER']) && parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST) ==
+ $_SERVER['HTTP_HOST'] ? $_SERVER['HTTP_REFERER'] : null;
+ }
+
+
+ /**
* Get the output format.
* Tries 'Content-Type' response header, otherwise uses 'Accept' request header.
*
* @param string $as 'short' or 'mime'
* @return string
*/
- public static function getOutputFormat($as='short')
+ public static function getOutputFormat($as)
{
// Explicitly set as Content-Type response header
foreach (headers_list() as $header) {
@@ -160,47 +279,120 @@ class Request
$ext = pathinfo(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), PATHINFO_EXTENSION);
if ($ext) {
if (isset(static::$fileExtension[$ext])) $ext = static::$fileExtension[$ext];
- if ($as === 'mime') return $ext;
- return array_search($ext, static::$contentFormats) ?: $ext;
+ return ($as === 'mime') ? (array_search($ext, static::$contentFormats) ?: $ext) : $ext;
}
// Don't know (default to HTML)
return $as === 'mime' ? '*/*' : 'html';
}
-
+
/**
- * Check if request is an AJAX request.
+ * Accept requests from a specific origin.
*
- * @return boolean
+ * Sets HTTP Access-Control headers (CORS).
+ * @link http://www.w3.org/TR/cors/
+ *
+ * The following settings are available:
+ * - expose-headers
+ * - max-age
+ * - allow-credentials
+ * - allow-methods (default '*')
+ * - allow-headers (default '*')
+ *
+ * <code>
+ * Request::allowOrigin('same');
+ * Request::allowOrigin('www.example.com');
+ * Request::allowOrigin('*.example.com');
+ * Request::allowOrigin(['*.example.com', 'www.example.net']);
+ * Request::allowOrigin('*');
+ *
+ * Request::allowOrigin('same', [], function() {
+ * static::respondWith("403 forbidden", 'html');
+ * echo "<h1>Forbidden</h1><p>Sorry, we have a strict same-origin policy.</p>";
+ * exit();
+ * });
+ * </code>
+ *
+ * @param string|array $urls Allowed URL/URLs, may use wildcards or "same"
+ * @param array $settings
+ * @param callback $failed Called when origin is not allowed
*/
- public static function isAjax()
+ public static function allowOrigin($urls, array $settings = [], $failed = null)
{
- return !empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
- strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
+ $origin = static::matchOrigin($urls);
+
+ static::setAllowOriginHeaders($origin ?: $urls, $settings);
+
+ if (!isset($origin)) {
+ $message = "Origin not allowed";
+ if (isset($failed)) call_user_func($failed, $message, $urls);
+ static::outputError("403 forbidden", $message);
+ exit();
+ }
}
/**
- * Check if requested to wrap JSON as JSONP response.
- * Assumes that the output format is json.
+ * Match `Origin` header to supplied urls.
*
- * @return boolean
+ * @param string|array $urls
+ * @return string
*/
- public static function isJsonp()
+ protected function matchOrigin($urls)
{
- return !empty($_GET['callback']);
+ if ($urls === '*') return '*';
+
+ if (!is_array($urls)) $urls = (array)$urls;
+
+ $origin = parse_url($_SERVER['origin']) + ['port' => 80];
+
+ foreach ($urls as &$url) {
+ if ($url === 'same') $url = '//' . $_SERVER['HTTP_HOST'];
+ if (strpos($url, ':') && substr($url, 0, 2) !== '//') $url = '//' . $url;
+
+ $match = parse_url($url);
+ $found =
+ (!isset($match['scheme']) || $match['scheme'] === $origin['schema']) &&
+ (!isset($match['port']) || $match['port'] === $origin['port']) &&
+ fnmatch($match['domain'], $origin['port']);
+
+ if ($found) return $_SERVER['origin'];
+ }
+
+ return null;
}
/**
- * Returns the HTTP referer if it is on the current host.
+ * Sets HTTP Access-Control headers (CORS).
*
- * @return string
+ * @param string|array $origin
+ * @param array $settings
*/
- public static function getLocalReferer()
+ protected function setAllowOriginHeaders($origin, array $settings = [])
{
- return !empty($_SERVER['HTTP_REFERER']) && parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST) ==
- $_SERVER['HTTP_HOST'] ? $_SERVER['HTTP_REFERER'] : null;
+ foreach ((array)$origin as $url) {
+ header("Access-Control-Allow-Origin: $url");
+ }
+
+ if (isset($settings['expose-headers'])) {
+ header("Access-Control-Allow-Credentials: " . join(', ', (array)$settings['expose-headers']));
+ }
+
+ if (isset($settings['max-age'])) {
+ header("Access-Control-Max-Age: " . join(', ', (array)$settings['max-age']));
+ }
+
+ if (isset($settings['allow-credentials'])) {
+ $set = $settings['allow-credentials'];
+ header("Access-Control-Allow-Credentials: " . (is_string($set) ? $set : ($set ? 'true' : 'false')));
+ }
+
+ $methods = isset($settings['allow-methods']) ? $settings['allow-methods'] : '*';
+ header("Access-Control-Allow-Methods: " . join(', ', (array)$methods));
+
+ $headers = isset($settings['allow-headers']) ? $settings['allow-headers'] : '*';
+ header("Access-Control-Allow-Headers: " . join(', ', (array)$headers));
}
-
+
/**
* Set the headers with HTTP status code and content type.
@@ -208,25 +400,26 @@ class Request
*
* Examples:
* <code>
- * static::respondWith(200, 'json');
- * static::respondWith(200, 'application/json');
- * static::respondWith(204);
- * static::respondWith('json');
+ * Request::respondWith(200, 'json');
+ * Request::respondWith(200, 'application/json');
+ * Request::respondWith(204);
+ * Request::respondWith("204 Created");
+ * Request::respondWith('json');
+ * Request::respondWith(['json', 'xml']);
* </code>
*
- * @param int $httpCode HTTP status code (may be omitted)
- * @param string $format Mime or content format
+ * @param int $httpCode HTTP status code (may be omitted)
+ * @param string|array $format Mime or content format
+ * @param callback $failed Called if format isn't accepted
*/
- public static function respondWith($httpCode, $format=null)
+ public static function respondWith($httpCode, $format = null)
{
- if (!isset($format) && !is_int($httpCode) && !ctype_digit($httpCode)) {
- $format = $httpCode;
- $httpCode = null;
+ // Shift arguments if $httpCode is omitted
+ if (!is_int($httpCode) && !preg_match('/^\d{3}\b/', $httpCode)) {
+ list($httpCode, $format) = array_merge([null], func_get_args());
}
- if (isset($httpCode)) {
- header(static::getProtocol() . ' ' . static::$httpStatusCodes[$httpCode]);
- }
+ if (isset($httpCode)) http_response_code((int)$httpCode);
if (isset($format)) {
$contentType = array_search($format, static::$contentFormats) ?: $format;
@@ -237,11 +430,12 @@ class Request
/**
* Output result as json, xml or image.
*
- * @param mixed $data
+ * @param mixed $data
+ * @param string $format Mime or content format
*/
- public function output($data)
+ public function output($data, $format = null)
{
- $format = static::getOutputFormat();
+ if (!isset($format)) $format = static::getOutputFormat();
switch ($format) {
case 'json': static::outputJSON($data); break;
@@ -256,8 +450,7 @@ class Request
default:
$type = (is_object($data) ? get_class($data) . ' ' : '') . gettype($data);
- $a = in_array($type[0], explode('', 'aeiouAEIOU')) ? 'an' : 'a';
- trigger_error("Don't know how to convert $a $type to $format", E_USER_ERROR);
+ trigger_error("Don't know how to convert a $type to $format", E_USER_ERROR);
}
}
@@ -270,8 +463,7 @@ class Request
{
if (!$result instanceof \DOMNode && !$result instanceof \SimpleXMLElement) {
$type = (is_object($result) ? get_class($result) . ' ' : '') . gettype($result);
- $a = in_array($type[0], explode('', 'aeiouAEIOU')) ? 'an' : 'a';
- throw new \Exception("Was expecting a DOMNode or SimpleXMLElement object, got $a $type");
+ throw new \Exception("Was expecting a DOMNode or SimpleXMLElement object, got a $type");
}
static::respondWith('xml');
@@ -287,7 +479,7 @@ class Request
*/
protected function outputJSON($result)
{
- if ($this->request->isJsonp()) {
+ if (static::isJsonp()) {
static::respondWith(200, 'js');
echo $_GET['callback'] . '(' . json_encode($result) . ')';
return;
@@ -307,8 +499,7 @@ class Request
{
if (!is_resource($image)) {
$type = (is_object($image) ? get_class($image) . ' ' : '') . gettype($image);
- $a = in_array($type[0], explode('', 'aeiouAEIOU')) ? 'an' : 'a';
- throw new \Exception("Was expecting a GD resource, got $a $type");
+ throw new \Exception("Was expecting a GD resource, got a $type");
}
static::respondWith("image/$format");
@@ -316,7 +507,7 @@ class Request
$out = 'image' . $format;
$out($image);
}
-
+
/**
* Output an HTTP error
@@ -358,7 +549,7 @@ class Request
*/
protected static function outputErrorJson($httpCode, $error)
{
- $result = ['_error' => $error, '_httpCode' => $httpCode];
+ $result = ['error' => $error, 'httpCode' => $httpCode];
static::respondWith($httpCode, 'json');
static::output($result);
diff --git a/src/Router.php b/src/Router.php
index 38d9f45..4ed5964 100644
--- a/src/Router.php
+++ b/src/Router.php
@@ -88,6 +88,10 @@ class Router
{
if (is_object($routes)) $routes = get_object_vars($routes);
+ foreach ($routes as &$route) {
+ if ($route instanceof \Closure) $route = (object)['fn' => $route];
+ }
+
$this->routes = $routes;
$this->route = null;
@@ -113,6 +117,8 @@ class Router
continue;
}
+ if ($route instanceof \Closure) $route = (object)['fn' => $route];
+
$this->routes[$path] = $route;
}
@@ -341,28 +347,25 @@ class Router
$route->$key = $value;
}
- // Route to file
- if (isset($route->file)) {
- $file = rtrim($_SERVER['DOCUMENT_ROOT'], '/') . '/' . ltrim($this->rebase($route->file), '/');
-
- if (!file_exists($file)) {
- trigger_error("Failed to route using '{$route->route}': File '$file' doesn't exist."
- , E_USER_WARNING);
- return false;
- }
-
- return include $file;
- }
+ if (isset($route->controller)) return $this->routeToController($route);
+ if (isset($route->fn)) return $this->routeToCallback($route);
+ if (isset($route->file)) return $this->routeToFile($route);
- // Route to controller
- if (empty($route->controller) || empty($route->action)) {
- trigger_error("Failed to route using '{$route->route}': "
- . (empty($route->controller) ? 'Controller' : 'Action') . " is not set", E_USER_WARNING);
- return false;
- }
+ trigger_error("Failed to route using '{$route->route}': Neither 'controller', 'fn' or 'file' is set",
+ E_USER_WARNING);
+ return false;
+ }
+ /**
+ * Route to controller action
+ *
+ * @param object $route
+ * @return mixed|boolean
+ */
+ protected function routeToController($route)
+ {
$class = $this->getControllerClass($route->controller);
- $method = $this->getActionMethod($route->action);
+ $method = $this->getActionMethod(isset($route->action) ? $route->action : null);
$args = isset($route->args) ? $route->args : [];
if (!class_exists($class)) return false;
@@ -373,6 +376,50 @@ class Router
$ret = call_user_func_array([$controller, $method], $args);
return isset($ret) ? $ret : true;
}
+
+ /**
+ * Route to a callback function
+ *
+ * @param object $route
+ * @return mixed|boolean
+ */
+ protected function routeToCallback($route)
+ {
+ if (!is_callable($route->fn)) {
+ trigger_error("Failed to route using '{$route->route}': Invalid callback."
+ , E_USER_WARNING);
+ return false;
+ }
+
+ $args = isset($route->args) ? $route->args : [];
+
+ return call_user_func_array($route->fn, $args);
+ }
+
+ /**
+ * Route to a file
+ *
+ * @param object $route
+ * @return mixed|boolean
+ */
+ protected function routeToFile($route)
+ {
+ $file = ltrim($route->file, '/');
+
+ if (!file_exists($file)) {
+ trigger_error("Failed to route using '{$route->route}': File '$file' doesn't exist.", E_USER_WARNING);
+ return false;
+ }
+
+ if ($route->file[0] === '~' || strpos($route->file, '..') !== false || strpos($route->file, ':') !== false) {
+ trigger_error("Won't route using '{$route->route}': '~', '..' and ':' not allowed in filename.",
+ E_USER_WARNING);
+ return false;
+ }
+
+ return include $file;
+ }
+
/**
* Execute the action.
@@ -423,7 +470,7 @@ class Router
}
if ($this->prevErrorHandler) {
- //call_user_func($this->prevErrorHandler, $code, $message, $file, $line, $context);
+ call_user_func($this->prevErrorHandler, $code, $message, $file, $line, $context);
}
if ($code & (E_RECOVERABLE_ERROR | E_USER_ERROR)) {
@@ -439,14 +486,15 @@ class Router
*/
public function onException($exception)
{
+ $this->error(null, 500, $exception);
if ($this->prevExceptionHandler) call_user_func($this->prevExceptionHandler, $exception);
- $this->error(null, 500, $exception);
+ exit();
}
/**
- * Redirect to another page and exit
+ * Redirect to another page
*
* @param string $url
* @param int $httpCode 301 (Moved Permanently), 303 (See Other) or 307 (Temporary Redirect)
@@ -461,11 +509,10 @@ class Router
header("Location: $url");
echo 'You are being redirected to <a href="' . $url . '">' . $url . '</a>';
- exit();
}
/**
- * Give a 400 Bad Request response and exit
+ * Give a 400 Bad Request response
*
* @param string $message
* @param int $httpCode Alternative HTTP status code, eg. 406 (Not Acceptable)
@@ -474,7 +521,6 @@ class Router
public function badRequest($message, $httpCode=400)
{
$this->respond(400, $message, $httpCode, array_slice(func_get_args(), 2));
- exit();
}
/**
@@ -484,7 +530,6 @@ class Router
public function requireLogin()
{
$this->routeTo(401) || $this->forbidden();
- exit();
}
/**
@@ -502,12 +547,10 @@ class Router
if (!isset($message)) $message = "Sorry, you are not allowed to view this page";
self::outputError($httpCode, $message, $this->getOutputFormat());
}
-
- exit();
}
/**
- * Give a 404 Not Found response and exit
+ * Give a 404 Not Found response
*
* @param string $message
* @param int $httpCode Alternative HTTP status code, eg. 410 (Gone)
@@ -524,31 +567,16 @@ class Router
self::outputError($httpCode, $message, $this->getOutputFormat());
}
-
- exit();
}
/**
- * Give a 500 Internal Server Error response and exit
+ * Give a 5xx Server Error response
*
- * @param string $message
- * @param int $httpCode Alternative HTTP status code, eg. 503 (Service unavailable)
- * @param mixed $.. Additional arguments are passed to action
- */
- protected function error($message=null, $httpCode=500)
- {
- call_user_func_array([$this, 'displayError'], func_get_args());
- exit();
- }
-
- /**
- * Give a 500 Internal Server Error response (but don't exit)
- *
- * @param string $message
- * @param int $httpCode Alternative HTTP status code, eg. 503 (Service unavailable)
- * @param mixed $.. Additional arguments are passed to action
+ * @param string $message
+ * @param int|string $httpCode HTTP status code, eg. "500 Internal Server Error" or 503
+ * @param mixed $.. Additional arguments are passed to action
*/
- protected function displayError($message=null, $httpCode=500)
+ public function error($message=null, $httpCode=500)
{
if (ob_get_level() > 1) ob_end_clean();