Router Update + Bugfix
This commit is contained in:
parent
658157167e
commit
1fb875fb2c
@ -12,3 +12,8 @@ RewriteOptions AllowNoSlash
|
|||||||
RewriteRule ^((\.idea|\.git|src|test|core|docker|files)(/.*)?)$ /index.php?site=$1 [L,QSA]
|
RewriteRule ^((\.idea|\.git|src|test|core|docker|files)(/.*)?)$ /index.php?site=$1 [L,QSA]
|
||||||
|
|
||||||
FallbackResource /index.php
|
FallbackResource /index.php
|
||||||
|
|
||||||
|
ErrorDocument 400 /index.php?error=400
|
||||||
|
ErrorDocument 403 /index.php?error=403
|
||||||
|
ErrorDocument 404 /index.php?error=404
|
||||||
|
ErrorDocument 500 /index.php?error=500
|
@ -1,4 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="PhpProjectSharedConfiguration" php_language_level="7.4" />
|
<component name="PhpProjectSharedConfiguration" php_language_level="8.0">
|
||||||
|
<option name="suggestChangeDefaultLanguageLevel" value="false" />
|
||||||
|
</component>
|
||||||
</project>
|
</project>
|
@ -3,11 +3,6 @@ import {Link} from "react-router-dom";
|
|||||||
import Alert from "../elements/alert";
|
import Alert from "../elements/alert";
|
||||||
import {Collapse} from "react-collapse/lib/Collapse";
|
import {Collapse} from "react-collapse/lib/Collapse";
|
||||||
import Icon from "../elements/icon";
|
import Icon from "../elements/icon";
|
||||||
import { EditorState, ContentState, convertToRaw } from 'draft-js'
|
|
||||||
import { Editor } from 'react-draft-wysiwyg'
|
|
||||||
import draftToHtml from 'draftjs-to-html';
|
|
||||||
import htmlToDraft from 'html-to-draftjs';
|
|
||||||
import sanitizeHtml from 'sanitize-html'
|
|
||||||
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
|
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
|
||||||
import ReactTooltip from "react-tooltip";
|
import ReactTooltip from "react-tooltip";
|
||||||
|
|
||||||
@ -36,15 +31,6 @@ export default class Settings extends React.Component {
|
|||||||
unsavedMailSettings: false,
|
unsavedMailSettings: false,
|
||||||
keys: ["mail_enabled", "mail_host", "mail_port", "mail_username", "mail_password", "mail_from"]
|
keys: ["mail_enabled", "mail_host", "mail_port", "mail_username", "mail_password", "mail_from"]
|
||||||
},
|
},
|
||||||
messages: {
|
|
||||||
alerts: [],
|
|
||||||
isOpen: true,
|
|
||||||
isSaving: false,
|
|
||||||
isResetting: false,
|
|
||||||
editor: EditorState.createEmpty(),
|
|
||||||
isEditing: null,
|
|
||||||
keys: ["message_confirm_email", "message_accept_invite", "message_reset_password"]
|
|
||||||
},
|
|
||||||
recaptcha: {
|
recaptcha: {
|
||||||
alerts: [],
|
alerts: [],
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
@ -77,7 +63,6 @@ export default class Settings extends React.Component {
|
|||||||
key = key.trim();
|
key = key.trim();
|
||||||
return this.state.general.keys.includes(key)
|
return this.state.general.keys.includes(key)
|
||||||
|| this.state.mail.keys.includes(key)
|
|| this.state.mail.keys.includes(key)
|
||||||
|| this.state.messages.keys.includes(key)
|
|
||||||
|| this.state.recaptcha.keys.includes(key)
|
|| this.state.recaptcha.keys.includes(key)
|
||||||
|| this.hiddenKeys.includes(key);
|
|| this.hiddenKeys.includes(key);
|
||||||
}
|
}
|
||||||
@ -345,6 +330,7 @@ export default class Settings extends React.Component {
|
|||||||
{this.state.mail.isSending ?
|
{this.state.mail.isSending ?
|
||||||
<span>Sending <Icon icon={"circle-notch"}/></span> : "Send Mail"}
|
<span>Sending <Icon icon={"circle-notch"}/></span> : "Send Mail"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className={"col-10"}>
|
<div className={"col-10"}>
|
||||||
{this.state.mail.unsavedMailSettings ?
|
{this.state.mail.unsavedMailSettings ?
|
||||||
<span className={"text-red"}>You need to save your mail settings first.</span> : null}
|
<span className={"text-red"}>You need to save your mail settings first.</span> : null}
|
||||||
@ -354,63 +340,6 @@ export default class Settings extends React.Component {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
getMessagesForm() {
|
|
||||||
|
|
||||||
const editor = <Editor
|
|
||||||
editorState={this.state.messages.editor}
|
|
||||||
onEditorStateChange={this.onEditorStateChange.bind(this)}
|
|
||||||
/>;
|
|
||||||
|
|
||||||
let messageTemplates = {
|
|
||||||
"message_confirm_email": "Confirm E-Mail Message",
|
|
||||||
"message_accept_invite": "Accept Invitation Message",
|
|
||||||
"message_reset_password": "Reset Password Message",
|
|
||||||
};
|
|
||||||
|
|
||||||
let formGroups = [];
|
|
||||||
for (let key in messageTemplates) {
|
|
||||||
let title = messageTemplates[key];
|
|
||||||
if (this.state.messages.isEditing === key) {
|
|
||||||
formGroups.push(
|
|
||||||
<div className={"form-group"} key={"group-" + key}>
|
|
||||||
<label htmlFor={key}>
|
|
||||||
{ title }
|
|
||||||
<ReactTooltip id={"tooltip-" + key} />
|
|
||||||
<Icon icon={"times"} className={"ml-2 text-danger"} style={{cursor: "pointer"}}
|
|
||||||
onClick={() => this.closeEditor(false)} data-type={"error"}
|
|
||||||
data-tip={"Discard Changes"} data-place={"top"} data-effect={"solid"}
|
|
||||||
data-for={"tooltip-" + key}
|
|
||||||
/>
|
|
||||||
<Icon icon={"check"} className={"ml-2 text-success"} style={{cursor: "pointer"}}
|
|
||||||
onClick={() => this.closeEditor(true)} data-type={"success"}
|
|
||||||
data-tip={"Save Changes"} data-place={"top"} data-effect={"solid"}
|
|
||||||
data-for={"tooltip-" + key}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
{ editor }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
formGroups.push(
|
|
||||||
<div className={"form-group"} key={"group-" + key}>
|
|
||||||
<ReactTooltip id={"tooltip-" + key} />
|
|
||||||
<label htmlFor={key}>
|
|
||||||
{ title }
|
|
||||||
<Icon icon={"pencil-alt"} className={"ml-2"} style={{cursor: "pointer"}}
|
|
||||||
onClick={() => this.openEditor(key)} data-type={"info"}
|
|
||||||
data-tip={"Edit Template"} data-place={"top"} data-effect={"solid"}
|
|
||||||
data-for={"tooltip-" + key}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<div className={"p-2 text-black"} style={{backgroundColor: "#d2d6de"}} dangerouslySetInnerHTML={{ __html: sanitizeHtml(this.state.settings[key] ?? "") }} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return formGroups;
|
|
||||||
}
|
|
||||||
|
|
||||||
getRecaptchaForm() {
|
getRecaptchaForm() {
|
||||||
return <>
|
return <>
|
||||||
<div className={"form-group mt-2"}>
|
<div className={"form-group mt-2"}>
|
||||||
@ -520,7 +449,6 @@ export default class Settings extends React.Component {
|
|||||||
const categories = {
|
const categories = {
|
||||||
"general": {color: "primary", icon: "cogs", title: "General Settings", content: this.createGeneralForm()},
|
"general": {color: "primary", icon: "cogs", title: "General Settings", content: this.createGeneralForm()},
|
||||||
"mail": {color: "warning", icon: "envelope", title: "Mail Settings", content: this.createMailForm()},
|
"mail": {color: "warning", icon: "envelope", title: "Mail Settings", content: this.createMailForm()},
|
||||||
"messages": {color: "info", icon: "copy", title: "Message Templates", content: this.getMessagesForm()},
|
|
||||||
"recaptcha": {color: "danger", icon: "google", title: "Google reCaptcha", content: this.getRecaptchaForm()},
|
"recaptcha": {color: "danger", icon: "google", title: "Google reCaptcha", content: this.getRecaptchaForm()},
|
||||||
"uncategorised": {color: "secondary", icon: "stream", title: "Uncategorised", content: this.getUncategorizedForm()},
|
"uncategorised": {color: "secondary", icon: "stream", title: "Uncategorised", content: this.getUncategorizedForm()},
|
||||||
};
|
};
|
||||||
@ -557,16 +485,6 @@ export default class Settings extends React.Component {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
onEditorStateChange(editorState) {
|
|
||||||
this.setState({
|
|
||||||
...this.state,
|
|
||||||
messages: {
|
|
||||||
...this.state.messages,
|
|
||||||
editor: editorState
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onChangeValue(event) {
|
onChangeValue(event) {
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
const name = target.name;
|
const name = target.name;
|
||||||
@ -620,8 +538,6 @@ export default class Settings extends React.Component {
|
|||||||
|
|
||||||
if (category === "mail") {
|
if (category === "mail") {
|
||||||
categoryUpdated.unsavedMailSettings = false;
|
categoryUpdated.unsavedMailSettings = false;
|
||||||
} else if (category === "messages") {
|
|
||||||
categoryUpdated.isEditing = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -635,10 +551,6 @@ export default class Settings extends React.Component {
|
|||||||
onSave(category) {
|
onSave(category) {
|
||||||
this.setState({...this.state, [category]: {...this.state[category], isSaving: true}});
|
this.setState({...this.state, [category]: {...this.state[category], isSaving: true}});
|
||||||
|
|
||||||
if (category === "messages" && this.state.messages.isEditing) {
|
|
||||||
this.closeEditor(true, () => this.onSave(category));
|
|
||||||
}
|
|
||||||
|
|
||||||
let values = {};
|
let values = {};
|
||||||
if (category === "uncategorised") {
|
if (category === "uncategorised") {
|
||||||
for (let prop of this.state.uncategorised.settings) {
|
for (let prop of this.state.uncategorised.settings) {
|
||||||
@ -705,38 +617,4 @@ export default class Settings extends React.Component {
|
|||||||
this.setState({...this.state, mail: newState});
|
this.setState({...this.state, mail: newState});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
closeEditor(save, callback = null) {
|
|
||||||
if (this.state.messages.isEditing) {
|
|
||||||
const key = this.state.messages.isEditing;
|
|
||||||
let newState = { ...this.state, messages: {...this.state.messages, isEditing: null }};
|
|
||||||
|
|
||||||
if (save) {
|
|
||||||
newState.settings = {
|
|
||||||
...this.state.settings,
|
|
||||||
[key]: draftToHtml(convertToRaw(this.state.messages.editor.getCurrentContent())),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
callback = callback || function () { };
|
|
||||||
this.setState(newState, callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
openEditor(message) {
|
|
||||||
this.closeEditor(true);
|
|
||||||
const contentBlock = htmlToDraft(this.state.settings[message] ?? "");
|
|
||||||
if (contentBlock) {
|
|
||||||
const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks);
|
|
||||||
const editorState = EditorState.createWithContent(contentState);
|
|
||||||
this.setState({
|
|
||||||
...this.state,
|
|
||||||
messages: {
|
|
||||||
...this.state.messages,
|
|
||||||
isEditing: message,
|
|
||||||
editor: editorState
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
7
cli.php
7
cli.php
@ -42,7 +42,7 @@ function getDatabaseConfig(): ConnectionData {
|
|||||||
$config = new Configuration();
|
$config = new Configuration();
|
||||||
$database = $config->getDatabase();
|
$database = $config->getDatabase();
|
||||||
if ($database !== null && $database->getProperty("isDocker", false) && !is_file("/.dockerenv")) {
|
if ($database !== null && $database->getProperty("isDocker", false) && !is_file("/.dockerenv")) {
|
||||||
if (count($argv) < 2 || $argv[1] !== "db") {
|
if (count($argv) < 3 || $argv[1] !== "db" || !in_array($argv[2], ["shell", "import", "export"])) {
|
||||||
$command = array_merge(["docker", "exec", "-it", "php", "php"], $argv);
|
$command = array_merge(["docker", "exec", "-it", "php", "php"], $argv);
|
||||||
$proc = proc_open($command, [1 => STDOUT, 2 => STDERR], $pipes, "/application");
|
$proc = proc_open($command, [1 => STDOUT, 2 => STDERR], $pipes, "/application");
|
||||||
exit(proc_close($proc));
|
exit(proc_close($proc));
|
||||||
@ -464,11 +464,12 @@ function onRoutes(array $argv) {
|
|||||||
_exit("Error fetching routes: " . $req->getLastError());
|
_exit("Error fetching routes: " . $req->getLastError());
|
||||||
} else {
|
} else {
|
||||||
$routes = $req->getResult()["routes"];
|
$routes = $req->getResult()["routes"];
|
||||||
$head = ["uid", "request", "action", "target", "extra", "active"];
|
$head = ["uid", "request", "action", "target", "extra", "active", "exact"];
|
||||||
|
|
||||||
// strict boolean
|
// strict boolean
|
||||||
foreach ($routes as &$route) {
|
foreach ($routes as &$route) {
|
||||||
$route["active"] = $route["active"] ? "true" : "false";
|
$route["active"] = $route["active"] ? "true" : "false";
|
||||||
|
$route["exact"] = $route["exact"] ? "true" : "false";
|
||||||
}
|
}
|
||||||
|
|
||||||
printTable($head, $routes);
|
printTable($head, $routes);
|
||||||
@ -482,7 +483,7 @@ function onRoutes(array $argv) {
|
|||||||
"request" => $argv[3],
|
"request" => $argv[3],
|
||||||
"action" => $argv[4],
|
"action" => $argv[4],
|
||||||
"target" => $argv[5],
|
"target" => $argv[5],
|
||||||
"extra" => $argv[6] ?? ""
|
"extra" => $argv[7] ?? "",
|
||||||
);
|
);
|
||||||
|
|
||||||
$req = new Api\Routes\Add($user);
|
$req = new Api\Routes\Add($user);
|
||||||
|
@ -94,7 +94,7 @@ namespace Api\Mail {
|
|||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
|
|
||||||
$mailConfig = $this->getMailConfig();
|
$mailConfig = $this->getMailConfig();
|
||||||
if (!$this->success) {
|
if (!$this->success || $mailConfig === null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -411,7 +411,7 @@ namespace Api\Mail {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$mailConfig = $this->getMailConfig();
|
$mailConfig = $this->getMailConfig();
|
||||||
if (!$this->success) {
|
if (!$this->success || $mailConfig === null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +68,10 @@ namespace Api\Routes {
|
|||||||
use Api\RoutesAPI;
|
use Api\RoutesAPI;
|
||||||
use Driver\SQL\Condition\Compare;
|
use Driver\SQL\Condition\Compare;
|
||||||
use Driver\SQL\Condition\CondBool;
|
use Driver\SQL\Condition\CondBool;
|
||||||
use Objects\Router;
|
use Objects\Router\DocumentRoute;
|
||||||
|
use Objects\Router\RedirectRoute;
|
||||||
|
use Objects\Router\Router;
|
||||||
|
use Objects\Router\StaticFileRoute;
|
||||||
use Objects\User;
|
use Objects\User;
|
||||||
|
|
||||||
class Fetch extends RoutesAPI {
|
class Fetch extends RoutesAPI {
|
||||||
@ -367,17 +370,17 @@ namespace Api\Routes {
|
|||||||
$exact = $sql->parseBool($row["exact"]);
|
$exact = $sql->parseBool($row["exact"]);
|
||||||
switch ($row["action"]) {
|
switch ($row["action"]) {
|
||||||
case "redirect_temporary":
|
case "redirect_temporary":
|
||||||
$this->router->addRoute(new Router\RedirectRoute($request, $exact, $target, 307));
|
$this->router->addRoute(new RedirectRoute($request, $exact, $target, 307));
|
||||||
break;
|
break;
|
||||||
case "redirect_permanently":
|
case "redirect_permanently":
|
||||||
$this->router->addRoute(new Router\RedirectRoute($request, $exact, $target, 308));
|
$this->router->addRoute(new RedirectRoute($request, $exact, $target, 308));
|
||||||
break;
|
break;
|
||||||
case "static":
|
case "static":
|
||||||
$this->router->addRoute(new Router\StaticFileRoute($request, $exact, $target));
|
$this->router->addRoute(new StaticFileRoute($request, $exact, $target));
|
||||||
break;
|
break;
|
||||||
case "dynamic":
|
case "dynamic":
|
||||||
$extra = json_decode($row["extra"]) ?? [];
|
$extra = json_decode($row["extra"]) ?? [];
|
||||||
$this->router->addRoute(new Router\DocumentRoute($request, $exact, $target, ...$extra));
|
$this->router->addRoute(new DocumentRoute($request, $exact, $target, ...$extra));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
@ -1076,6 +1076,10 @@ namespace Api\User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$settings = $this->user->getConfiguration()->getSettings();
|
$settings = $this->user->getConfiguration()->getSettings();
|
||||||
|
if (!$settings->isMailEnabled()) {
|
||||||
|
return $this->createError("The mail service is not enabled, please contact the server administration.");
|
||||||
|
}
|
||||||
|
|
||||||
if ($settings->isRecaptchaEnabled()) {
|
if ($settings->isRecaptchaEnabled()) {
|
||||||
$captcha = $this->getParam("captcha");
|
$captcha = $this->getParam("captcha");
|
||||||
$req = new VerifyCaptcha($this->user);
|
$req = new VerifyCaptcha($this->user);
|
||||||
|
@ -1,390 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Objects {
|
|
||||||
|
|
||||||
use Driver\Logger\Logger;
|
|
||||||
use Objects\Router\AbstractRoute;
|
|
||||||
|
|
||||||
class Router {
|
|
||||||
|
|
||||||
private User $user;
|
|
||||||
private Logger $logger;
|
|
||||||
protected array $routes;
|
|
||||||
protected array $statusCodeRoutes;
|
|
||||||
|
|
||||||
public function __construct(User $user) {
|
|
||||||
$this->user = $user;
|
|
||||||
$this->logger = new Logger("Router", $user->getSQL());
|
|
||||||
$this->routes = [];
|
|
||||||
$this->statusCodeRoutes = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function run(string $url): string {
|
|
||||||
|
|
||||||
// TODO: do we want a global try cache and return status page 500 on any error?
|
|
||||||
// or do we want to have a global status page function here?
|
|
||||||
|
|
||||||
$url = strtok($url, "?");
|
|
||||||
foreach ($this->routes as $route) {
|
|
||||||
$pathParams = $route->match($url);
|
|
||||||
if ($pathParams !== false) {
|
|
||||||
return $route->call($this, $pathParams);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->returnStatusCode(404);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function returnStatusCode(int $code, array $params = []): string {
|
|
||||||
http_response_code($code);
|
|
||||||
$params["status_code"] = $code;
|
|
||||||
$params["status_description"] = HTTP_STATUS_DESCRIPTIONS[$code] ?? "Unknown Error";
|
|
||||||
$route = $this->statusCodeRoutes[strval($code)] ?? null;
|
|
||||||
if ($route) {
|
|
||||||
return $route->call($this, $params);
|
|
||||||
} else {
|
|
||||||
$req = new \Api\Template\Render($this->user);
|
|
||||||
$res = $req->execute(["file" => "error_document.twig", "parameters" => $params]);
|
|
||||||
if ($res) {
|
|
||||||
return $req->getResult()["html"];
|
|
||||||
} else {
|
|
||||||
var_dump($req->getLastError());
|
|
||||||
$description = htmlspecialchars($params["status_description"]);
|
|
||||||
return "<b>$code - $description</b>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function addRoute(AbstractRoute $route) {
|
|
||||||
if (preg_match("/^\d+$/", $route->getPattern())) {
|
|
||||||
$this->statusCodeRoutes[$route->getPattern()] = $route;
|
|
||||||
} else {
|
|
||||||
$this->routes[] = $route;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function writeCache(string $file): bool {
|
|
||||||
|
|
||||||
$routes = "";
|
|
||||||
foreach ($this->routes as $route) {
|
|
||||||
$constructor = $route->generateCache();
|
|
||||||
$routes .= "\n \$this->addRoute($constructor);";
|
|
||||||
}
|
|
||||||
|
|
||||||
$date = (new \DateTime())->format("Y/m/d H:i:s");
|
|
||||||
$code = "<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DO NOT EDIT!
|
|
||||||
* This file is automatically generated by the RoutesAPI on $date.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Cache;
|
|
||||||
use Objects\User;
|
|
||||||
use Objects\Router;
|
|
||||||
|
|
||||||
class RouterCache extends Router {
|
|
||||||
|
|
||||||
public function __construct(User \$user) {
|
|
||||||
parent::__construct(\$user);$routes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
";
|
|
||||||
|
|
||||||
if (@file_put_contents($file, $code) === false) {
|
|
||||||
$this->logger->severe("Could not write Router cache file: $file");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getUser(): User {
|
|
||||||
return $this->user;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getLogger(): Logger {
|
|
||||||
return $this->logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function cleanURL(string $url, bool $cleanGET = true): string {
|
|
||||||
// strip GET parameters
|
|
||||||
if ($cleanGET) {
|
|
||||||
if (($index = strpos($url, "?")) !== false) {
|
|
||||||
$url = substr($url, 0, $index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// strip document reference part
|
|
||||||
if (($index = strpos($url, "#")) !== false) {
|
|
||||||
$url = substr($url, 0, $index);
|
|
||||||
}
|
|
||||||
|
|
||||||
// strip leading slash
|
|
||||||
return preg_replace("/^\/+/", "", $url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace Objects\Router {
|
|
||||||
|
|
||||||
use Api\Parameter\Parameter;
|
|
||||||
use Elements\Document;
|
|
||||||
use Objects\Router;
|
|
||||||
use PHPUnit\TextUI\ReflectionException;
|
|
||||||
|
|
||||||
abstract class AbstractRoute {
|
|
||||||
|
|
||||||
private string $pattern;
|
|
||||||
private bool $exact;
|
|
||||||
|
|
||||||
public function __construct(string $pattern, bool $exact = true) {
|
|
||||||
$this->pattern = $pattern;
|
|
||||||
$this->exact = $exact;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function parseParamType(?string $type): ?int {
|
|
||||||
if ($type === null || trim($type) === "") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$type = strtolower(trim($type));
|
|
||||||
if (in_array($type, ["int", "integer"])) {
|
|
||||||
return Parameter::TYPE_INT;
|
|
||||||
} else if (in_array($type, ["float", "double"])) {
|
|
||||||
return Parameter::TYPE_FLOAT;
|
|
||||||
} else if (in_array($type, ["bool", "boolean"])) {
|
|
||||||
return Parameter::TYPE_BOOLEAN;
|
|
||||||
} else {
|
|
||||||
return Parameter::TYPE_STRING;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getPattern(): string {
|
|
||||||
return $this->pattern;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract function call(Router $router, array $params): string;
|
|
||||||
|
|
||||||
protected function getArgs(): array {
|
|
||||||
return [$this->pattern, $this->exact];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function generateCache(): string {
|
|
||||||
$reflection = new \ReflectionClass($this);
|
|
||||||
$className = $reflection->getName();
|
|
||||||
$args = implode(", ", array_map(function ($arg) {
|
|
||||||
return var_export($arg, true);
|
|
||||||
}, $this->getArgs()));
|
|
||||||
return "new \\$className($args)";
|
|
||||||
}
|
|
||||||
|
|
||||||
public function match(string $url) {
|
|
||||||
|
|
||||||
# /test/{abc}/{param:?}/{xyz:int}/{aaa:int?}
|
|
||||||
$patternParts = explode("/", Router::cleanURL($this->pattern, false));
|
|
||||||
$countPattern = count($patternParts);
|
|
||||||
$patternOffset = 0;
|
|
||||||
|
|
||||||
# /test/param/optional/123
|
|
||||||
$urlParts = explode("/", $url);
|
|
||||||
$countUrl = count($urlParts);
|
|
||||||
$urlOffset = 0;
|
|
||||||
|
|
||||||
$params = [];
|
|
||||||
for (; $patternOffset < $countPattern; $patternOffset++) {
|
|
||||||
|
|
||||||
if (!preg_match("/^{.*}$/", $patternParts[$patternOffset])) {
|
|
||||||
|
|
||||||
// not a parameter? check if it matches
|
|
||||||
if ($urlOffset >= $countUrl || $urlParts[$urlOffset] !== $patternParts[$patternOffset]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$urlOffset++;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// we got a parameter here
|
|
||||||
$paramDefinition = explode(":", substr($patternParts[$patternOffset], 1, -1));
|
|
||||||
$paramName = array_shift($paramDefinition);
|
|
||||||
$paramType = array_shift($paramDefinition);
|
|
||||||
$paramOptional = endsWith($paramType, "?");
|
|
||||||
if ($paramOptional) {
|
|
||||||
$paramType = substr($paramType, 0, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$paramType = self::parseParamType($paramType);
|
|
||||||
if ($urlOffset >= $countUrl || $urlParts[$urlOffset] === "") {
|
|
||||||
if ($paramOptional) {
|
|
||||||
$param = $urlParts[$urlOffset] ?? null;
|
|
||||||
if ($param !== null && $paramType !== null && Parameter::parseType($param) !== $paramType) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$params[$paramName] = $param;
|
|
||||||
if ($urlOffset < $countUrl) {
|
|
||||||
$urlOffset++;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$param = $urlParts[$urlOffset];
|
|
||||||
if ($paramType !== null && Parameter::parseType($param) !== $paramType) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$params[$paramName] = $param;
|
|
||||||
$urlOffset++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($urlOffset !== $countUrl && $this->exact) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $params;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class EmptyRoute extends AbstractRoute {
|
|
||||||
|
|
||||||
public function __construct(string $pattern, bool $exact = true) {
|
|
||||||
parent::__construct($pattern, $exact);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function call(Router $router, array $params): string {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StaticFileRoute extends AbstractRoute {
|
|
||||||
|
|
||||||
private string $path;
|
|
||||||
private int $code;
|
|
||||||
|
|
||||||
public function __construct(string $pattern, bool $exact, string $path, int $code = 200) {
|
|
||||||
parent::__construct($pattern, $exact);
|
|
||||||
$this->path = $path;
|
|
||||||
$this->code = $code;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function call(Router $router, array $params): string {
|
|
||||||
http_response_code($this->code);
|
|
||||||
return serveStatic(WEBROOT, $this->path);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getArgs(): array {
|
|
||||||
return array_merge(parent::getArgs(), [$this->path, $this->code]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StaticRoute extends AbstractRoute {
|
|
||||||
|
|
||||||
private string $data;
|
|
||||||
private int $code;
|
|
||||||
|
|
||||||
public function __construct(string $pattern, bool $exact, string $data, int $code = 200) {
|
|
||||||
parent::__construct($pattern, $exact);
|
|
||||||
$this->data = $data;
|
|
||||||
$this->code = $code;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function call(Router $router, array $params): string {
|
|
||||||
http_response_code($this->code);
|
|
||||||
return $this->data;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getArgs(): array {
|
|
||||||
return array_merge(parent::getArgs(), [$this->data, $this->code]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RedirectRoute extends AbstractRoute {
|
|
||||||
|
|
||||||
private string $destination;
|
|
||||||
private int $code;
|
|
||||||
|
|
||||||
public function __construct(string $pattern, bool $exact, string $destination, int $code = 307) {
|
|
||||||
parent::__construct($pattern, $exact);
|
|
||||||
$this->destination = $destination;
|
|
||||||
$this->code = $code;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function call(Router $router, array $params): string {
|
|
||||||
header("Location: $this->destination");
|
|
||||||
http_response_code($this->code);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getArgs(): array {
|
|
||||||
return array_merge(parent::getArgs(), [$this->destination, $this->code]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DocumentRoute extends AbstractRoute {
|
|
||||||
|
|
||||||
private string $className;
|
|
||||||
private array $args;
|
|
||||||
private ?\ReflectionClass $reflectionClass;
|
|
||||||
|
|
||||||
public function __construct(string $pattern, bool $exact, string $className, ...$args) {
|
|
||||||
parent::__construct($pattern, $exact);
|
|
||||||
$this->className = $className;
|
|
||||||
$this->args = $args;
|
|
||||||
$this->reflectionClass = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function loadClass(): bool {
|
|
||||||
|
|
||||||
if ($this->reflectionClass === null) {
|
|
||||||
try {
|
|
||||||
$file = getClassPath($this->className);
|
|
||||||
if (file_exists($file)) {
|
|
||||||
$this->reflectionClass = new \ReflectionClass($this->className);
|
|
||||||
if ($this->reflectionClass->isSubclassOf(Document::class)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (ReflectionException $exception) {
|
|
||||||
$this->reflectionClass = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->reflectionClass = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function match(string $url) {
|
|
||||||
$match = parent::match($url);
|
|
||||||
if ($match === false || !$this->loadClass()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $match;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getArgs(): array {
|
|
||||||
return array_merge(parent::getArgs(), [$this->className], $this->args);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function call(Router $router, array $params): string {
|
|
||||||
if (!$this->loadClass()) {
|
|
||||||
return $router->returnStatusCode(500, [ "message" => "Error loading class: $this->className"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$args = array_merge([$router->getUser()], $this->args);
|
|
||||||
$document = $this->reflectionClass->newInstanceArgs($args);
|
|
||||||
return $document->getCode($params);
|
|
||||||
} catch (\ReflectionException $e) {
|
|
||||||
return $router->returnStatusCode(500, [ "message" => "Error loading class $this->className: " . $e->getMessage()]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
121
core/Objects/Router/AbstractRoute.class.php
Normal file
121
core/Objects/Router/AbstractRoute.class.php
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Objects\Router;
|
||||||
|
|
||||||
|
use Api\Parameter\Parameter;
|
||||||
|
|
||||||
|
abstract class AbstractRoute {
|
||||||
|
|
||||||
|
private string $pattern;
|
||||||
|
private bool $exact;
|
||||||
|
|
||||||
|
public function __construct(string $pattern, bool $exact = true) {
|
||||||
|
$this->pattern = $pattern;
|
||||||
|
$this->exact = $exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function parseParamType(?string $type): ?int {
|
||||||
|
if ($type === null || trim($type) === "") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = strtolower(trim($type));
|
||||||
|
if (in_array($type, ["int", "integer"])) {
|
||||||
|
return Parameter::TYPE_INT;
|
||||||
|
} else if (in_array($type, ["float", "double"])) {
|
||||||
|
return Parameter::TYPE_FLOAT;
|
||||||
|
} else if (in_array($type, ["bool", "boolean"])) {
|
||||||
|
return Parameter::TYPE_BOOLEAN;
|
||||||
|
} else {
|
||||||
|
return Parameter::TYPE_STRING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPattern(): string {
|
||||||
|
return $this->pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract function call(Router $router, array $params): string;
|
||||||
|
|
||||||
|
protected function getArgs(): array {
|
||||||
|
return [$this->pattern, $this->exact];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateCache(): string {
|
||||||
|
$reflection = new \ReflectionClass($this);
|
||||||
|
$className = $reflection->getName();
|
||||||
|
$args = implode(", ", array_map(function ($arg) {
|
||||||
|
return var_export($arg, true);
|
||||||
|
}, $this->getArgs()));
|
||||||
|
return "new \\$className($args)";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function match(string $url) {
|
||||||
|
|
||||||
|
# /test/{abc}/{param:?}/{xyz:int}/{aaa:int?}
|
||||||
|
$patternParts = explode("/", Router::cleanURL($this->pattern, false));
|
||||||
|
$countPattern = count($patternParts);
|
||||||
|
$patternOffset = 0;
|
||||||
|
|
||||||
|
# /test/param/optional/123
|
||||||
|
$urlParts = explode("/", $url);
|
||||||
|
$countUrl = count($urlParts);
|
||||||
|
$urlOffset = 0;
|
||||||
|
|
||||||
|
$params = [];
|
||||||
|
for (; $patternOffset < $countPattern; $patternOffset++) {
|
||||||
|
|
||||||
|
if (!preg_match("/^{.*}$/", $patternParts[$patternOffset])) {
|
||||||
|
|
||||||
|
// not a parameter? check if it matches
|
||||||
|
if ($urlOffset >= $countUrl || $urlParts[$urlOffset] !== $patternParts[$patternOffset]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$urlOffset++;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// we got a parameter here
|
||||||
|
$paramDefinition = explode(":", substr($patternParts[$patternOffset], 1, -1));
|
||||||
|
$paramName = array_shift($paramDefinition);
|
||||||
|
$paramType = array_shift($paramDefinition);
|
||||||
|
$paramOptional = endsWith($paramType, "?");
|
||||||
|
if ($paramOptional) {
|
||||||
|
$paramType = substr($paramType, 0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$paramType = self::parseParamType($paramType);
|
||||||
|
if ($urlOffset >= $countUrl || $urlParts[$urlOffset] === "") {
|
||||||
|
if ($paramOptional) {
|
||||||
|
$param = $urlParts[$urlOffset] ?? null;
|
||||||
|
if ($param !== null && $paramType !== null && Parameter::parseType($param) !== $paramType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$params[$paramName] = $param;
|
||||||
|
if ($urlOffset < $countUrl) {
|
||||||
|
$urlOffset++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$param = $urlParts[$urlOffset];
|
||||||
|
if ($paramType !== null && Parameter::parseType($param) !== $paramType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$params[$paramName] = $param;
|
||||||
|
$urlOffset++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($urlOffset !== $countUrl && $this->exact) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $params;
|
||||||
|
}
|
||||||
|
}
|
60
core/Objects/Router/ApiRoute.class.php
Normal file
60
core/Objects/Router/ApiRoute.class.php
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Objects\Router;
|
||||||
|
|
||||||
|
use Api\Request;
|
||||||
|
use ReflectionClass;
|
||||||
|
use ReflectionException;
|
||||||
|
|
||||||
|
class ApiRoute extends AbstractRoute {
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
parent::__construct("/api/{endpoint:?}/{method:?}", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function call(Router $router, array $params): string {
|
||||||
|
$user = $router->getUser();
|
||||||
|
if (empty($params["endpoint"])) {
|
||||||
|
header("Content-Type: text/html");
|
||||||
|
$document = new \Elements\TemplateDocument($user, "swagger.twig");
|
||||||
|
return $document->getCode();
|
||||||
|
} else if(!preg_match("/[a-zA-Z]+(\/[a-zA-Z]+)*/", $params["endpoint"])) {
|
||||||
|
http_response_code(400);
|
||||||
|
$response = createError("Invalid Method");
|
||||||
|
} else {
|
||||||
|
$apiEndpoint = ucfirst($params["endpoint"]);
|
||||||
|
if (!empty($params["method"])) {
|
||||||
|
$apiMethod = ucfirst($params["method"]);
|
||||||
|
$parentClass = "\\Api\\${apiEndpoint}API";
|
||||||
|
$apiClass = "\\Api\\${apiEndpoint}\\${apiMethod}";
|
||||||
|
} else {
|
||||||
|
$apiClass = "\\Api\\${apiEndpoint}";
|
||||||
|
$parentClass = $apiClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$file = getClassPath($parentClass);
|
||||||
|
if (!file_exists($file) || !class_exists($parentClass) || !class_exists($apiClass)) {
|
||||||
|
http_response_code(404);
|
||||||
|
$response = createError("Not found");
|
||||||
|
} else {
|
||||||
|
$apiClass = new ReflectionClass($apiClass);
|
||||||
|
if(!$apiClass->isSubclassOf(Request::class) || !$apiClass->isInstantiable()) {
|
||||||
|
http_response_code(400);
|
||||||
|
$response = createError("Invalid Method");
|
||||||
|
} else {
|
||||||
|
$request = $apiClass->newInstanceArgs(array($user, true));
|
||||||
|
$request->execute();
|
||||||
|
$response = $request->getJsonResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ReflectionException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
$response = createError("Error instantiating class: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header("Content-Type: application/json");
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
70
core/Objects/Router/DocumentRoute.class.php
Normal file
70
core/Objects/Router/DocumentRoute.class.php
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Objects\Router;
|
||||||
|
|
||||||
|
use Elements\Document;
|
||||||
|
use ReflectionException;
|
||||||
|
|
||||||
|
class DocumentRoute extends AbstractRoute {
|
||||||
|
|
||||||
|
private string $className;
|
||||||
|
private array $args;
|
||||||
|
private ?\ReflectionClass $reflectionClass;
|
||||||
|
|
||||||
|
public function __construct(string $pattern, bool $exact, string $className, ...$args) {
|
||||||
|
parent::__construct($pattern, $exact);
|
||||||
|
$this->className = $className;
|
||||||
|
$this->args = $args;
|
||||||
|
$this->reflectionClass = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadClass(): bool {
|
||||||
|
|
||||||
|
if ($this->reflectionClass === null) {
|
||||||
|
try {
|
||||||
|
$file = getClassPath($this->className);
|
||||||
|
if (file_exists($file)) {
|
||||||
|
$this->reflectionClass = new \ReflectionClass($this->className);
|
||||||
|
if ($this->reflectionClass->isSubclassOf(Document::class)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ReflectionException $exception) {
|
||||||
|
$this->reflectionClass = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->reflectionClass = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function match(string $url) {
|
||||||
|
$match = parent::match($url);
|
||||||
|
if ($match === false || !$this->loadClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $match;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getArgs(): array {
|
||||||
|
return array_merge(parent::getArgs(), [$this->className], $this->args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function call(Router $router, array $params): string {
|
||||||
|
if (!$this->loadClass()) {
|
||||||
|
return $router->returnStatusCode(500, [ "message" => "Error loading class: $this->className"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$args = array_merge([$router->getUser()], $this->args);
|
||||||
|
$document = $this->reflectionClass->newInstanceArgs($args);
|
||||||
|
return $document->getCode($params);
|
||||||
|
} catch (\ReflectionException $e) {
|
||||||
|
return $router->returnStatusCode(500, [ "message" => "Error loading class $this->className: " . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
core/Objects/Router/EmptyRoute.class.php
Normal file
14
core/Objects/Router/EmptyRoute.class.php
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Objects\Router;
|
||||||
|
|
||||||
|
class EmptyRoute extends AbstractRoute {
|
||||||
|
|
||||||
|
public function __construct(string $pattern, bool $exact = true) {
|
||||||
|
parent::__construct($pattern, $exact);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function call(Router $router, array $params): string {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
25
core/Objects/Router/RedirectRoute.class.php
Normal file
25
core/Objects/Router/RedirectRoute.class.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Objects\Router;
|
||||||
|
|
||||||
|
class RedirectRoute extends AbstractRoute {
|
||||||
|
|
||||||
|
private string $destination;
|
||||||
|
private int $code;
|
||||||
|
|
||||||
|
public function __construct(string $pattern, bool $exact, string $destination, int $code = 307) {
|
||||||
|
parent::__construct($pattern, $exact);
|
||||||
|
$this->destination = $destination;
|
||||||
|
$this->code = $code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function call(Router $router, array $params): string {
|
||||||
|
header("Location: $this->destination");
|
||||||
|
http_response_code($this->code);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getArgs(): array {
|
||||||
|
return array_merge(parent::getArgs(), [$this->destination, $this->code]);
|
||||||
|
}
|
||||||
|
}
|
128
core/Objects/Router/Router.class.php
Normal file
128
core/Objects/Router/Router.class.php
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Objects\Router;
|
||||||
|
|
||||||
|
use Driver\Logger\Logger;
|
||||||
|
use Objects\User;
|
||||||
|
|
||||||
|
class Router {
|
||||||
|
|
||||||
|
private User $user;
|
||||||
|
private Logger $logger;
|
||||||
|
protected array $routes;
|
||||||
|
protected array $statusCodeRoutes;
|
||||||
|
|
||||||
|
public function __construct(User $user) {
|
||||||
|
$this->user = $user;
|
||||||
|
$this->logger = new Logger("Router", $user->getSQL());
|
||||||
|
$this->routes = [];
|
||||||
|
$this->statusCodeRoutes = [];
|
||||||
|
|
||||||
|
$this->addRoute(new ApiRoute());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run(string $url): string {
|
||||||
|
|
||||||
|
// TODO: do we want a global try cache and return status page 500 on any error?
|
||||||
|
// or do we want to have a global status page function here?
|
||||||
|
|
||||||
|
$url = strtok($url, "?");
|
||||||
|
foreach ($this->routes as $route) {
|
||||||
|
$pathParams = $route->match($url);
|
||||||
|
if ($pathParams !== false) {
|
||||||
|
return $route->call($this, $pathParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->returnStatusCode(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function returnStatusCode(int $code, array $params = []): string {
|
||||||
|
http_response_code($code);
|
||||||
|
$params["status_code"] = $code;
|
||||||
|
$params["status_description"] = HTTP_STATUS_DESCRIPTIONS[$code] ?? "Unknown Error";
|
||||||
|
$route = $this->statusCodeRoutes[strval($code)] ?? null;
|
||||||
|
if ($route) {
|
||||||
|
return $route->call($this, $params);
|
||||||
|
} else {
|
||||||
|
$req = new \Api\Template\Render($this->user);
|
||||||
|
$res = $req->execute(["file" => "error_document.twig", "parameters" => $params]);
|
||||||
|
if ($res) {
|
||||||
|
return $req->getResult()["html"];
|
||||||
|
} else {
|
||||||
|
var_dump($req->getLastError());
|
||||||
|
$description = htmlspecialchars($params["status_description"]);
|
||||||
|
return "<b>$code - $description</b>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addRoute(AbstractRoute $route) {
|
||||||
|
if (preg_match("/^\d+$/", $route->getPattern())) {
|
||||||
|
$this->statusCodeRoutes[$route->getPattern()] = $route;
|
||||||
|
} else {
|
||||||
|
$this->routes[] = $route;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function writeCache(string $file): bool {
|
||||||
|
|
||||||
|
$routes = "";
|
||||||
|
foreach ($this->routes as $route) {
|
||||||
|
$constructor = $route->generateCache();
|
||||||
|
$routes .= "\n \$this->addRoute($constructor);";
|
||||||
|
}
|
||||||
|
|
||||||
|
$date = (new \DateTime())->format("Y/m/d H:i:s");
|
||||||
|
$code = "<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DO NOT EDIT!
|
||||||
|
* This file is automatically generated by the RoutesAPI on $date.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Cache;
|
||||||
|
use Objects\User;
|
||||||
|
use Objects\Router\Router;
|
||||||
|
|
||||||
|
class RouterCache extends Router {
|
||||||
|
|
||||||
|
public function __construct(User \$user) {
|
||||||
|
parent::__construct(\$user);$routes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
";
|
||||||
|
|
||||||
|
if (@file_put_contents($file, $code) === false) {
|
||||||
|
$this->logger->severe("Could not write Router cache file: $file");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUser(): User {
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLogger(): Logger {
|
||||||
|
return $this->logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function cleanURL(string $url, bool $cleanGET = true): string {
|
||||||
|
// strip GET parameters
|
||||||
|
if ($cleanGET) {
|
||||||
|
if (($index = strpos($url, "?")) !== false) {
|
||||||
|
$url = substr($url, 0, $index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// strip document reference part
|
||||||
|
if (($index = strpos($url, "#")) !== false) {
|
||||||
|
$url = substr($url, 0, $index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// strip leading slash
|
||||||
|
return preg_replace("/^\/+/", "", $url);
|
||||||
|
}
|
||||||
|
}
|
24
core/Objects/Router/StaticFileRoute.class.php
Normal file
24
core/Objects/Router/StaticFileRoute.class.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Objects\Router;
|
||||||
|
|
||||||
|
class StaticFileRoute extends AbstractRoute {
|
||||||
|
|
||||||
|
private string $path;
|
||||||
|
private int $code;
|
||||||
|
|
||||||
|
public function __construct(string $pattern, bool $exact, string $path, int $code = 200) {
|
||||||
|
parent::__construct($pattern, $exact);
|
||||||
|
$this->path = $path;
|
||||||
|
$this->code = $code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function call(Router $router, array $params): string {
|
||||||
|
http_response_code($this->code);
|
||||||
|
return serveStatic(WEBROOT, $this->path);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getArgs(): array {
|
||||||
|
return array_merge(parent::getArgs(), [$this->path, $this->code]);
|
||||||
|
}
|
||||||
|
}
|
24
core/Objects/Router/StaticRoute.class.php
Normal file
24
core/Objects/Router/StaticRoute.class.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Objects\Router;
|
||||||
|
|
||||||
|
class StaticRoute extends AbstractRoute {
|
||||||
|
|
||||||
|
private string $data;
|
||||||
|
private int $code;
|
||||||
|
|
||||||
|
public function __construct(string $pattern, bool $exact, string $data, int $code = 200) {
|
||||||
|
parent::__construct($pattern, $exact);
|
||||||
|
$this->data = $data;
|
||||||
|
$this->code = $code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function call(Router $router, array $params): string {
|
||||||
|
http_response_code($this->code);
|
||||||
|
return $this->data;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getArgs(): array {
|
||||||
|
return array_merge(parent::getArgs(), [$this->data, $this->code]);
|
||||||
|
}
|
||||||
|
}
|
@ -20,7 +20,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<input type="password" autocomplete='password' name='password' id='password' class="form-control" placeholder="Password">
|
<input type="password" autocomplete='password' name='password' id='password' class="form-control" placeholder="Password">
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group mt-5 mb-4">
|
<div class="ml-2" style="line-height: 38px;">
|
||||||
|
<a href="/resetPassword">Forgot Password?</a>
|
||||||
|
</div>
|
||||||
|
<div class="input-group mt-3 mb-4">
|
||||||
<button type="button" class="btn btn-primary" id='btnLogin'>Sign In</button>
|
<button type="button" class="btn btn-primary" id='btnLogin'>Sign In</button>
|
||||||
{% if site.registrationEnabled %}
|
{% if site.registrationEnabled %}
|
||||||
<div class="ml-2" style="line-height: 38px;">Don't have an account yet? <a href="/register">Click here</a> to register.</div>
|
<div class="ml-2" style="line-height: 38px;">Don't have an account yet? <a href="/register">Click here</a> to register.</div>
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
You can either <a href="javascript:history.back()">Go Back to previous page</a>
|
You can either <a href="javascript:history.back()">Go Back to previous page</a>
|
||||||
or try to <a href="javascript:document.location.reload()">reload the page</a>.
|
or try to <a href="javascript:document.location.reload()">reload the page</a>.
|
||||||
</p>
|
</p>
|
||||||
|
<p>{{ message }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,6 +4,12 @@ server {
|
|||||||
access_log /var/log/nginx/access.log;
|
access_log /var/log/nginx/access.log;
|
||||||
root /application;
|
root /application;
|
||||||
|
|
||||||
|
# rewrite error codes
|
||||||
|
error_page 400 /index.php?error=400;
|
||||||
|
error_page 403 /index.php?error=403;
|
||||||
|
error_page 404 /index.php?error=404;
|
||||||
|
error_page 500 /index.php?error=500;
|
||||||
|
|
||||||
# rewrite api
|
# rewrite api
|
||||||
rewrite ^/api(/.*)$ /index.php?api=$1;
|
rewrite ^/api(/.*)$ /index.php?api=$1;
|
||||||
|
|
||||||
|
@ -1 +0,0 @@
|
|||||||
php_flag engine on
|
|
123
index.php
123
index.php
@ -12,13 +12,12 @@ if (is_file("MAINTENANCE") && !in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '
|
|||||||
die();
|
die();
|
||||||
}
|
}
|
||||||
|
|
||||||
use Api\Request;
|
|
||||||
use Configuration\Configuration;
|
use Configuration\Configuration;
|
||||||
use Objects\Router;
|
use Objects\Router\Router;
|
||||||
|
|
||||||
if (!is_readable(getClassPath(Configuration::class))) {
|
if (!is_readable(getClassPath(Configuration::class))) {
|
||||||
header("Content-Type: application/json");
|
header("Content-Type: application/json");
|
||||||
die(json_encode(array( "success" => false, "msg" => "Configuration class is not readable, check permissions before proceeding." )));
|
die(json_encode([ "success" => false, "msg" => "Configuration class is not readable, check permissions before proceeding." ]));
|
||||||
}
|
}
|
||||||
|
|
||||||
$config = new Configuration();
|
$config = new Configuration();
|
||||||
@ -27,103 +26,49 @@ $sql = $user->getSQL();
|
|||||||
$settings = $config->getSettings();
|
$settings = $config->getSettings();
|
||||||
$installation = !$sql || ($sql->isConnected() && !$settings->isInstalled());
|
$installation = !$sql || ($sql->isConnected() && !$settings->isInstalled());
|
||||||
|
|
||||||
// API routes, prefix: /api/
|
$requestedUri = $_GET["site"] ?? $_GET["api"] ?? $_SERVER["REQUEST_URI"];
|
||||||
// TODO: move this to Router?
|
$requestedUri = Router::cleanURL($requestedUri);
|
||||||
if (isset($_GET["api"]) && is_string($_GET["api"])) {
|
|
||||||
$isApiResponse = true;
|
if ($installation) {
|
||||||
if ($installation) {
|
if ($requestedUri !== "" && $requestedUri !== "index.php") {
|
||||||
$response = createError("Not installed");
|
$response = "Redirecting to <a href=\"/\">/</a>";
|
||||||
|
header("Location: /");
|
||||||
} else {
|
} else {
|
||||||
$apiFunction = $_GET["api"];
|
$document = new Documents\Install($user);
|
||||||
if (empty($apiFunction) || $apiFunction === "/") {
|
$response = $document->getCode();
|
||||||
$document = new \Elements\TemplateDocument($user, "swagger.twig");
|
|
||||||
$response = $document->getCode();
|
|
||||||
$isApiResponse = false;
|
|
||||||
} else if(!preg_match("/[a-zA-Z]+(\/[a-zA-Z]+)*/", $apiFunction)) {
|
|
||||||
http_response_code(400);
|
|
||||||
$response = createError("Invalid Method");
|
|
||||||
} else {
|
|
||||||
$apiFunction = array_filter(array_map('ucfirst', explode("/", $apiFunction)));
|
|
||||||
if (count($apiFunction) > 1) {
|
|
||||||
$parentClass = "\\Api\\" . reset($apiFunction) . "API";
|
|
||||||
$apiClass = "\\Api\\" . implode("\\", $apiFunction);
|
|
||||||
} else {
|
|
||||||
$apiClass = "\\Api\\" . implode("\\", $apiFunction);
|
|
||||||
$parentClass = $apiClass;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$file = getClassPath($parentClass);
|
|
||||||
if(!file_exists($file) || !class_exists($parentClass) || !class_exists($apiClass)) {
|
|
||||||
http_response_code(404);
|
|
||||||
$response = createError("Not found");
|
|
||||||
} else {
|
|
||||||
$parentClass = new ReflectionClass($parentClass);
|
|
||||||
$apiClass = new ReflectionClass($apiClass);
|
|
||||||
if(!$apiClass->isSubclassOf(Request::class) || !$apiClass->isInstantiable()) {
|
|
||||||
http_response_code(400);
|
|
||||||
$response = createError("Invalid Method");
|
|
||||||
} else {
|
|
||||||
$request = $apiClass->newInstanceArgs(array($user, true));
|
|
||||||
$success = $request->execute();
|
|
||||||
$msg = $request->getLastError();
|
|
||||||
$response = $request->getJsonResult();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (ReflectionException $e) {
|
|
||||||
$response = createError("Error instantiating class: $e");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($isApiResponse) {
|
|
||||||
header("Content-Type: application/json");
|
|
||||||
} else {
|
|
||||||
header("Content-Type: text/html");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// all other routes
|
$router = null;
|
||||||
$requestedUri = $_GET["site"] ?? $_SERVER["REQUEST_URI"];
|
$routerCacheClass = '\Cache\RouterCache';
|
||||||
$requestedUri = Router::cleanURL($requestedUri);
|
$routerCachePath = getClassPath($routerCacheClass);
|
||||||
|
if (is_file($routerCachePath)) {
|
||||||
|
@include_once $routerCachePath;
|
||||||
|
if (class_exists($routerCacheClass)) {
|
||||||
|
$router = new $routerCacheClass($user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($installation) {
|
if ($router === null) {
|
||||||
if ($requestedUri !== "" && $requestedUri !== "index.php") {
|
$req = new \Api\Routes\GenerateCache($user);
|
||||||
$response = "Redirecting to <a href=\"/\">/</a>";
|
if ($req->execute()) {
|
||||||
header("Location: /");
|
$router = $req->getRouter();
|
||||||
} else {
|
} else {
|
||||||
$document = new Documents\Install($user);
|
$message = "Unable to generate router cache: " . $req->getLastError();
|
||||||
$response = $document->getCode();
|
$response = (new Router($user))->returnStatusCode(500, [ "message" => $message ]);
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
|
||||||
$router = null;
|
if ($router !== null) {
|
||||||
|
if (!isset($_GET["site"]) && isset($_GET["error"]) &&
|
||||||
$routerCacheClass = '\Cache\RouterCache';
|
is_string($_GET["error"]) && preg_match("^\d+$", $_GET["error"])) {
|
||||||
$routerCachePath = getClassPath($routerCacheClass);
|
$response = $router->returnStatusCode(intval($_GET["error"]));
|
||||||
if (is_file($routerCachePath)) {
|
} else {
|
||||||
@include_once $routerCachePath;
|
|
||||||
if (class_exists($routerCacheClass)) {
|
|
||||||
$router = new $routerCacheClass($user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($router === null) {
|
|
||||||
$req = new \Api\Routes\GenerateCache($user);
|
|
||||||
if ($req->execute()) {
|
|
||||||
$router = $req->getRouter();
|
|
||||||
} else {
|
|
||||||
$message = "Unable to generate router cache: " . $req->getLastError();
|
|
||||||
$response = (new Router($user))->returnStatusCode(500, [ "message" => $message ]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($router !== null) {
|
|
||||||
$response = $router->run($requestedUri);
|
$response = $router->run($requestedUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
$user->processVisit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$user->processVisit();
|
||||||
}
|
}
|
||||||
|
|
||||||
$user->sendCookies();
|
$user->sendCookies();
|
||||||
|
113
js/admin.min.js
vendored
113
js/admin.min.js
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user