*/ 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; /// 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 (); } /** Return the baseURL of the site * Always finish with a slash * @param string|null $module The module name (if thereis one) * @param bool|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 !== "") 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"])) $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 (); $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; $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 bool|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"])) $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"]) !== "/") $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 * 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 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 (); // FIXME : Why do I add this slash ? I can not redirect the word to word/ // if (substr ($requestURL, -1) !== "/") // $requestURL .= "/"; if ($destURL[0] === "/") { // 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 === "") throw 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") throw new Exception ("Redirect to myself", 400); if (substr_count ($baseURL, "../") > 1+ substr_count ($destURL,"/")) throw 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"))
{
$this->method = $_POST["_METHOD"];
return $_POST["_METHOD"];
}
if (!isset ($_SERVER["REQUEST_METHOD"]))
throw new Exception ("No REQUEST_METHOD", 415);
if ($_SERVER["REQUEST_METHOD"] === "GET" ||
$_SERVER["REQUEST_METHOD"] === "POST" ||
$_SERVER["REQUEST_METHOD"] === "PUT" ||
$_SERVER["REQUEST_METHOD"] === "DELETE")
{
$this->method = $_SERVER["REQUEST_METHOD"];
return $_SERVER["REQUEST_METHOD"];
}
throw new Exception ("Invalid REQUEST_METHOD", 406);
}
/** If the URL is corresponding with $url, and the method is GET, then the
* function is called.
* Ex. : $app->get('/hello/{name}', function ($name) {
* echo "Hello, $name";
* });
* @param string $route Route to check with the URL
* @param function $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. : $app->get('/hello/{name}', function ($name) {
* echo "Hello, $name";
* });
* @param string $route Route to check with the URL
* @param function $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. : $app->get('/hello/{name}', function ($name) {
* echo "Hello, $name";
* });
* @param string $route Route to check with the URL
* @param function $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. : $app->get('/hello/{name}', function ($name) {
* echo "Hello, $name";
* });
* @param string $route Route to check with the URL
* @param function $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);
}
/** Allow multiple methods to execute the function if the route is
* corresponding to the URL.
* Ex. : $app->multi("get,post,put,delete", '/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 function $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 function $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";
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 && $rcRegex !== 0)
{
if ($this->debug)
echo "==> FOUND REGEX !\n";
$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 $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);
}
}