Router, Logger, Bump v1.5
This commit is contained in:
parent
5bb0d1419f
commit
658157167e
@ -91,6 +91,12 @@ export default class PageOverview extends React.Component {
|
||||
checked={route.active === 1}
|
||||
onChange={(e) => this.changeActive(i, e)} />
|
||||
</td>
|
||||
<td className={"text-center"}>
|
||||
<input
|
||||
type={"checkbox"}
|
||||
checked={route.exact === 1}
|
||||
onChange={(e) => this.changeExact(i, e)} />
|
||||
</td>
|
||||
<td>
|
||||
<ReactTooltip id={"delete-" + i} />
|
||||
<Icon icon={"trash"} style={{color: "red", cursor: "pointer"}}
|
||||
@ -158,6 +164,12 @@ export default class PageOverview extends React.Component {
|
||||
data-tip={"True, if the route is currently active."}
|
||||
data-type={"info"} data-place={"bottom"}/>
|
||||
</th>
|
||||
<th className={"text-center"}>
|
||||
Exact
|
||||
<Icon icon={"question-circle"} style={{"color": "#17a2b8"}}
|
||||
data-tip={"True, if the URL must match exactly."}
|
||||
data-type={"info"} data-place={"bottom"}/>
|
||||
</th>
|
||||
<th/>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -200,7 +212,8 @@ export default class PageOverview extends React.Component {
|
||||
action: typeof route.action === 'object' ? route.action.value : route.action,
|
||||
target: route.target,
|
||||
extra: route.extra ?? "",
|
||||
active: route.active === 1
|
||||
active: route.active === 1,
|
||||
exact: route.exact === 1,
|
||||
});
|
||||
}
|
||||
|
||||
@ -235,7 +248,7 @@ export default class PageOverview extends React.Component {
|
||||
|
||||
onAddRoute() {
|
||||
let routes = this.state.routes.slice();
|
||||
routes.push({ request: "", action: "dynamic", target: "", extra: "", active: 1 });
|
||||
routes.push({ request: "", action: "dynamic", target: "", extra: "", active: 1, exact: 1 });
|
||||
this.setState({ ...this.state, routes: routes });
|
||||
}
|
||||
|
||||
@ -247,6 +260,10 @@ export default class PageOverview extends React.Component {
|
||||
this.changeRoute(index, "active", e.target.checked ? 1 : 0);
|
||||
}
|
||||
|
||||
changeExact(index, e) {
|
||||
this.changeRoute(index, "exact", e.target.checked ? 1 : 0);
|
||||
}
|
||||
|
||||
changeRequest(index, e) {
|
||||
this.changeRoute(index, "request", e.target.value);
|
||||
}
|
||||
|
14
cli.php
14
cli.php
@ -103,7 +103,7 @@ function handleDatabase(array $argv) {
|
||||
$user = getUser() or die();
|
||||
$sql = $user->getSQL();
|
||||
applyPatch($sql, $class);
|
||||
} else if ($action === "export" || $action === "import") {
|
||||
} else if (in_array($action, ["export", "import", "shell"])) {
|
||||
|
||||
// database config
|
||||
$config = getDatabaseConfig();
|
||||
@ -147,6 +147,9 @@ function handleDatabase(array $argv) {
|
||||
} else if ($action === "import") {
|
||||
$command_bin = "mysql";
|
||||
$descriptorSpec[0] = ["pipe", "r"];
|
||||
} else if ($action === "shell") {
|
||||
$command_bin = "mysql";
|
||||
$descriptorSpec = [];
|
||||
}
|
||||
} else if ($dbType === "postgres") {
|
||||
|
||||
@ -161,6 +164,9 @@ function handleDatabase(array $argv) {
|
||||
} else if ($action === "import") {
|
||||
$command_bin = "/usr/bin/psql";
|
||||
$descriptorSpec[0] = ["pipe", "r"];
|
||||
} else if ($action === "shell") {
|
||||
$command_bin = "/usr/bin/psql";
|
||||
$descriptorSpec = [];
|
||||
}
|
||||
|
||||
} else {
|
||||
@ -173,7 +179,7 @@ function handleDatabase(array $argv) {
|
||||
|
||||
$command = array_merge([$command_bin], $command_args);
|
||||
if ($config->getProperty("isDocker", false)) {
|
||||
$command = array_merge(["docker", "exec", "-it", "db"], $command);
|
||||
$command = array_merge(["docker", "exec", "-it", "db"], $command);
|
||||
}
|
||||
|
||||
$process = proc_open($command, $descriptorSpec, $pipes, null, $env);
|
||||
@ -227,7 +233,7 @@ function handleDatabase(array $argv) {
|
||||
|
||||
printLine("Done!");
|
||||
} else {
|
||||
_exit("Usage: cli.php db <migrate|import|export> [options...]");
|
||||
_exit("Usage: cli.php db <migrate|import|export|shell> [options...]");
|
||||
}
|
||||
}
|
||||
|
||||
@ -537,7 +543,7 @@ function onTest($argv) {
|
||||
$requestedTests = array_filter(array_slice($argv, 2), function ($t) {
|
||||
return !startsWith($t, "-");
|
||||
});
|
||||
$verbose = in_array("-v", $requestedTests);
|
||||
$verbose = in_array("-v", $argv);
|
||||
|
||||
foreach ($files as $file) {
|
||||
include_once $file;
|
||||
|
2
core/.gitignore
vendored
2
core/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
TemplateCache
|
||||
External/cache
|
@ -101,7 +101,7 @@ namespace Api\News {
|
||||
$this->result["newsId"] = $sql->getLastInsertId();
|
||||
}
|
||||
|
||||
return true;
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,13 +2,14 @@
|
||||
|
||||
namespace Api;
|
||||
|
||||
use Api\Parameter\Parameter;
|
||||
use Driver\Logger\Logger;
|
||||
use Objects\User;
|
||||
use PhpMqtt\Client\MqttClient;
|
||||
|
||||
abstract class Request {
|
||||
|
||||
protected User $user;
|
||||
protected Logger $logger;
|
||||
protected array $params;
|
||||
protected string $lastError;
|
||||
protected array $result;
|
||||
@ -26,6 +27,7 @@ abstract class Request {
|
||||
|
||||
public function __construct(User $user, bool $externalCall = false, array $params = array()) {
|
||||
$this->user = $user;
|
||||
$this->logger = new Logger($this->getAPIName(), $this->user->getSQL());
|
||||
$this->defaultParams = $params;
|
||||
|
||||
$this->success = false;
|
||||
@ -41,6 +43,19 @@ abstract class Request {
|
||||
$this->csrfTokenRequired = true;
|
||||
}
|
||||
|
||||
public function getAPIName(): string {
|
||||
if (get_class($this) === Request::class) {
|
||||
return "API";
|
||||
}
|
||||
|
||||
$reflection = new \ReflectionClass($this);
|
||||
if ($reflection->getParentClass()->isAbstract() && $reflection->getParentClass()->isSubclassOf(Request::class)) {
|
||||
return $reflection->getParentClass()->getShortName() . "/" . $reflection->getShortName();
|
||||
} else {
|
||||
return $reflection->getShortName();
|
||||
}
|
||||
}
|
||||
|
||||
protected function forbidMethod($method) {
|
||||
if (($key = array_search($method, $this->allowedMethods)) !== false) {
|
||||
unset($this->allowedMethods[$key]);
|
||||
@ -225,6 +240,7 @@ abstract class Request {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->success = true;
|
||||
$success = $this->_execute();
|
||||
if ($this->success !== $success) {
|
||||
// _execute returns a different value then it set for $this->success
|
||||
|
@ -2,24 +2,20 @@
|
||||
|
||||
namespace Api {
|
||||
|
||||
use Api\Routes\GenerateCache;
|
||||
use Driver\SQL\Condition\Compare;
|
||||
use Objects\User;
|
||||
|
||||
abstract class RoutesAPI extends Request {
|
||||
|
||||
const ACTIONS = array("redirect_temporary", "redirect_permanently", "static", "dynamic");
|
||||
const ROUTER_CACHE_CLASS = "\\Cache\\RouterCache";
|
||||
|
||||
protected function formatRegex(string $input, bool $append) : string {
|
||||
$start = startsWith($input, "^");
|
||||
$end = endsWith($input, "$");
|
||||
if ($append) {
|
||||
if (!$start) $input = "^$input";
|
||||
if (!$end) $input = "$input$";
|
||||
} else {
|
||||
if ($start) $input = substr($input, 1);
|
||||
if ($end) $input = substr($input, 0, strlen($input)-1);
|
||||
}
|
||||
protected string $routerCachePath;
|
||||
|
||||
return $input;
|
||||
public function __construct(User $user, bool $externalCall, array $params) {
|
||||
parent::__construct($user, $externalCall, $params);
|
||||
$this->routerCachePath = getClassPath(self::ROUTER_CACHE_CLASS);
|
||||
}
|
||||
|
||||
protected function routeExists($uid): bool {
|
||||
@ -52,6 +48,14 @@ namespace Api {
|
||||
->execute();
|
||||
|
||||
$this->lastError = $sql->getLastError();
|
||||
$this->success = $this->success && $this->regenerateCache();
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
protected function regenerateCache(): bool {
|
||||
$req = new GenerateCache($this->user);
|
||||
$this->success = $req->execute();
|
||||
$this->lastError = $req->getLastError();
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
@ -62,94 +66,45 @@ namespace Api\Routes {
|
||||
use Api\Parameter\Parameter;
|
||||
use Api\Parameter\StringType;
|
||||
use Api\RoutesAPI;
|
||||
use Driver\SQL\Column\Column;
|
||||
use Driver\SQL\Condition\Compare;
|
||||
use Driver\SQL\Condition\CondBool;
|
||||
use Driver\SQL\Condition\CondRegex;
|
||||
use Objects\Router;
|
||||
use Objects\User;
|
||||
|
||||
class Fetch extends RoutesAPI {
|
||||
|
||||
public function __construct($user, $externalCall = false) {
|
||||
parent::__construct($user, $externalCall, array());
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$sql = $this->user->getSQL();
|
||||
|
||||
$res = $sql
|
||||
->select("uid", "request", "action", "target", "extra", "active")
|
||||
->from("Route")
|
||||
->orderBy("uid")
|
||||
->ascending()
|
||||
->execute();
|
||||
|
||||
$this->lastError = $sql->getLastError();
|
||||
$this->success = ($res !== FALSE);
|
||||
|
||||
if ($this->success) {
|
||||
$routes = array();
|
||||
foreach($res as $row) {
|
||||
$routes[] = array(
|
||||
"uid" => intval($row["uid"]),
|
||||
"request" => $this->formatRegex($row["request"], false),
|
||||
"action" => $row["action"],
|
||||
"target" => $row["target"],
|
||||
"extra" => $row["extra"] ?? "",
|
||||
"active" => intval($sql->parseBool($row["active"])),
|
||||
);
|
||||
}
|
||||
|
||||
$this->result["routes"] = $routes;
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class Find extends RoutesAPI {
|
||||
|
||||
public function __construct($user, $externalCall = false) {
|
||||
parent::__construct($user, $externalCall, array(
|
||||
'request' => new StringType('request', 128, true, '/')
|
||||
));
|
||||
|
||||
$this->isPublic = false;
|
||||
parent::__construct($user, $externalCall, array());
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$request = $this->getParam('request');
|
||||
if (!startsWith($request, '/')) {
|
||||
$request = "/$request";
|
||||
}
|
||||
|
||||
$sql = $this->user->getSQL();
|
||||
|
||||
$res = $sql
|
||||
->select("uid", "request", "action", "target", "extra")
|
||||
->select("uid", "request", "action", "target", "extra", "active", "exact")
|
||||
->from("Route")
|
||||
->where(new CondBool("active"))
|
||||
->where(new CondRegex($request, new Column("request")))
|
||||
->orderBy("uid")->ascending()
|
||||
->limit(1)
|
||||
->orderBy("uid")
|
||||
->ascending()
|
||||
->execute();
|
||||
|
||||
$this->lastError = $sql->getLastError();
|
||||
$this->success = ($res !== FALSE);
|
||||
|
||||
if ($this->success) {
|
||||
if (!empty($res)) {
|
||||
$row = $res[0];
|
||||
$this->result["route"] = array(
|
||||
"uid" => intval($row["uid"]),
|
||||
$routes = array();
|
||||
foreach ($res as $row) {
|
||||
$routes[] = array(
|
||||
"uid" => intval($row["uid"]),
|
||||
"request" => $row["request"],
|
||||
"action" => $row["action"],
|
||||
"target" => $row["target"],
|
||||
"extra" => $row["extra"]
|
||||
"action" => $row["action"],
|
||||
"target" => $row["target"],
|
||||
"extra" => $row["extra"] ?? "",
|
||||
"active" => intval($sql->parseBool($row["active"])),
|
||||
"exact" => intval($sql->parseBool($row["exact"])),
|
||||
);
|
||||
} else {
|
||||
$this->result["route"] = NULL;
|
||||
}
|
||||
|
||||
$this->result["routes"] = $routes;
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
@ -162,7 +117,7 @@ namespace Api\Routes {
|
||||
|
||||
public function __construct($user, $externalCall = false) {
|
||||
parent::__construct($user, $externalCall, array(
|
||||
'routes' => new Parameter('routes',Parameter::TYPE_ARRAY, false)
|
||||
'routes' => new Parameter('routes', Parameter::TYPE_ARRAY, false)
|
||||
));
|
||||
}
|
||||
|
||||
@ -179,15 +134,16 @@ namespace Api\Routes {
|
||||
|
||||
// INSERT new routes
|
||||
if ($this->success) {
|
||||
$stmt = $sql->insert("Route", array("request", "action", "target", "extra", "active"));
|
||||
$stmt = $sql->insert("Route", array("request", "action", "target", "extra", "active", "exact"));
|
||||
|
||||
foreach($this->routes as $route) {
|
||||
$stmt->addRow($route["request"], $route["action"], $route["target"], $route["extra"], $route["active"]);
|
||||
foreach ($this->routes as $route) {
|
||||
$stmt->addRow($route["request"], $route["action"], $route["target"], $route["extra"], $route["active"], $route["exact"]);
|
||||
}
|
||||
$this->success = ($stmt->execute() !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
}
|
||||
|
||||
$this->success = $this->success && $this->regenerateCache();
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
@ -195,25 +151,37 @@ namespace Api\Routes {
|
||||
|
||||
$this->routes = array();
|
||||
$keys = array(
|
||||
"request" => Parameter::TYPE_STRING,
|
||||
"request" => [Parameter::TYPE_STRING, Parameter::TYPE_INT],
|
||||
"action" => Parameter::TYPE_STRING,
|
||||
"target" => Parameter::TYPE_STRING,
|
||||
"extra" => Parameter::TYPE_STRING,
|
||||
"active" => Parameter::TYPE_BOOLEAN
|
||||
"extra" => Parameter::TYPE_STRING,
|
||||
"active" => Parameter::TYPE_BOOLEAN,
|
||||
"exact" => Parameter::TYPE_BOOLEAN,
|
||||
);
|
||||
|
||||
foreach($this->getParam("routes") as $index => $route) {
|
||||
foreach($keys as $key => $expectedType) {
|
||||
foreach ($this->getParam("routes") as $index => $route) {
|
||||
foreach ($keys as $key => $expectedType) {
|
||||
if (!array_key_exists($key, $route)) {
|
||||
return $this->createError("Route $index missing key: $key");
|
||||
}
|
||||
|
||||
$value = $route[$key];
|
||||
$type = Parameter::parseType($value);
|
||||
if ($type !== $expectedType) {
|
||||
$expectedTypeName = Parameter::names[$expectedType];
|
||||
if (!is_array($expectedType)) {
|
||||
$expectedType = [$expectedType];
|
||||
}
|
||||
|
||||
if (!in_array($type, $expectedType)) {
|
||||
if (count($expectedType) > 0) {
|
||||
$expectedTypeName = "expected: " . Parameter::names[$expectedType];
|
||||
} else {
|
||||
$expectedTypeName = "expected one of: " . implode(",", array_map(
|
||||
function ($type) {
|
||||
return Parameter::names[$type];
|
||||
}, $expectedType));
|
||||
}
|
||||
$gotTypeName = Parameter::names[$type];
|
||||
return $this->createError("Route $index has invalid value for key: $key, expected: $expectedTypeName, got: $gotTypeName");
|
||||
return $this->createError("Route $index has invalid value for key: $key, $expectedTypeName, got: $gotTypeName");
|
||||
}
|
||||
}
|
||||
|
||||
@ -222,16 +190,14 @@ namespace Api\Routes {
|
||||
return $this->createError("Invalid action: $action");
|
||||
}
|
||||
|
||||
if(empty($route["request"])) {
|
||||
if (empty($route["request"])) {
|
||||
return $this->createError("Request cannot be empty.");
|
||||
}
|
||||
|
||||
if(empty($route["target"])) {
|
||||
if (empty($route["target"])) {
|
||||
return $this->createError("Target cannot be empty.");
|
||||
}
|
||||
|
||||
// add start- and end pattern for database queries
|
||||
$route["request"] = $this->formatRegex($route["request"], true);
|
||||
$this->routes[] = $route;
|
||||
}
|
||||
|
||||
@ -246,14 +212,14 @@ namespace Api\Routes {
|
||||
"request" => new StringType("request", 128),
|
||||
"action" => new StringType("action"),
|
||||
"target" => new StringType("target", 128),
|
||||
"extra" => new StringType("extra", 64, true, ""),
|
||||
"extra" => new StringType("extra", 64, true, ""),
|
||||
));
|
||||
$this->isPublic = false;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
|
||||
$request = $this->formatRegex($this->getParam("request"), true);
|
||||
$request = $this->getParam("request");
|
||||
$action = $this->getParam("action");
|
||||
$target = $this->getParam("target");
|
||||
$extra = $this->getParam("extra");
|
||||
@ -268,6 +234,7 @@ namespace Api\Routes {
|
||||
->execute();
|
||||
|
||||
$this->lastError = $sql->getLastError();
|
||||
$this->success = $this->success && $this->regenerateCache();
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
@ -279,7 +246,7 @@ namespace Api\Routes {
|
||||
"request" => new StringType("request", 128),
|
||||
"action" => new StringType("action"),
|
||||
"target" => new StringType("target", 128),
|
||||
"extra" => new StringType("extra", 64, true, ""),
|
||||
"extra" => new StringType("extra", 64, true, ""),
|
||||
));
|
||||
$this->isPublic = false;
|
||||
}
|
||||
@ -291,7 +258,7 @@ namespace Api\Routes {
|
||||
return false;
|
||||
}
|
||||
|
||||
$request = $this->formatRegex($this->getParam("request"), true);
|
||||
$request = $this->getParam("request");
|
||||
$action = $this->getParam("action");
|
||||
$target = $this->getParam("target");
|
||||
$extra = $this->getParam("extra");
|
||||
@ -309,6 +276,7 @@ namespace Api\Routes {
|
||||
->execute();
|
||||
|
||||
$this->lastError = $sql->getLastError();
|
||||
$this->success = $this->success && $this->regenerateCache();
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
@ -334,6 +302,7 @@ namespace Api\Routes {
|
||||
->execute();
|
||||
|
||||
$this->lastError = $sql->getLastError();
|
||||
$this->success = $this->success && $this->regenerateCache();
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
@ -347,7 +316,6 @@ namespace Api\Routes {
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
|
||||
$uid = $this->getParam("uid");
|
||||
return $this->toggleRoute($uid, true);
|
||||
}
|
||||
@ -362,10 +330,71 @@ namespace Api\Routes {
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
|
||||
$uid = $this->getParam("uid");
|
||||
return $this->toggleRoute($uid, false);
|
||||
}
|
||||
}
|
||||
|
||||
class GenerateCache extends RoutesAPI {
|
||||
|
||||
private ?Router $router;
|
||||
|
||||
public function __construct(User $user, bool $externalCall = false) {
|
||||
parent::__construct($user, $externalCall, []);
|
||||
$this->isPublic = false;
|
||||
$this->router = null;
|
||||
}
|
||||
|
||||
protected function _execute(): bool {
|
||||
$sql = $this->user->getSQL();
|
||||
$res = $sql
|
||||
->select("uid", "request", "action", "target", "extra", "exact")
|
||||
->from("Route")
|
||||
->where(new CondBool("active"))
|
||||
->orderBy("uid")->ascending()
|
||||
->execute();
|
||||
|
||||
$this->success = $res !== false;
|
||||
$this->lastError = $sql->getLastError();
|
||||
if (!$this->success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->router = new Router($this->user);
|
||||
foreach ($res as $row) {
|
||||
$request = $row["request"];
|
||||
$target = $row["target"];
|
||||
$exact = $sql->parseBool($row["exact"]);
|
||||
switch ($row["action"]) {
|
||||
case "redirect_temporary":
|
||||
$this->router->addRoute(new Router\RedirectRoute($request, $exact, $target, 307));
|
||||
break;
|
||||
case "redirect_permanently":
|
||||
$this->router->addRoute(new Router\RedirectRoute($request, $exact, $target, 308));
|
||||
break;
|
||||
case "static":
|
||||
$this->router->addRoute(new Router\StaticFileRoute($request, $exact, $target));
|
||||
break;
|
||||
case "dynamic":
|
||||
$extra = json_decode($row["extra"]) ?? [];
|
||||
$this->router->addRoute(new Router\DocumentRoute($request, $exact, $target, ...$extra));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->success = $this->router->writeCache($this->routerCachePath);
|
||||
if (!$this->success) {
|
||||
return $this->createError("Error saving router cache file: " . $this->routerCachePath);
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public function getRouter(): ?Router {
|
||||
return $this->router;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -46,7 +46,7 @@ namespace Api\Template {
|
||||
}
|
||||
|
||||
$templateDir = WEBROOT . "/core/Templates/";
|
||||
$templateCache = WEBROOT . "/core/TemplateCache/";
|
||||
$templateCache = WEBROOT . "/core/Cache/Templates/";
|
||||
$path = realpath($templateDir . $templateFile);
|
||||
if (!startsWith($path, realpath($templateDir))) {
|
||||
return $this->createError("Template file not in template directory");
|
||||
|
@ -525,11 +525,13 @@ namespace Api\User {
|
||||
}
|
||||
|
||||
if (!$this->success) {
|
||||
$this->logger->error("Could not deliver email to=$email type=invite reason=" . $this->lastError);
|
||||
$this->lastError = "The invitation was created but the confirmation email could not be sent. " .
|
||||
"Please contact the server administration. Reason: " . $this->lastError;
|
||||
"Please contact the server administration. This issue has been automatically logged. Reason: " . $this->lastError;
|
||||
}
|
||||
}
|
||||
|
||||
$this->logger->info("Created new user with uid=$id");
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
@ -844,17 +846,20 @@ namespace Api\User {
|
||||
$this->success = $request->execute(array(
|
||||
"to" => $email,
|
||||
"subject" => "[$siteName] E-Mail Confirmation",
|
||||
"body" => $messageBody
|
||||
"body" => $messageBody,
|
||||
"async" => true,
|
||||
));
|
||||
$this->lastError = $request->getLastError();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->success) {
|
||||
$this->logger->error("Could not deliver email to=$email type=register reason=" . $this->lastError);
|
||||
$this->lastError = "Your account was registered but the confirmation email could not be sent. " .
|
||||
"Please contact the server administration. Reason: " . $this->lastError;
|
||||
"Please contact the server administration. This issue has been automatically logged. Reason: " . $this->lastError;
|
||||
}
|
||||
|
||||
$this->logger->info("Registered new user with uid=" . $this->userId);
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
@ -1124,6 +1129,7 @@ namespace Api\User {
|
||||
"gpgFingerprint" => $gpgFingerprint
|
||||
));
|
||||
$this->lastError = $request->getLastError();
|
||||
$this->logger->info("Requested password reset for user uid=" . $user["uid"] . " by ip_address=" . $_SERVER["REMOTE_ADDR"]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1304,6 +1310,7 @@ namespace Api\User {
|
||||
} else if (!$this->updateUser($result["user"]["uid"], $password)) {
|
||||
return false;
|
||||
} else {
|
||||
$this->logger->info("Issued password reset for user uid=" . $result["user"]["uid"]);
|
||||
$this->invalidateToken($token);
|
||||
return true;
|
||||
}
|
||||
@ -1475,11 +1482,12 @@ namespace Api\User {
|
||||
$settings = $this->user->getConfiguration()->getSettings();
|
||||
$baseUrl = htmlspecialchars($settings->getBaseUrl());
|
||||
$token = htmlspecialchars(urlencode($token));
|
||||
$url = "$baseUrl/settings?confirmGPG&token=$token";
|
||||
$mailBody = "Hello $name,<br><br>" .
|
||||
"you imported a GPG public key for end-to-end encrypted mail communication. " .
|
||||
"To confirm the key and verify, you own the corresponding private key, please click on the following link. " .
|
||||
"The link is active for one hour.<br><br>" .
|
||||
"<a href='$baseUrl/confirmGPG?token=$token'>$baseUrl/settings?confirmGPG&token=$token</a><br>
|
||||
"<a href='$url'>$url</a><br>
|
||||
Best Regards<br>
|
||||
ilum:e Security Lab";
|
||||
|
||||
|
2
core/Cache/.gitignore
vendored
Normal file
2
core/Cache/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitkeep
|
@ -31,12 +31,12 @@ class Configuration {
|
||||
return $this->settings;
|
||||
}
|
||||
|
||||
public function create(string $className, $data) {
|
||||
public static function create(string $className, $data) {
|
||||
$path = getClassPath("\\Configuration\\$className");
|
||||
|
||||
if ($data) {
|
||||
if (is_string($data)) {
|
||||
$key = addslashes($data);
|
||||
$key = var_export($data, true);
|
||||
$code = intendCode(
|
||||
"<?php
|
||||
|
||||
@ -45,23 +45,23 @@ class Configuration {
|
||||
class $className extends KeyData {
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct('$key');
|
||||
parent::__construct($key);
|
||||
}
|
||||
|
||||
}", false
|
||||
);
|
||||
} else if ($data instanceof ConnectionData) {
|
||||
$superClass = get_class($data);
|
||||
$host = addslashes($data->getHost());
|
||||
$port = $data->getPort();
|
||||
$login = addslashes($data->getLogin());
|
||||
$password = addslashes($data->getPassword());
|
||||
$host = var_export($data->getHost(), true);
|
||||
$port = var_export($data->getPort(), true);
|
||||
$login = var_export($data->getLogin(), true);
|
||||
$password = var_export($data->getPassword(), true);
|
||||
|
||||
$properties = "";
|
||||
foreach ($data->getProperties() as $key => $val) {
|
||||
$key = addslashes($key);
|
||||
$val = is_string($val) ? "'" . addslashes($val) . "'" : $val;
|
||||
$properties .= "\n\$this->setProperty('$key', $val);";
|
||||
$key = var_export($key, true);
|
||||
$val = var_export($val, true);
|
||||
$properties .= "\n\$this->setProperty($key, $val);";
|
||||
}
|
||||
|
||||
$code = intendCode(
|
||||
@ -72,7 +72,7 @@ class Configuration {
|
||||
class $className extends \\$superClass {
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct('$host', $port, '$login', '$password');$properties
|
||||
parent::__construct($host, $port, $login, $password);$properties
|
||||
}
|
||||
}", false
|
||||
);
|
||||
@ -94,4 +94,8 @@ class Configuration {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function setDatabase(ConnectionData $connectionData) {
|
||||
$this->database = $connectionData;
|
||||
}
|
||||
}
|
@ -151,18 +151,19 @@ class CreateDatabase extends DatabaseScript {
|
||||
->addString("target", 128)
|
||||
->addString("extra", 64, true)
|
||||
->addBool("active", true)
|
||||
->addBool("exact", true)
|
||||
->primaryKey("uid")
|
||||
->unique("request");
|
||||
|
||||
$queries[] = $sql->insert("Route", array("request", "action", "target", "extra"))
|
||||
->addRow("^/admin(/.*)?$", "dynamic", "\\Documents\\Admin", NULL)
|
||||
->addRow("^/register/?$", "dynamic", "\\Documents\\Account", "account/register.twig")
|
||||
->addRow("^/confirmEmail/?$", "dynamic", "\\Documents\\Account", "account/confirm_email.twig")
|
||||
->addRow("^/acceptInvite/?$", "dynamic", "\\Documents\\Account", "account/accept_invite.twig")
|
||||
->addRow("^/resetPassword/?$", "dynamic", "\\Documents\\Account", "account/reset_password.twig")
|
||||
->addRow("^/login/?$", "dynamic", "\\Documents\\Account", "account/login.twig")
|
||||
->addRow("^/resendConfirmEmail/?$", "dynamic", "\\Documents\\Account", "account/resend_confirm_email.twig")
|
||||
->addRow("^/$", "static", "/static/welcome.html", NULL);
|
||||
$queries[] = $sql->insert("Route", ["request", "action", "target", "extra", "exact"])
|
||||
->addRow("/admin", "dynamic", "\\Documents\\Admin", NULL, false)
|
||||
->addRow("/register", "dynamic", "\\Documents\\Account", json_encode(["account/register.twig"]), true)
|
||||
->addRow("/confirmEmail", "dynamic", "\\Documents\\Account", json_encode(["account/confirm_email.twig"]), true)
|
||||
->addRow("/acceptInvite", "dynamic", "\\Documents\\Account", json_encode(["account/accept_invite.twig"]), true)
|
||||
->addRow("/resetPassword", "dynamic", "\\Documents\\Account", json_encode(["account/reset_password.twig"]), true)
|
||||
->addRow("/login", "dynamic", "\\Documents\\Account", json_encode(["account/login.twig"]), true)
|
||||
->addRow("/resendConfirmEmail", "dynamic", "\\Documents\\Account", json_encode(["account/resend_confirm_email.twig"]), true)
|
||||
->addRow("/", "static", "/static/welcome.html", NULL, true);
|
||||
|
||||
$queries[] = $sql->createTable("Settings")
|
||||
->addString("name", 32)
|
||||
|
24
core/Configuration/Patch/SystemLog_2022_03_30.class.php
Normal file
24
core/Configuration/Patch/SystemLog_2022_03_30.class.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Configuration\Patch;
|
||||
|
||||
use Configuration\DatabaseScript;
|
||||
use Driver\SQL\SQL;
|
||||
|
||||
class SystemLog_2022_03_30 extends DatabaseScript {
|
||||
|
||||
public static function createQueries(SQL $sql): array {
|
||||
return [
|
||||
$sql->createTable("SystemLog")
|
||||
->onlyIfNotExists()
|
||||
->addSerial("id")
|
||||
->addDateTime("timestamp", false, $sql->now())
|
||||
->addString("message")
|
||||
->addString("module", 64, false, "global")
|
||||
->addEnum("severity", ["debug", "info", "warning", "error", "severe"])
|
||||
->primaryKey("id"),
|
||||
$sql->insert("ApiPermission", ["method", "groups", "description"])
|
||||
->addRow("Logs/get", [USER_GROUP_ADMIN], "Allows users to fetch system logs")
|
||||
];
|
||||
}
|
||||
}
|
@ -8,8 +8,8 @@ use Objects\User;
|
||||
|
||||
|
||||
class Account extends TemplateDocument {
|
||||
public function __construct(User $user, ?string $template) {
|
||||
parent::__construct($user, $template);
|
||||
public function __construct(User $user, string $templateName) {
|
||||
parent::__construct($user, $templateName);
|
||||
$this->enableCSP();
|
||||
}
|
||||
|
||||
|
@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Documents;
|
||||
|
||||
use Elements\TemplateDocument;
|
||||
use Objects\User;
|
||||
|
||||
class Document404 extends TemplateDocument {
|
||||
|
||||
public function __construct(User $user) {
|
||||
parent::__construct($user, "404.twig");
|
||||
}
|
||||
|
||||
public function loadParameters() {
|
||||
parent::loadParameters();
|
||||
http_response_code(404);
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@ namespace Documents {
|
||||
|
||||
namespace Documents\Install {
|
||||
|
||||
use Configuration\Configuration;
|
||||
use Configuration\CreateDatabase;
|
||||
use Driver\SQL\Query\Commit;
|
||||
use Driver\SQL\Query\RollBack;
|
||||
@ -233,7 +234,7 @@ namespace Documents\Install {
|
||||
$username = posix_getpwuid($userId)['name'];
|
||||
$failedRequirements[] = sprintf("<b>%s</b> is not owned by current user: $username ($userId). " .
|
||||
"Try running <b>chown -R $userId %s</b> or give the required directories write permissions: " .
|
||||
"<b>core/Configuration</b>, <b>core/TemplateCache</b>, <b>core/External</b>",
|
||||
"<b>core/Configuration</b>, <b>core/Cache</b>, <b>core/External</b>",
|
||||
WEBROOT, WEBROOT);
|
||||
$success = false;
|
||||
}
|
||||
@ -363,10 +364,23 @@ namespace Documents\Install {
|
||||
}
|
||||
}
|
||||
|
||||
$config = $this->getDocument()->getUser()->getConfiguration();
|
||||
if (!$config->create("Database", $connectionData)) {
|
||||
$user = $this->getDocument()->getUser();
|
||||
$config = $user->getConfiguration();
|
||||
if (Configuration::create("Database", $connectionData) === false) {
|
||||
$success = false;
|
||||
$msg = "Unable to write file";
|
||||
$msg = "Unable to write database file";
|
||||
} else {
|
||||
$config->setDatabase($connectionData);
|
||||
if (!$user->connectDB()) {
|
||||
$success = false;
|
||||
$msg = "Unable to verify database connection after installation";
|
||||
} else {
|
||||
$req = new \Api\Routes\GenerateCache($user);
|
||||
if (!$req->execute()) {
|
||||
$success = false;
|
||||
$msg = "Unable to write route file: " . $req->getLastError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sql->close();
|
||||
|
94
core/Driver/Logger/Logger.class.php
Normal file
94
core/Driver/Logger/Logger.class.php
Normal file
@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Driver\Logger;
|
||||
|
||||
use Driver\SQL\SQL;
|
||||
|
||||
class Logger {
|
||||
|
||||
public const LOG_FILE_DATE_FORMAT = "Y-m-d_H-i-s_v";
|
||||
public const LOG_LEVELS = [
|
||||
0 => "debug",
|
||||
1 => "info",
|
||||
2 => "warning",
|
||||
3 => "error",
|
||||
4 => "severe"
|
||||
];
|
||||
|
||||
public static Logger $INSTANCE;
|
||||
|
||||
private ?SQL $sql;
|
||||
private string $module;
|
||||
|
||||
public function __construct(string $module = "Unknown", ?SQL $sql = null) {
|
||||
$this->module = $module;
|
||||
$this->sql = $sql;
|
||||
}
|
||||
|
||||
protected function getStackTrace(int $pop = 2): string {
|
||||
$debugTrace = debug_backtrace();
|
||||
if ($pop > 0) {
|
||||
array_splice($debugTrace, 0, $pop);
|
||||
}
|
||||
|
||||
return implode("\n", array_map(function ($trace) {
|
||||
return $trace["file"] . "#" . $trace["line"] . ": " . $trace["function"] . "()";
|
||||
}, $debugTrace));
|
||||
}
|
||||
|
||||
public function log(string $message, string $severity, bool $appendStackTrace = true) {
|
||||
|
||||
if ($appendStackTrace) {
|
||||
$message .= "\n" . $this->getStackTrace();
|
||||
}
|
||||
|
||||
if ($this->sql !== null && $this->sql->isConnected()) {
|
||||
$success = $this->sql->insert("SystemLog", ["module", "message", "severity"])
|
||||
->addRow($this->module, $message, $severity)
|
||||
->execute();
|
||||
if ($success !== false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// database logging failed, try to log to file
|
||||
$module = preg_replace("/[^a-zA-Z0-9-]/", "-", $this->module);
|
||||
$date = (\DateTime::createFromFormat('U.u', microtime(true)))->format(self::LOG_FILE_DATE_FORMAT);
|
||||
$logFile = implode("_", [$module, $severity, $date]) . ".log";
|
||||
$logPath = implode(DIRECTORY_SEPARATOR, [WEBROOT, "core", "Logs", $logFile]);
|
||||
@file_put_contents($logPath, $message);
|
||||
}
|
||||
|
||||
public function error(string $message): string {
|
||||
$this->log($message, "error");
|
||||
return $message;
|
||||
}
|
||||
|
||||
public function severe(string $message): string {
|
||||
$this->log($message, "severe");
|
||||
return $message;
|
||||
}
|
||||
|
||||
public function warning(string $message): string {
|
||||
$this->log($message, "warning", false);
|
||||
return $message;
|
||||
}
|
||||
|
||||
public function info(string $message): string {
|
||||
$this->log($message, "info", false);
|
||||
return $message;
|
||||
}
|
||||
|
||||
public function debug(string $message): string {
|
||||
$this->log($message, "debug");
|
||||
return $message;
|
||||
}
|
||||
|
||||
public static function instance(): Logger {
|
||||
if (self::$INSTANCE === null) {
|
||||
self::$INSTANCE = new Logger("Global");
|
||||
}
|
||||
|
||||
return self::$INSTANCE;
|
||||
}
|
||||
}
|
@ -59,7 +59,7 @@ class MySQL extends SQL {
|
||||
);
|
||||
|
||||
if (mysqli_connect_errno()) {
|
||||
$this->lastError = "Failed to connect to MySQL: " . mysqli_connect_error();
|
||||
$this->lastError = $this->logger->severe("Failed to connect to MySQL: " . mysqli_connect_error());
|
||||
$this->connection = NULL;
|
||||
return false;
|
||||
}
|
||||
@ -164,20 +164,20 @@ class MySQL extends SQL {
|
||||
}
|
||||
$success = true;
|
||||
} else {
|
||||
$this->lastError = "PreparedStatement::get_result failed: $stmt->error ($stmt->errno)";
|
||||
$this->lastError = $this->logger->error("PreparedStatement::get_result failed: $stmt->error ($stmt->errno)");
|
||||
}
|
||||
} else {
|
||||
$success = true;
|
||||
}
|
||||
} else {
|
||||
$this->lastError = "PreparedStatement::execute failed: $stmt->error ($stmt->errno)";
|
||||
$this->lastError = $this->logger->error("PreparedStatement::execute failed: $stmt->error ($stmt->errno)");
|
||||
}
|
||||
} else {
|
||||
$this->lastError = "PreparedStatement::prepare failed: $stmt->error ($stmt->errno)";
|
||||
$this->lastError = $this->logger->error("PreparedStatement::prepare failed: $stmt->error ($stmt->errno)");
|
||||
}
|
||||
}
|
||||
} catch (\mysqli_sql_exception $exception) {
|
||||
$this->lastError = "MySQL::execute failed: $stmt->error ($stmt->errno)";
|
||||
$this->lastError = $this->logger->error("MySQL::execute failed: $stmt->error ($stmt->errno)");
|
||||
} finally {
|
||||
if ($res !== null && !is_bool($res)) {
|
||||
$res->close();
|
||||
@ -214,7 +214,7 @@ class MySQL extends SQL {
|
||||
return " ON DUPLICATE KEY UPDATE " . implode(",", $updateValues);
|
||||
} else {
|
||||
$strategyClass = get_class($strategy);
|
||||
$this->lastError = "ON DUPLICATE Strategy $strategyClass is not supported yet.";
|
||||
$this->lastError = $this->logger->error("ON DUPLICATE Strategy $strategyClass is not supported yet.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -243,7 +243,7 @@ class MySQL extends SQL {
|
||||
} else if($column instanceof JsonColumn) {
|
||||
return "LONGTEXT"; # some maria db setups don't allow JSON here…
|
||||
} else {
|
||||
$this->lastError = "Unsupported Column Type: " . get_class($column);
|
||||
$this->lastError = $this->logger->error("Unsupported Column Type: " . get_class($column));
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
@ -251,17 +251,17 @@ class MySQL extends SQL {
|
||||
public function getColumnDefinition(Column $column): ?string {
|
||||
$columnName = $this->columnName($column->getName());
|
||||
$defaultValue = $column->getDefaultValue();
|
||||
$type = $this->getColumnType($column);
|
||||
if (!$type) {
|
||||
if ($column instanceof EnumColumn) {
|
||||
$values = array();
|
||||
foreach($column->getValues() as $value) {
|
||||
$values[] = $this->getValueDefinition($value);
|
||||
}
|
||||
if ($column instanceof EnumColumn) { // check this, shouldn't it be in getColumnType?
|
||||
$values = array();
|
||||
foreach($column->getValues() as $value) {
|
||||
$values[] = $this->getValueDefinition($value);
|
||||
}
|
||||
|
||||
$values = implode(",", $values);
|
||||
$type = "ENUM($values)";
|
||||
} else {
|
||||
$values = implode(",", $values);
|
||||
$type = "ENUM($values)";
|
||||
} else {
|
||||
$type = $this->getColumnType($column);
|
||||
if (!$type) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -393,7 +393,7 @@ class MySQL extends SQL {
|
||||
if ($param instanceof Column) {
|
||||
$paramDefs[] = $this->getParameterDefinition($param);
|
||||
} else {
|
||||
$this->setLastError("PROCEDURE parameter type " . gettype($returns) . " is not implemented yet");
|
||||
$this->lastError = $this->logger->error("PROCEDURE parameter type " . gettype($returns) . " is not implemented yet");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -402,7 +402,7 @@ class MySQL extends SQL {
|
||||
if ($returns instanceof Column) {
|
||||
$paramDefs[] = $this->getParameterDefinition($returns, true);
|
||||
} else if (!($returns instanceof Trigger)) { // mysql does not need to return triggers here
|
||||
$this->setLastError("PROCEDURE RETURN type " . gettype($returns) . " is not implemented yet");
|
||||
$this->lastError = $this->logger->error("PROCEDURE RETURN type " . gettype($returns) . " is not implemented yet");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ class PostgreSQL extends SQL {
|
||||
|
||||
$this->connection = @pg_connect(implode(" ", $connectionString), PGSQL_CONNECT_FORCE_NEW);
|
||||
if (!$this->connection) {
|
||||
$this->lastError = "Failed to connect to Database";
|
||||
$this->lastError = $this->logger->severe("Failed to connect to Database");
|
||||
$this->connection = NULL;
|
||||
return false;
|
||||
}
|
||||
@ -170,7 +170,7 @@ class PostgreSQL extends SQL {
|
||||
return " ON CONFLICT ($conflictingColumns) DO UPDATE SET $updateValues";
|
||||
} else {
|
||||
$strategyClass = get_class($strategy);
|
||||
$this->lastError = "ON DUPLICATE Strategy $strategyClass is not supported yet.";
|
||||
$this->lastError = $this->logger->error("ON DUPLICATE Strategy $strategyClass is not supported yet.");
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
@ -233,7 +233,7 @@ class PostgreSQL extends SQL {
|
||||
} else if($column instanceof JsonColumn) {
|
||||
return "JSON";
|
||||
} else {
|
||||
$this->lastError = "Unsupported Column Type: " . get_class($column);
|
||||
$this->lastError = $this->logger->error("Unsupported Column Type: " . get_class($column));
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
@ -317,8 +317,7 @@ class PostgreSQL extends SQL {
|
||||
if ($col instanceof KeyWord) {
|
||||
return $col->getValue();
|
||||
} elseif(is_array($col)) {
|
||||
$columns = array();
|
||||
foreach($col as $c) $columns[] = $this->columnName($c);
|
||||
$columns = array_map(function ($c) { return $this->columnName($c); }, $col);
|
||||
return implode(",", $columns);
|
||||
} else {
|
||||
if (($index = strrpos($col, ".")) !== FALSE) {
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace Driver\SQL;
|
||||
|
||||
use Driver\Logger\Logger;
|
||||
use Driver\SQL\Column\Column;
|
||||
use Driver\SQL\Condition\Compare;
|
||||
use Driver\SQL\Condition\CondAnd;
|
||||
@ -40,6 +41,7 @@ use Objects\ConnectionData;
|
||||
|
||||
abstract class SQL {
|
||||
|
||||
protected Logger $logger;
|
||||
protected string $lastError;
|
||||
protected $connection;
|
||||
protected ConnectionData $connectionData;
|
||||
@ -50,6 +52,7 @@ abstract class SQL {
|
||||
$this->lastError = 'Unknown Error';
|
||||
$this->connectionData = $connectionData;
|
||||
$this->lastInsertId = 0;
|
||||
$this->logger = new Logger(getClassName($this), $this);
|
||||
}
|
||||
|
||||
public function isConnected(): bool {
|
||||
@ -168,7 +171,7 @@ abstract class SQL {
|
||||
|
||||
return $code;
|
||||
} else {
|
||||
$this->lastError = "Unsupported constraint type: " . get_class($constraint);
|
||||
$this->lastError = $this->logger->error("Unsupported constraint type: " . get_class($constraint));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -200,7 +203,7 @@ abstract class SQL {
|
||||
} else if ($value === null) {
|
||||
return "NULL";
|
||||
} else {
|
||||
$this->lastError = "Cannot create unsafe value of type: " . gettype($value);
|
||||
$this->lastError = $this->logger->error("Cannot create unsafe value of type: " . gettype($value));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -290,7 +293,7 @@ abstract class SQL {
|
||||
} else if($haystack instanceof Select) {
|
||||
$values = $haystack->build($params);
|
||||
} else {
|
||||
$this->lastError = "Unsupported in-expression value: " . get_class($condition);
|
||||
$this->lastError = $this->logger->error("Unsupported in-expression value: " . get_class($condition));
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -322,7 +325,7 @@ abstract class SQL {
|
||||
} else if ($condition instanceof Exists) {
|
||||
return "EXISTS(" .$condition->getSubQuery()->build($params) . ")";
|
||||
} else {
|
||||
$this->lastError = "Unsupported condition type: " . gettype($condition);
|
||||
$this->lastError = $this->logger->error("Unsupported condition type: " . gettype($condition));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -345,7 +348,7 @@ abstract class SQL {
|
||||
$alias = $this->columnName($exp->getAlias());
|
||||
return "SUM($value) AS $alias";
|
||||
} else {
|
||||
$this->lastError = "Unsupported expression type: " . get_class($exp);
|
||||
$this->lastError = $this->logger->error("Unsupported expression type: " . get_class($exp));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -370,6 +373,7 @@ abstract class SQL {
|
||||
} else if ($type === "postgres") {
|
||||
$sql = new PostgreSQL($connectionData);
|
||||
} else {
|
||||
Logger::instance()->error("Unknown database type: $type");
|
||||
return "Unknown database type";
|
||||
}
|
||||
|
||||
|
@ -48,7 +48,7 @@ abstract class Document {
|
||||
$this->cspWhitelist[] = $this->domain . $path;
|
||||
}
|
||||
|
||||
public function getCode(): string {
|
||||
public function getCode(array $params = []): string {
|
||||
if ($this->databaseRequired) {
|
||||
$sql = $this->user->getSQL();
|
||||
if (is_null($sql)) {
|
||||
|
@ -17,14 +17,14 @@ class TemplateDocument extends Document {
|
||||
private FilesystemLoader $twigLoader;
|
||||
protected string $title;
|
||||
|
||||
public function __construct(User $user, string $templateName, array $initialParameters = []) {
|
||||
public function __construct(User $user, string $templateName, array $params = []) {
|
||||
parent::__construct($user);
|
||||
$this->title = "";
|
||||
$this->templateName = $templateName;
|
||||
$this->parameters = $initialParameters;
|
||||
$this->parameters = $params;
|
||||
$this->twigLoader = new FilesystemLoader(WEBROOT . '/core/Templates');
|
||||
$this->twigEnvironment = new Environment($this->twigLoader, [
|
||||
'cache' => WEBROOT . '/core/TemplateCache',
|
||||
'cache' => WEBROOT . '/core/Cache/Templates/',
|
||||
'auto_reload' => true
|
||||
]);
|
||||
}
|
||||
@ -37,8 +37,8 @@ class TemplateDocument extends Document {
|
||||
|
||||
}
|
||||
|
||||
public function getCode(): string {
|
||||
parent::getCode();
|
||||
public function getCode(array $params = []): string {
|
||||
parent::getCode($params);
|
||||
$this->loadParameters();
|
||||
return $this->renderTemplate($this->templateName, $this->parameters);
|
||||
}
|
||||
|
3
core/External/.gitignore
vendored
3
core/External/.gitignore
vendored
@ -1 +1,2 @@
|
||||
vendor/
|
||||
vendor/
|
||||
cache/
|
390
core/Objects/Router.class.php
Normal file
390
core/Objects/Router.class.php
Normal file
@ -0,0 +1,390 @@
|
||||
<?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()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -30,7 +30,7 @@ class User extends ApiObject {
|
||||
public function __construct($configuration) {
|
||||
$this->configuration = $configuration;
|
||||
$this->reset();
|
||||
$this->connectDb();
|
||||
$this->connectDB();
|
||||
|
||||
if (!is_cli()) {
|
||||
@session_start();
|
||||
@ -45,17 +45,20 @@ class User extends ApiObject {
|
||||
}
|
||||
}
|
||||
|
||||
private function connectDb() {
|
||||
public function connectDB(): bool {
|
||||
$databaseConf = $this->configuration->getDatabase();
|
||||
if($databaseConf) {
|
||||
if ($databaseConf) {
|
||||
$this->sql = SQL::createConnection($databaseConf);
|
||||
if ($this->sql->isConnected()) {
|
||||
$settings = $this->configuration->getSettings();
|
||||
$settings->loadFromDatabase($this);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
$this->sql = null;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getId(): int { return $this->uid; }
|
||||
|
@ -1,4 +0,0 @@
|
||||
{% extends "base.twig" %}
|
||||
{% block body %}
|
||||
<b>Not found</b>
|
||||
{% endblock %}
|
32
core/Templates/error_document.twig
Normal file
32
core/Templates/error_document.twig
Normal file
@ -0,0 +1,32 @@
|
||||
{% if var is null %}
|
||||
{% set site = {'title': "#{status_code} - #{status_description}" } %}
|
||||
{% else %}
|
||||
{% set site = site|merge({'title': "#{status_code} - #{status_description}"}) %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% extends "base.twig" %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css" type="text/css">
|
||||
<script type="text/javascript" src="/js/bootstrap.bundle.min.js"></script>
|
||||
<link rel="stylesheet" href="/css/fontawesome.min.css" type="text/css">
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="container mt-5">
|
||||
<div class="row">
|
||||
<div class="col-lg-9 col-12 mx-auto">
|
||||
<div class="jumbotron">
|
||||
<h1>{{ site.title }}!</h1>
|
||||
<hr class="my-4" />
|
||||
<p>
|
||||
Something went wrong or the site you wanted to visit does not exist anymore. <br />
|
||||
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>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -19,4 +19,45 @@ function GroupName($index) {
|
||||
);
|
||||
|
||||
return ($groupNames[$index] ?? "Unknown Group");
|
||||
}
|
||||
}
|
||||
|
||||
// adapted from https://www.php.net/manual/en/function.http-response-code.php
|
||||
const HTTP_STATUS_DESCRIPTIONS = [
|
||||
100 => 'Continue',
|
||||
101 => 'Switching Protocols',
|
||||
200 => 'OK',
|
||||
201 => 'Created',
|
||||
202 => 'Accepted',
|
||||
203 => 'Non-Authoritative Information',
|
||||
204 => 'No Content',
|
||||
205 => 'Reset Content',
|
||||
206 => 'Partial Content',
|
||||
300 => 'Multiple Choices',
|
||||
301 => 'Moved Permanently',
|
||||
302 => 'Moved Temporarily',
|
||||
303 => 'See Other',
|
||||
304 => 'Not Modified',
|
||||
305 => 'Use Proxy',
|
||||
400 => 'Bad Request',
|
||||
401 => 'Unauthorized',
|
||||
402 => 'Payment Required',
|
||||
403 => 'Forbidden',
|
||||
404 => 'Not Found',
|
||||
405 => 'Method Not Allowed',
|
||||
406 => 'Not Acceptable',
|
||||
407 => 'Proxy Authentication Required',
|
||||
408 => 'Request Time-out',
|
||||
409 => 'Conflict',
|
||||
410 => 'Gone',
|
||||
411 => 'Length Required',
|
||||
412 => 'Precondition Failed',
|
||||
413 => 'Request Entity Too Large',
|
||||
414 => 'Request-URI Too Large',
|
||||
415 => 'Unsupported Media Type',
|
||||
500 => 'Internal Server Error',
|
||||
501 => 'Not Implemented',
|
||||
502 => 'Bad Gateway',
|
||||
503 => 'Service Unavailable',
|
||||
504 => 'Gateway Time-out',
|
||||
505 => 'HTTP Version not supported',
|
||||
];
|
@ -5,7 +5,7 @@ if (is_file($autoLoad)) {
|
||||
require_once $autoLoad;
|
||||
}
|
||||
|
||||
define("WEBBASE_VERSION", "1.4.5");
|
||||
define("WEBBASE_VERSION", "1.5.0");
|
||||
|
||||
spl_autoload_extensions(".php");
|
||||
spl_autoload_register(function($class) {
|
||||
@ -215,6 +215,15 @@ function getClassPath($class, string $suffix = ".class"): string {
|
||||
return "core/$path$suffix.php";
|
||||
}
|
||||
|
||||
function getClassName($class, bool $short = true): string {
|
||||
$reflection = new \ReflectionClass($class);
|
||||
if ($short) {
|
||||
return $reflection->getShortName();
|
||||
} else {
|
||||
return $reflection->getName();
|
||||
}
|
||||
}
|
||||
|
||||
function createError($msg) {
|
||||
return json_encode(array("success" => false, "msg" => $msg));
|
||||
}
|
||||
|
91
index.php
91
index.php
@ -14,8 +14,7 @@ if (is_file("MAINTENANCE") && !in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '
|
||||
|
||||
use Api\Request;
|
||||
use Configuration\Configuration;
|
||||
use Documents\Document404;
|
||||
use Elements\Document;
|
||||
use Objects\Router;
|
||||
|
||||
if (!is_readable(getClassPath(Configuration::class))) {
|
||||
header("Content-Type: application/json");
|
||||
@ -28,6 +27,8 @@ $sql = $user->getSQL();
|
||||
$settings = $config->getSettings();
|
||||
$installation = !$sql || ($sql->isConnected() && !$settings->isInstalled());
|
||||
|
||||
// API routes, prefix: /api/
|
||||
// TODO: move this to Router?
|
||||
if (isset($_GET["api"]) && is_string($_GET["api"])) {
|
||||
$isApiResponse = true;
|
||||
if ($installation) {
|
||||
@ -81,18 +82,10 @@ if (isset($_GET["api"]) && is_string($_GET["api"])) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// all other routes
|
||||
$requestedUri = $_GET["site"] ?? $_SERVER["REQUEST_URI"];
|
||||
if (($index = strpos($requestedUri, "?")) !== false) {
|
||||
$requestedUri = substr($requestedUri, 0, $index);
|
||||
}
|
||||
|
||||
if (($index = strpos($requestedUri, "#")) !== false) {
|
||||
$requestedUri = substr($requestedUri, 0, $index);
|
||||
}
|
||||
|
||||
if (startsWith($requestedUri, "/")) {
|
||||
$requestedUri = substr($requestedUri, 1);
|
||||
}
|
||||
$requestedUri = Router::cleanURL($requestedUri);
|
||||
|
||||
if ($installation) {
|
||||
if ($requestedUri !== "" && $requestedUri !== "index.php") {
|
||||
@ -104,61 +97,31 @@ if (isset($_GET["api"]) && is_string($_GET["api"])) {
|
||||
}
|
||||
} else {
|
||||
|
||||
$req = new \Api\Routes\Find($user);
|
||||
$success = $req->execute(array("request" => $requestedUri));
|
||||
$response = "";
|
||||
if (!$success) {
|
||||
http_response_code(500);
|
||||
$response = "Unable to find route: " . $req->getLastError();
|
||||
} else {
|
||||
$route = $req->getResult()["route"];
|
||||
if (is_null($route)) {
|
||||
$response = (new Document404($user))->getCode();
|
||||
} else {
|
||||
$target = trim(explode("\n", $route["target"])[0]);
|
||||
$extra = $route["extra"] ?? "";
|
||||
$router = null;
|
||||
|
||||
$pattern = str_replace("/","\\/", $route["request"]);
|
||||
$pattern = "/$pattern/i";
|
||||
if (!startsWith($requestedUri, '/')) {
|
||||
$requestedUri = "/$requestedUri";
|
||||
}
|
||||
|
||||
@preg_match("$pattern", $requestedUri, $match);
|
||||
if (is_array($match) && !empty($match)) {
|
||||
foreach($match as $index => $value) {
|
||||
$target = str_replace("$$index", $value, $target);
|
||||
$extra = str_replace("$$index", $value, $extra);
|
||||
}
|
||||
}
|
||||
|
||||
switch ($route["action"]) {
|
||||
case "redirect_temporary":
|
||||
http_response_code(307);
|
||||
header("Location: $target");
|
||||
break;
|
||||
case "redirect_permanently":
|
||||
http_response_code(308);
|
||||
header("Location: $target");
|
||||
break;
|
||||
case "static":
|
||||
$currentDir = dirname(__FILE__);
|
||||
$response = serveStatic($currentDir, $target);
|
||||
break;
|
||||
case "dynamic":
|
||||
$file = getClassPath($target);
|
||||
if (!file_exists($file) || !is_subclass_of($target, Document::class)) {
|
||||
$document = new Document404($user, $extra);
|
||||
} else {
|
||||
$document = new $target($user, $extra);
|
||||
}
|
||||
|
||||
$response = $document->getCode();
|
||||
break;
|
||||
}
|
||||
$routerCacheClass = '\Cache\RouterCache';
|
||||
$routerCachePath = getClassPath($routerCacheClass);
|
||||
if (is_file($routerCachePath)) {
|
||||
@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);
|
||||
}
|
||||
|
||||
$user->processVisit();
|
||||
}
|
||||
}
|
||||
|
2
js/admin.min.js
vendored
2
js/admin.min.js
vendored
File diff suppressed because one or more lines are too long
@ -155,6 +155,10 @@ abstract class TestRequest extends Request {
|
||||
__new_die_impl($data);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function _execute(): bool {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class RequestAllMethods extends TestRequest {
|
||||
|
64
test/Router.test.php
Normal file
64
test/Router.test.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
require_once "core/Objects/Router.class.php";
|
||||
|
||||
use Configuration\Configuration;
|
||||
use Objects\Router\EmptyRoute;
|
||||
use Objects\User;
|
||||
|
||||
class RouterTest extends \PHPUnit\Framework\TestCase {
|
||||
|
||||
private static User $USER;
|
||||
|
||||
public static function setUpBeforeClass(): void {
|
||||
|
||||
$config = new Configuration();
|
||||
RouterTest::$USER = new User($config);
|
||||
}
|
||||
|
||||
public function testSimpleRoutes() {
|
||||
$this->assertNotFalse((new EmptyRoute("/"))->match("/"));
|
||||
$this->assertNotFalse((new EmptyRoute("/a"))->match("/a"));
|
||||
$this->assertNotFalse((new EmptyRoute("/b/"))->match("/b/"));
|
||||
$this->assertNotFalse((new EmptyRoute("/c/d"))->match("/c/d"));
|
||||
$this->assertNotFalse((new EmptyRoute("/e/f/"))->match("/e/f/"));
|
||||
}
|
||||
|
||||
public function testParamRoutes() {
|
||||
$paramsEmpty = (new EmptyRoute("/"))->match("/");
|
||||
$this->assertEquals([], $paramsEmpty);
|
||||
|
||||
$params1 = (new EmptyRoute("/:param"))->match("/test");
|
||||
$this->assertEquals(["param" => "test"], $params1);
|
||||
|
||||
$params2 = (new EmptyRoute("/:param1/:param2"))->match("/test/123");
|
||||
$this->assertEquals(["param1" => "test", "param2" => "123"], $params2);
|
||||
|
||||
$paramOptional1 = (new EmptyRoute("/:optional1?"))->match("/");
|
||||
$this->assertEquals(["optional1" => null], $paramOptional1);
|
||||
|
||||
$paramOptional2 = (new EmptyRoute("/:optional2?"))->match("/yes");
|
||||
$this->assertEquals(["optional2" => "yes"], $paramOptional2);
|
||||
|
||||
$paramOptional3 = (new EmptyRoute("/:optional3?/:optional4?"))->match("/1/2");
|
||||
$this->assertEquals(["optional3" => "1", "optional4" => "2"], $paramOptional3);
|
||||
|
||||
$mixedRoute = new EmptyRoute("/:optional5?/:notOptional");
|
||||
$paramMixed1 = $mixedRoute->match("/3/4");
|
||||
$this->assertEquals(["optional5" => "3", "notOptional" => "4"], $paramMixed1);
|
||||
}
|
||||
|
||||
public function testMixedRoute() {
|
||||
$mixedRoute1 = new EmptyRoute("/:param/static");
|
||||
$this->assertEquals(["param" => "yes"], $mixedRoute1->match("/yes/static"));
|
||||
|
||||
$mixedRoute2 = new EmptyRoute("/static/:param");
|
||||
$this->assertEquals(["param" => "yes"], $mixedRoute2->match("/static/yes"));
|
||||
}
|
||||
|
||||
public function testEmptyRoute() {
|
||||
$router = new Objects\Router(self::$USER);
|
||||
$emptyRoute = new EmptyRoute("/");
|
||||
$this->assertEquals("", $emptyRoute->call($router, []));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user