*/ require_once ("domframework/http.php"); require_once ("domframework/ratelimitfile.php"); /** The routing module, base of the DomFramework */ class route { /** The baseURL of the site */ private $baseURL = ""; /** The baseURL of the module in the site */ private $baseURLmodule = ""; /** The method used to ask the page */ public $method = ""; /** The module name */ public $module = NULL; /** The debug mode : * 0:NoDebug, 1:routing, 2:more debug (developpement)*/ public $debug=0; //public $defaultOutput = "html"; // Default renderer : html /** Allow slashes in the url when matching the regex */ public $allowSlashes = true; /** Provide the the class catch errors in routing. * Must be provided in an array(class, method); */ public $errors = null; /** Preroute used in modules */ public $preroute = ""; /** Authentication URL used if a 401 error is raised. If none is defined, * just display a "Unauthorized" message */ public $authenticationURL = null; /** Ratelimit the errors in route.php to not allow the hackers to brute force * the backend. The objct can be put to null to disable the feature */ public $ratelimiter = null; /// MAPPING /// /** The matching route comparison */ public $mapRoute = null; /// RENDERER PART /// /** Output type to no previous catched renderer (allow : json, xml, txt html) */ public $output = "html"; /** Title by default : space to be compatible with HTML5 */ public $title = " "; /** Filename of class containing the presentation layer */ public $viewClass = FALSE; /** Method apply to class object to display the $result */ public $viewMethod = FALSE; /** Classname containing the error layer */ public $viewErrorClass = null; /** Method apply to class object to display the error */ public $viewErrorMethod = null; /** Filename in views containing the HTML layout. Without .html at the end */ public $layout = FALSE; /** Array to search/replace */ public $replacement = array(); /** Array to variable definition */ public $variable = array (); /** The route constructor : initialize the parameters */ function __construct () { $this->ratelimiter = new ratelimitfile (); } /** Get / Set the debug level * @param integer|null $val The value to set. If null, will return the actual * value */ public function debug ($val = null) { if ($val === null) return $this->debug; $this->debug = intval ($val); } /** Return the baseURL of the site * Always finish with a slash * @param string|null $module The module name (if thereis one) * @param boolean|null $absolute Return the baseURL in absolute * @return string The URL base */ function baseURL ($module = FALSE, $absolute=false) { if ($this->module === NULL) $this->module = $module; if ($this->baseURL !== "" && $absolute == false) return $this->baseURL; if (!isset ($_SERVER["SERVER_PORT"])) $_SERVER["SERVER_PORT"] = "80"; $port = ":".$_SERVER["SERVER_PORT"]; if (!isset ($_SERVER["HTTPS"]) && $_SERVER["SERVER_PORT"] === "80") $port = ""; if (isset ($_SERVER["HTTPS"]) && $_SERVER["SERVER_PORT"] === "443") $port = ""; if ($absolute === true) { $this->baseURL = ""; if (isset ($_SERVER["SCRIPT_NAME"])) $this->baseURL = dirname ($_SERVER["SCRIPT_NAME"]); if (isset ($_SERVER["SERVER_NAME"])) $this->baseURL = "//".$_SERVER["SERVER_NAME"].$port.$this->baseURL; if (isset ($_SERVER["HTTPS"]) || (isset ($_SERVER["HTTP_X_FORWARDED_PROTO"]) && $_SERVER["HTTP_X_FORWARDED_PROTO"] === "https") ) $this->baseURL = "https:".$this->baseURL; else $this->baseURL = "http:".$this->baseURL; if (substr ($this->baseURL, -1) !== "/") $this->baseURL .= "/"; } elseif (isset ($_SERVER["REQUEST_URI"]) && strpos ($_SERVER["REQUEST_URI"], "index.php?url=") !== false) { $this->baseURL = ""; } else { // Calculate the root in relative $request = $this->requestURL (); if (! isset ($_SERVER["REQUEST_URI"])) $this->baseURL = "/"; elseif (substr ($_SERVER["REQUEST_URI"], -1 * strlen ($request)) === $request) { $this->baseURL = substr ($_SERVER["REQUEST_URI"], 0, strlen ($_SERVER["REQUEST_URI"]) - strlen ($request)); } elseif ($_SERVER["REQUEST_URI"] !== "") { $this->baseURL = $_SERVER["REQUEST_URI"]; } else { $this->baseURL = "/"; } /* // This part was to get really relative to current directory (with ../) // But it is not really well supported by the bad coded crawlers. So, the // URL is now relative to base of URL $this->baseURL = str_repeat ("../", substr_count ($request, "/")). $this->baseURL; if ($this->baseURL === "") $this->baseURL = "./"; */ } $this->baseURLmodule = $this->baseURL; // Only != NOT !== (cause : $this->module can be converted in string "0") if ($this->module != FALSE) { $this->baseURLmodule = $this->baseURL; if (dirname ($this->baseURL) !== "/") $this->baseURL = dirname ($this->baseURL)."/"; } return $this->baseURL; } /** Return the baseURL for a resource (add a index.php?url= if there is no * mod_rewrite support. Used to link to modules in the HTML page. * The baseURL is the real base of the project, which is different when not * using the mod_rewrite. They are equal when using the mod_rewrite * @return string The baseURL for the resource */ function baseURLresource () { if ($this->baseURL === "") $this->baseURL (); if (isset ($_SERVER["REQUEST_URI"]) && strpos ($_SERVER["REQUEST_URI"], "index.php?url=") !== false) return "index.php?url=".$this->baseURL; return $this->baseURL; } /** Return the baseURL of the module * Always finish with a slash * @return string The baseURL for the module */ function baseURLmodule () { if ($this->baseURLmodule !== "") return $this->baseURLmodule; $this->baseURL ($this->module); return $this->baseURLmodule; } /** Define the base URL of the site * @param string $baseURL The base URL of the site */ function baseURLset ($baseURL) { $this->baseURL = $baseURL; } /** Return the complete URL used to see this page * @param boolean|null $absolute Return the absolute URL * @return string the complete URL used to see this page */ function requestURL ($absolute = false) { $url = ""; if ($absolute === true) { if (isset ($_SERVER["HTTPS"]) || isset ($_SERVER["HTTP_X_FORWARDED_PROTO"]) && $_SERVER["HTTP_X_FORWARDED_PROTO"] === "https") $url = "https:"; else $url = "http:"; if (!isset ($_SERVER["SERVER_PORT"])) $_SERVER["SERVER_PORT"] = "80"; $port = ":".$_SERVER["SERVER_PORT"]; if (!isset ($_SERVER["HTTPS"]) && $_SERVER["SERVER_PORT"] === "80") $port = ""; if (isset ($_SERVER["HTTPS"]) && $_SERVER["SERVER_PORT"] === "443") $port = ""; if (isset ($_SERVER["SERVER_NAME"])) $url .= "//".$_SERVER["SERVER_NAME"].$port."/"; } if (isset ($_SERVER["REQUEST_URI"])) { // If there is a directory before the index.php file, must remove the // directory structure if (dirname ($_SERVER["SCRIPT_NAME"]) !== "/" && dirname ($_SERVER["SCRIPT_NAME"]) !== ".") $url .= substr ($_SERVER["REQUEST_URI"], 1+strlen (dirname ($_SERVER["SCRIPT_NAME"]))); else // If there is no directory before the index.php (root of the Web server), // just remove the / $url = substr ($_SERVER["REQUEST_URI"], 1); } return $url; } /** Do all the routing with redirections * $destURL can be absolute or relative. * - absolute : "/main" redirect to "baseURL/mail" * - relative : "main" redirect to "./main" * If module is set, the site is modular and a directory is named with module * name * @param string $destURL Do a redirection of the HTTP page * @param string|null $module The module name * @param boolean|null $permanent Permanent redirect (false by default) * @return Exit of the PHP after doing the redirection */ function redirect ($destURL, $module="", $permanent = false) { if (php_sapi_name () === "cli") exit; $destURL = trim ($destURL); $baseURL = $this->baseURL (); if ($module !== "") $baseURL .= $module."/"; $requestURL = $this->requestURL (); if (substr ($destURL, 0, 1) === "/") { // Absolute : return to project base $destURL = $baseURL.substr ($destURL, 1); } if (strpos ($requestURL, "index.php?url=") !== false) { // If not using the mod_rewrite, force the index.php?url= $destURL = substr ($destURL, strlen ($baseURL)); $destURL = "index.php?url=".$destURL; } // Else http : keep the complete URL if ($destURL === "") $this->error (new \Exception ("Destination URL is empty", 500)); // Allow to redirect from POST to GET, but not GET to GET (can loop) if ($destURL === $requestURL && $_SERVER["REQUEST_METHOD"] === "GET") $this->error (new \Exception ("Redirect to myself", 400)); if (substr_count ($baseURL, "../") > 1+ substr_count ($destURL, "/")) $this->error (new \Exception ( "Can't redirect outside this site (Base $baseURL)", 405)); if ($this->debug) { echo "
\n";
      echo "==== DEBUG : REDIRECT START ====\n";
      echo "BASEURL=$baseURL\n";
      echo "REQUURL=$requestURL\n";
      echo "destURL=$destURL\n";
      echo " --->  Redirect to $destURL\n";
      echo "==== DEBUG : REDIRECT END ====\n";
      echo "
\n"; exit; } if (! headers_sent ()) { header ("Cache-Control: no-store, no-cache, must-revalidate"); header ("Pragma: no-cache"); if ($permanent) header ("HTTP/1.1 301 Moved Permanently"); header ("Location: $destURL"); } else { echo ""; echo "Redirect to $destURL"; } exit; } /** Return the HTTP method used to connect to the page * Can be override by a _METHOD parameter provided in POST * @return string the method used to connect * @throws an exception in case of error */ public function method () { if (isset ($_POST["_METHOD"]) && ($_POST["_METHOD"] === "GET" || $_POST["_METHOD"] === "POST" || $_POST["_METHOD"] === "PUT" || $_POST["_METHOD"] === "DELETE" || $_POST["_METHOD"] === "OPTIONS" )) { $this->method = $_POST["_METHOD"]; return $_POST["_METHOD"]; } if (!isset ($_SERVER["REQUEST_METHOD"])) $this->error (new \Exception ("No REQUEST_METHOD", 415)); if ($_SERVER["REQUEST_METHOD"] === "GET" || $_SERVER["REQUEST_METHOD"] === "POST" || $_SERVER["REQUEST_METHOD"] === "PUT" || $_SERVER["REQUEST_METHOD"] === "DELETE" || $_SERVER["REQUEST_METHOD"] === "OPTIONS") { $this->method = $_SERVER["REQUEST_METHOD"]; return $_SERVER["REQUEST_METHOD"]; } $this->error (new \Exception ("Invalid REQUEST_METHOD", 406)); } /** Return the mached route search */ public function mapRoute () { return $this->mapRoute; } /** If the URL is corresponding with $url, and the method is GET, then the * function is called. * Ex. : $route->get ('/hello/{name}', function ($name) { * echo "Hello, $name"; * }); * @param string $route Route to check with the URL * @param callable $function Function to be executed if the route match * @return Exit of the PHP after displaying the page if match, or return the * route object to chain it whith the next test */ public function get ($route, $function) { $route = $this->preroute.$route; if ($this->debug) echo "==> GET $route ?? "; if ($this->method () !== "GET") { if ($this->debug) echo "==> Not a GET Method
\n"; return $this; } return $this->map ($route, $function); } /** If the URL is corresponding with $url, and the method is POST, then the * function is called. * Ex. : $route->post ('/hello/{name}', function ($name) { * echo "Hello, $name"; * }); * @param string $route Route to check with the URL * @param callable $function Function to be executed if the route match * @return Exit of the PHP after displaying the page if match, or return the * route object to chain it whith the next test */ public function post ($route, $function) { $route = $this->preroute.$route; if ($this->debug) echo "==> POST $route ?? "; if ($this->method () !== "POST") { if ($this->debug) echo "==> Not a POST Method
\n"; return $this; } return $this->map ($route, $function); } /** If the URL is corresponding with $url, and the method is PUT, then the * function is called. * Ex. : $route->put ('/hello/{name}', function ($name) { * echo "Hello, $name"; * }); * @param string $route Route to check with the URL * @param callable $function Function to be executed if the route match * @return Exit of the PHP after displaying the page if match, or return the * route object to chain it whith the next test */ public function put ($route, $function) { $route = $this->preroute.$route; if ($this->debug) echo "==> PUT $route ?? "; if ($this->method () !== "PUT") { if ($this->debug) echo "==> Not a PUT Method
\n"; return $this; } return $this->map ($route, $function); } /** If the URL is corresponding with $url, and the method is DELETE, then the * function is called. * Ex. : $route->delete ('/hello/{name}', function ($name) { * echo "Hello, $name"; * }); * @param string $route Route to check with the URL * @param callable $function Function to be executed if the route match * @return Exit of the PHP after displaying the page if match, or return the * route object to chain it whith the next test */ public function delete ($route, $function) { $route = $this->preroute.$route; if ($this->debug) echo "==> DELETE $route ?? "; if ($this->method () !== "DELETE") { if ($this->debug) echo "==> Not a DELETE Method
\n"; return $this; } return $this->map ($route, $function); } /** If the URL is corresponding with $url, and the method is OPTIONS, then the * function is called. * Ex. : $route->options ('/hello/{name}', function ($name) { * echo "Hello, $name"; * }); * @param string $route Route to check with the URL * @param callable $function Function to be executed if the route match * @return Exit of the PHP after displaying the page if match, or return the * route object to chain it whith the next test */ public function options ($route, $function) { $route = $this->preroute.$route; if ($this->debug) echo "==> OPTIONS $route ?? "; if ($this->method () !== "OPTIONS") { if ($this->debug) echo "==> Not a OPTIONS Method
\n"; return $this; } return $this->map ($route, $function); } /** Allow multiple methods to execute the function if the route is * corresponding to the URL. * Ex. : $route->multi("get,post,put,delete,options", '/hello/{name}', * function ($name) { * echo "Hello, $name"; * }); * @param string $methods The allowed methods, separated by commas (,) * @param string $route Route to check with the URL * @param callable $function Function to be executed if the route match * @return Exit of the PHP after displaying the page if match, or return the * route object to chain it whith the next test */ public function multi ($methods, $route, $function) { foreach (explode (",", $methods) as $method) { $this->$method ($route, $function); } return $this; } /** Do the mapping between the url and the route : call the function if * thereis a match * @param string $route Route to check with the URL * @param callable $function Function to be executed if the route match * @return Exit of the PHP after displaying the page if match, or return the * route object to chain it whith the next test */ public function map ($route, $function) { $url = str_replace ("index.php?url=", "", $this->requestURL ()); if ($this->debug) echo "$url "; if ($url === $route) { if ($this->debug) echo "==> FOUND EQUAL !
\n"; $this->mapRoute = $route; try { $data = $function (); if ($this->method () === "GET" && isset ($_SESSION)) $_SESSION["domframework"]["route"]["lastGet"] = "/".$this->requestURL (); } catch (\Exception $e) { $this->error ($e); } require_once ("domframework/renderer.php"); $renderer = new renderer (); $renderer->result = $data; $renderer->output = $this->output; $renderer->title = $this->title; $renderer->viewClass = $this->viewClass; $renderer->viewMethod = $this->viewMethod; $renderer->layout = $this->layout; $renderer->replacement = $this->replacement; $renderer->variable = $this->variable; $renderer->run (); exit; } // URL === REGEXP ROUTE // Variables are exposed in url/{var1}/{var2} $regex = "#^$route$#U"; $regex = str_replace ("{", "(?P<", $regex); if ($this->allowSlashes) $regex = str_replace ("}", ">.+)", $regex); else $regex = str_replace ("}", ">[^/]+)", $regex); unset ($matches); $rcRegex = @preg_match ($regex, $url, $matches); if ($rcRegex === false) { if (count (\error_get_last ())) $this->error (new \Exception ("Invalid regex provided: $regex : ". error_get_last()["message"], 500)); } if ($rcRegex !== FALSE && $rcRegex !== 0) { if ($this->debug) echo "==> FOUND REGEX !
\n"; $this->mapRoute = $route; $params = array (); $reflect = new \ReflectionFunction ($function); foreach ($reflect->getParameters() as $key=>$val) { if (isset ($matches[$val->name])) $params[] = urldecode ($matches[$val->name]); else $params[] = null; } try { $data = \call_user_func_array ($function, $params); if ($this->method () === "GET" && isset ($_SESSION)) $_SESSION["domframework"]["route"]["lastGet"] = "/".$this->requestURL (); } catch (\Exception $e) { $this->error ($e); } require_once ("domframework/renderer.php"); $renderer = new renderer (); $renderer->result = $data; $renderer->output = $this->output; $renderer->title = $this->title; $renderer->viewClass = $this->viewClass; $renderer->viewMethod = $this->viewMethod; $renderer->layout = $this->layout; $renderer->replacement = $this->replacement; $renderer->variable = $this->variable; $renderer->run (); exit; } if ($this->debug) echo "==> NO MATCH
\n"; return $this; } /** Print an error page. If a custom page exists, use it * @param object $e Exception to print * @return bool true after the error is displayed */ public function error ($e) { $ipClient = null; if (isset ($_SERVER["HTTP_X_FORWARDED_FOR"])) $ipClient = $_SERVER["HTTP_X_FORWARDED_FOR"]; elseif (isset ($_SERVER["REMOTE_ADDR"])) $ipClient = $_SERVER["REMOTE_ADDR"]; elseif (defined ("PHPUNIT")) $ipClient = "CLI"; try { if ($this->ratelimiter !== null && $ipClient !== null && $this->ratelimiter->set ("error-$ipClient") === false) { $getCode = 406; $message = dgettext ("domframework", "Too much error requests"); } else { if ($e->getCode () === "" || $e->getCode () === 0) $getCode = 500; else $getCode = $e->getCode (); $message = $e->getMessage (); if ($e->getCode () === 0) $message .= "

The Exception code was 0 and converted to 500

\n"; } } catch (\Exception $e) { // If there is an error with the ratelimiter (file not writeable...), // overload the Exception $message = $e->getMessage (); $getCode = $e->getCode (); } // If an error class/method is defined, use it in place of the default // one. // The errors must be defined in an array("class", "method"); if ($this->errors !== null) { if (! is_array ($this->errors)) die ("The route::\$errors is not an array\n"); if (! isset ($this->errors[0]) || ! isset ($this->errors[1])) die ("The route::\$errors is not correct : must be the class and ". "the method in an array\n"); $a = new $this->errors[0]; $method = $this->errors[1]; $a->$method ($getCode, $message); return true; } if ($this->authenticationURL !== null && $getCode === 401) { // Non autorized users should be authenticated $this->redirect ($this->authenticationURL.$this->requestURL(), ""); } $http = new http (); @header ($_SERVER["SERVER_PROTOCOL"]." $getCode ". $http->codetext ($getCode)); if ($getCode === 401) { // When using the 401 "Authentication required", it must be an // information provided to the caller. Just add it. @header("WWW-Authenticate: Basic realm=\"Authentication needed\""); } // TODO : If the output is HTML, add the header line : // echo " \n"; require_once ("domframework/renderer.php"); $renderer = new renderer (); $renderer->result = $message; $renderer->output = $this->output; $renderer->title = $http->codetext ($getCode); if ($this->viewErrorClass !== null) $renderer->viewClass = $this->viewErrorClass; else $renderer->viewClass = $this->viewClass; if ($this->viewErrorMethod !== null) $renderer->viewMethod = $this->viewErrorMethod; else $renderer->viewMethod = $this->viewMethod; $renderer->layout = $this->layout; $renderer->replacement = array_merge ($this->replacement, array ("{exceptionCode}"=>$getCode, "{exceptionText}"=>$http->codetext($getCode), "{baseurl}"=>$this->baseURL(), "{baseurlmodule}"=>$this->baseURLmodule())); $renderer->variable = array_merge ($this->variable, array ("exceptionCode"=>$getCode, "exceptionText"=>$http->codetext($getCode), "baseurl"=>$this->baseURL(), "baseurlmodule"=>$this->baseURLmodule())); $renderer->run (); return true; } /** Return the last valid get page to return * @return The last valid page URL */ public function lastValidGetPage () { if (isset ($_SESSION["domframework"]["route"]["lastGet"])) return $_SESSION["domframework"]["route"]["lastGet"]; return ""; } /** Redirect to last valid get page if defined * @return Exit from PHP after redirect or null if the last valid page is not * defined */ public function lastValidGetPageRedirect () { $lastValidGetPage = $this->lastValidGetPage (); if ($lastValidGetPage !== "") $this->redirect ($lastValidGetPage); } }