v2.1.0
This commit is contained in:
parent
58c905acf5
commit
35d7e4000a
2
cli.php
2
cli.php
@ -188,7 +188,7 @@ function handleDatabase(array $argv) {
|
|||||||
|
|
||||||
$command = array_merge([$command_bin], $command_args);
|
$command = array_merge([$command_bin], $command_args);
|
||||||
if ($config->getProperty("isDocker", false)) {
|
if ($config->getProperty("isDocker", false)) {
|
||||||
$command = array_merge(["docker", "exec", "-it", "db"], $command);
|
$command = array_merge(["docker", "exec", "-it", $config->getHost()], $command);
|
||||||
}
|
}
|
||||||
|
|
||||||
$process = proc_open($command, $descriptorSpec, $pipes, null, $env);
|
$process = proc_open($command, $descriptorSpec, $pipes, null, $env);
|
||||||
|
41
core/Api/Search.class.php
Normal file
41
core/Api/Search.class.php
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Api;
|
||||||
|
|
||||||
|
use Api\Parameter\StringType;
|
||||||
|
use Objects\Context;
|
||||||
|
use Objects\Search\Searchable;
|
||||||
|
use Objects\Search\SearchQuery;
|
||||||
|
|
||||||
|
class Search extends Request {
|
||||||
|
|
||||||
|
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
|
||||||
|
parent::__construct($context, $externalCall, [
|
||||||
|
"text" => new StringType("text", 32)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function _execute(): bool {
|
||||||
|
|
||||||
|
$query = new SearchQuery(trim($this->getParam("text")));
|
||||||
|
if (strlen($query->getQuery()) < 3) {
|
||||||
|
return $this->createError("You have to type at least 3 characters to search for");
|
||||||
|
}
|
||||||
|
|
||||||
|
$router = $this->context->router;
|
||||||
|
if ($router === null) {
|
||||||
|
return $this->createError("There is currently no router configured. Error during installation?");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->result["results"] = [];
|
||||||
|
foreach ($router->getRoutes(false) as $route) {
|
||||||
|
if(in_array(Searchable::class, array_keys((new \ReflectionClass($route))->getTraits()))) {
|
||||||
|
foreach ($route->doSearch($this->context, $query) as $searchResult) {
|
||||||
|
$this->result["results"][] = $searchResult->jsonSerialize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -198,7 +198,7 @@ class CreateDatabase extends DatabaseScript {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function loadEntities(&$queries, $sql) {
|
public static function loadEntities(&$queries, $sql) {
|
||||||
$entityDirectory = './core/Objects/DatabaseEntity/';
|
$entityDirectory = './core/Objects/DatabaseEntity/';
|
||||||
if (file_exists($entityDirectory) && is_dir($entityDirectory)) {
|
if (file_exists($entityDirectory) && is_dir($entityDirectory)) {
|
||||||
$scan_arr = scandir($entityDirectory);
|
$scan_arr = scandir($entityDirectory);
|
||||||
|
@ -10,6 +10,8 @@ use Objects\Router\Router;
|
|||||||
class Account extends TemplateDocument {
|
class Account extends TemplateDocument {
|
||||||
public function __construct(Router $router, string $templateName) {
|
public function __construct(Router $router, string $templateName) {
|
||||||
parent::__construct($router, $templateName);
|
parent::__construct($router, $templateName);
|
||||||
|
$this->title = "Account";
|
||||||
|
$this->searchable = false;
|
||||||
$this->enableCSP();
|
$this->enableCSP();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,8 @@ class Admin extends TemplateDocument {
|
|||||||
$user = $router->getContext()->getUser();
|
$user = $router->getContext()->getUser();
|
||||||
$template = $user ? "admin.twig" : "redirect.twig";
|
$template = $user ? "admin.twig" : "redirect.twig";
|
||||||
$params = $user ? [] : ["url" => "/login"];
|
$params = $user ? [] : ["url" => "/login"];
|
||||||
|
$this->title = "Administration";
|
||||||
|
$this->searchable = false;
|
||||||
parent::__construct($router, $template, $params);
|
parent::__construct($router, $template, $params);
|
||||||
$this->enableCSP();
|
$this->enableCSP();
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ use Objects\Router\Router;
|
|||||||
class Info extends HtmlDocument {
|
class Info extends HtmlDocument {
|
||||||
public function __construct(Router $router) {
|
public function __construct(Router $router) {
|
||||||
parent::__construct($router, EmptyHead::class, InfoBody::class);
|
parent::__construct($router, EmptyHead::class, InfoBody::class);
|
||||||
|
$this->searchable = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,8 +6,13 @@ use Configuration\Settings;
|
|||||||
use Driver\Logger\Logger;
|
use Driver\Logger\Logger;
|
||||||
use Driver\SQL\SQL;
|
use Driver\SQL\SQL;
|
||||||
use Objects\Context;
|
use Objects\Context;
|
||||||
|
use Objects\lang\LanguageModule;
|
||||||
|
use Objects\Router\DocumentRoute;
|
||||||
use Objects\Router\Router;
|
use Objects\Router\Router;
|
||||||
use Objects\DatabaseEntity\User;
|
use Objects\DatabaseEntity\User;
|
||||||
|
use Objects\Search\Searchable;
|
||||||
|
use Objects\Search\SearchQuery;
|
||||||
|
use Objects\Search\SearchResult;
|
||||||
|
|
||||||
abstract class Document {
|
abstract class Document {
|
||||||
|
|
||||||
@ -18,6 +23,7 @@ abstract class Document {
|
|||||||
private ?string $cspNonce;
|
private ?string $cspNonce;
|
||||||
private array $cspWhitelist;
|
private array $cspWhitelist;
|
||||||
private string $domain;
|
private string $domain;
|
||||||
|
protected bool $searchable;
|
||||||
|
|
||||||
public function __construct(Router $router) {
|
public function __construct(Router $router) {
|
||||||
$this->router = $router;
|
$this->router = $router;
|
||||||
@ -27,6 +33,13 @@ abstract class Document {
|
|||||||
$this->cspWhitelist = [];
|
$this->cspWhitelist = [];
|
||||||
$this->domain = $this->getSettings()->getBaseUrl();
|
$this->domain = $this->getSettings()->getBaseUrl();
|
||||||
$this->logger = new Logger("Document", $this->getSQL());
|
$this->logger = new Logger("Document", $this->getSQL());
|
||||||
|
$this->searchable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract function getTitle(): string;
|
||||||
|
|
||||||
|
public function isSearchable(): bool {
|
||||||
|
return $this->searchable;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLogger(): Logger {
|
public function getLogger(): Logger {
|
||||||
@ -66,30 +79,29 @@ abstract class Document {
|
|||||||
return $this->router;
|
return $this->router;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function addCSPWhitelist(string $path) {
|
public function addCSPWhitelist(string $path) {
|
||||||
$this->cspWhitelist[] = $this->domain . $path;
|
$urlParts = parse_url($path);
|
||||||
|
if (!$urlParts || !isset($urlParts["host"])) {
|
||||||
|
$this->cspWhitelist[] = $this->domain . $path;
|
||||||
|
} else {
|
||||||
|
$this->cspWhitelist[] = $path;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCode(array $params = []): string {
|
public function loadLanguageModule(LanguageModule|string $module) {
|
||||||
if ($this->databaseRequired) {
|
$language = $this->getContext()->getLanguage();
|
||||||
$sql = $this->getSQL();
|
$language->loadModule($module);
|
||||||
if (is_null($sql)) {
|
}
|
||||||
die("Database is not configured yet.");
|
|
||||||
} else if (!$sql->isConnected()) {
|
|
||||||
die("Database is not connected: " . $sql->getLastError());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public function sendHeaders() {
|
||||||
if ($this->cspEnabled) {
|
if ($this->cspEnabled) {
|
||||||
|
|
||||||
$cspWhiteList = implode(" ", $this->cspWhitelist);
|
$cspWhiteList = implode(" ", $this->cspWhitelist);
|
||||||
|
|
||||||
$csp = [
|
$csp = [
|
||||||
"default-src 'self'",
|
"default-src $cspWhiteList 'self'",
|
||||||
"object-src 'none'",
|
"object-src 'none'",
|
||||||
"base-uri 'self'",
|
"base-uri 'self'",
|
||||||
"style-src 'self' 'unsafe-inline'",
|
"style-src 'self' 'unsafe-inline'",
|
||||||
"img-src 'self' data:",
|
"img-src 'self' 'unsafe-inline' data: https:;",
|
||||||
"script-src $cspWhiteList 'nonce-$this->cspNonce'"
|
"script-src $cspWhiteList 'nonce-$this->cspNonce'"
|
||||||
];
|
];
|
||||||
if ($this->getSettings()->isRecaptchaEnabled()) {
|
if ($this->getSettings()->isRecaptchaEnabled()) {
|
||||||
@ -99,7 +111,42 @@ abstract class Document {
|
|||||||
$compiledCSP = implode("; ", $csp);
|
$compiledCSP = implode("; ", $csp);
|
||||||
header("Content-Security-Policy: $compiledCSP;");
|
header("Content-Security-Policy: $compiledCSP;");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return "";
|
public abstract function getCode(array $params = []);
|
||||||
|
|
||||||
|
public function load(array $params = []): string {
|
||||||
|
|
||||||
|
if ($this->databaseRequired) {
|
||||||
|
$sql = $this->getSQL();
|
||||||
|
if (is_null($sql)) {
|
||||||
|
return "Database is not configured yet.";
|
||||||
|
} else if (!$sql->isConnected()) {
|
||||||
|
return "Database is not connected: " . $sql->getLastError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$code = $this->getCode($params);
|
||||||
|
$this->sendHeaders();
|
||||||
|
return $code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function doSearch(SearchQuery $query, DocumentRoute $route): array {
|
||||||
|
$code = $this->getCode();
|
||||||
|
$results = Searchable::searchHtml($code, $query);
|
||||||
|
return array_map(function ($res) use ($route) {
|
||||||
|
return new SearchResult($route->getUrl(), $this->getTitle(), $res["text"]);
|
||||||
|
}, $results);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createScript($type, $src, $content = ""): Script {
|
||||||
|
$script = new Script($type, $src, $content);
|
||||||
|
|
||||||
|
if ($this->isCSPEnabled()) {
|
||||||
|
$script->setNonce($this->getCSPNonce());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $script;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -6,8 +6,8 @@ use Objects\Router\Router;
|
|||||||
|
|
||||||
class HtmlDocument extends Document {
|
class HtmlDocument extends Document {
|
||||||
|
|
||||||
protected Head $head;
|
protected ?Head $head;
|
||||||
protected Body $body;
|
protected ?Body $body;
|
||||||
private ?string $activeView;
|
private ?string $activeView;
|
||||||
|
|
||||||
public function __construct(Router $router, $headClass, $bodyClass, ?string $view = NULL) {
|
public function __construct(Router $router, $headClass, $bodyClass, ?string $view = NULL) {
|
||||||
@ -17,8 +17,8 @@ class HtmlDocument extends Document {
|
|||||||
$this->activeView = $view;
|
$this->activeView = $view;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getHead(): Head { return $this->head; }
|
public function getHead(): ?Head { return $this->head; }
|
||||||
public function getBody(): Body { return $this->body; }
|
public function getBody(): ?Body { return $this->body; }
|
||||||
|
|
||||||
public function getView() : ?View {
|
public function getView() : ?View {
|
||||||
|
|
||||||
@ -35,24 +35,11 @@ class HtmlDocument extends Document {
|
|||||||
return new $view($this);
|
return new $view($this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createScript($type, $src, $content = ""): Script {
|
|
||||||
$script = new Script($type, $src, $content);
|
|
||||||
|
|
||||||
if ($this->isCSPEnabled()) {
|
|
||||||
$script->setNonce($this->getCSPNonce());
|
|
||||||
}
|
|
||||||
|
|
||||||
return $script;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getRequestedView(): string {
|
public function getRequestedView(): string {
|
||||||
return $this->activeView;
|
return $this->activeView;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCode(array $params = []): string {
|
function getCode(array $params = []): string {
|
||||||
|
|
||||||
parent::getCode();
|
|
||||||
|
|
||||||
// generate body first, so we can modify head
|
// generate body first, so we can modify head
|
||||||
$body = $this->body->getCode();
|
$body = $this->body->getCode();
|
||||||
|
|
||||||
@ -74,4 +61,11 @@ class HtmlDocument extends Document {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function getTitle(): string {
|
||||||
|
if ($this->head !== null) {
|
||||||
|
return $this->head->getTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Untitled Document";
|
||||||
|
}
|
||||||
}
|
}
|
@ -2,7 +2,12 @@
|
|||||||
|
|
||||||
namespace Elements;
|
namespace Elements;
|
||||||
|
|
||||||
|
use Objects\CustomTwigFunctions;
|
||||||
|
use Objects\Router\DocumentRoute;
|
||||||
use Objects\Router\Router;
|
use Objects\Router\Router;
|
||||||
|
use Objects\Search\Searchable;
|
||||||
|
use Objects\Search\SearchQuery;
|
||||||
|
use Objects\Search\SearchResult;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
use Twig\Error\LoaderError;
|
use Twig\Error\LoaderError;
|
||||||
use Twig\Error\RuntimeError;
|
use Twig\Error\RuntimeError;
|
||||||
@ -11,6 +16,8 @@ use Twig\Loader\FilesystemLoader;
|
|||||||
|
|
||||||
class TemplateDocument extends Document {
|
class TemplateDocument extends Document {
|
||||||
|
|
||||||
|
const TEMPLATE_PATH = WEBROOT . '/core/Templates';
|
||||||
|
|
||||||
private string $templateName;
|
private string $templateName;
|
||||||
protected array $parameters;
|
protected array $parameters;
|
||||||
private Environment $twigEnvironment;
|
private Environment $twigEnvironment;
|
||||||
@ -19,14 +26,19 @@ class TemplateDocument extends Document {
|
|||||||
|
|
||||||
public function __construct(Router $router, string $templateName, array $params = []) {
|
public function __construct(Router $router, string $templateName, array $params = []) {
|
||||||
parent::__construct($router);
|
parent::__construct($router);
|
||||||
$this->title = "";
|
$this->title = "Untitled Document";
|
||||||
$this->templateName = $templateName;
|
$this->templateName = $templateName;
|
||||||
$this->parameters = $params;
|
$this->parameters = $params;
|
||||||
$this->twigLoader = new FilesystemLoader(WEBROOT . '/core/Templates');
|
$this->twigLoader = new FilesystemLoader(self::TEMPLATE_PATH);
|
||||||
$this->twigEnvironment = new Environment($this->twigLoader, [
|
$this->twigEnvironment = new Environment($this->twigLoader, [
|
||||||
'cache' => WEBROOT . '/core/Cache/Templates/',
|
'cache' => WEBROOT . '/core/Cache/Templates/',
|
||||||
'auto_reload' => true
|
'auto_reload' => true
|
||||||
]);
|
]);
|
||||||
|
$this->twigEnvironment->addExtension(new CustomTwigFunctions());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string {
|
||||||
|
return $this->title;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getTemplateName(): string {
|
protected function getTemplateName(): string {
|
||||||
@ -37,8 +49,11 @@ class TemplateDocument extends Document {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function setTemplate(string $file) {
|
||||||
|
$this->templateName = $file;
|
||||||
|
}
|
||||||
|
|
||||||
public function getCode(array $params = []): string {
|
public function getCode(array $params = []): string {
|
||||||
parent::getCode($params);
|
|
||||||
$this->loadParameters();
|
$this->loadParameters();
|
||||||
return $this->renderTemplate($this->templateName, $this->parameters);
|
return $this->renderTemplate($this->templateName, $this->parameters);
|
||||||
}
|
}
|
||||||
@ -48,33 +63,62 @@ class TemplateDocument extends Document {
|
|||||||
|
|
||||||
$context = $this->getContext();
|
$context = $this->getContext();
|
||||||
$session = $context->getSession();
|
$session = $context->getSession();
|
||||||
$params["user"] = [
|
|
||||||
"lang" => $context->getLanguage()->getShortCode(),
|
|
||||||
"loggedIn" => $session !== null,
|
|
||||||
"session" => ($session ? [
|
|
||||||
"csrfToken" => $session->getCsrfToken()
|
|
||||||
] : null)
|
|
||||||
];
|
|
||||||
|
|
||||||
$settings = $this->getSettings();
|
$settings = $this->getSettings();
|
||||||
$params["site"] = [
|
$language = $context->getLanguage();
|
||||||
"name" => $settings->getSiteName(),
|
|
||||||
"baseUrl" => $settings->getBaseUrl(),
|
|
||||||
"registrationEnabled" => $settings->isRegistrationAllowed(),
|
|
||||||
"title" => $this->title,
|
|
||||||
"recaptcha" => [
|
|
||||||
"key" => $settings->isRecaptchaEnabled() ? $settings->getRecaptchaSiteKey() : null,
|
|
||||||
"enabled" => $settings->isRecaptchaEnabled(),
|
|
||||||
],
|
|
||||||
"csp" => [
|
|
||||||
"nonce" => $this->getCSPNonce(),
|
|
||||||
"enabled" => $this->isCSPEnabled()
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
|
$urlParts = parse_url($this->getRouter()->getRequestedUri());
|
||||||
|
|
||||||
|
$params = array_replace_recursive([
|
||||||
|
"user" => [
|
||||||
|
"lang" => $language->getShortCode(),
|
||||||
|
"loggedIn" => $session !== null,
|
||||||
|
"session" => ($session ? [
|
||||||
|
"csrfToken" => $session->getCsrfToken()
|
||||||
|
] : null)
|
||||||
|
],
|
||||||
|
"site" => [
|
||||||
|
"name" => $settings->getSiteName(),
|
||||||
|
"url" => [
|
||||||
|
"base" => $settings->getBaseUrl(),
|
||||||
|
"path" => $urlParts["path"],
|
||||||
|
"query" => $urlParts["query"] ?? "",
|
||||||
|
"fragment" => $urlParts["fragment"] ?? ""
|
||||||
|
],
|
||||||
|
"lastModified" => date(L('Y-m-d H:i:s'), @filemtime(implode(DIRECTORY_SEPARATOR, [self::TEMPLATE_PATH, $name]))),
|
||||||
|
"registrationEnabled" => $settings->isRegistrationAllowed(),
|
||||||
|
"title" => $this->title,
|
||||||
|
"recaptcha" => [
|
||||||
|
"key" => $settings->isRecaptchaEnabled() ? $settings->getRecaptchaSiteKey() : null,
|
||||||
|
"enabled" => $settings->isRecaptchaEnabled(),
|
||||||
|
],
|
||||||
|
"csp" => [
|
||||||
|
"nonce" => $this->getCSPNonce(),
|
||||||
|
"enabled" => $this->isCSPEnabled()
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], $params);
|
||||||
return $this->twigEnvironment->render($name, $params);
|
return $this->twigEnvironment->render($name, $params);
|
||||||
} catch (LoaderError | RuntimeError | SyntaxError $e) {
|
} catch (LoaderError | RuntimeError | SyntaxError $e) {
|
||||||
return "<b>Error rendering twig template: " . htmlspecialchars($e->getMessage()) . "</b>";
|
return "<b>Error rendering twig template: " . htmlspecialchars($e->getMessage()) . "</b>";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function loadView(string $class): array {
|
||||||
|
$view = new $class($this);
|
||||||
|
$view->loadParameters($this->parameters);
|
||||||
|
if ($view->getTitle()) {
|
||||||
|
$this->title = $view->getTitle();
|
||||||
|
}
|
||||||
|
return $this->parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function doSearch(SearchQuery $query, DocumentRoute $route): array {
|
||||||
|
$this->loadParameters();
|
||||||
|
$viewParams = $this->parameters["view"] ?? [];
|
||||||
|
$siteTitle = $this->parameters["site"]["title"] ?? $this->title;
|
||||||
|
$results = Searchable::searchArray($viewParams, $query);
|
||||||
|
return array_map(function ($res) use ($siteTitle, $route) {
|
||||||
|
return new SearchResult($route->getUrl(), $siteTitle, $res["text"]);
|
||||||
|
}, $results);
|
||||||
|
}
|
||||||
}
|
}
|
39
core/Elements/TemplateView.class.php
Normal file
39
core/Elements/TemplateView.class.php
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Elements;
|
||||||
|
|
||||||
|
abstract class TemplateView extends View {
|
||||||
|
|
||||||
|
protected array $keywords = [];
|
||||||
|
protected string $description = "";
|
||||||
|
|
||||||
|
public function __construct(TemplateDocument $document) {
|
||||||
|
parent::__construct($document);
|
||||||
|
$this->title = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getParameters(): array {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadParameters(array &$parameters) {
|
||||||
|
|
||||||
|
$siteParameters = [
|
||||||
|
"title" => $this->title,
|
||||||
|
"description" => $this->description,
|
||||||
|
"keywords" => $this->keywords
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($siteParameters as $key => $value) {
|
||||||
|
if ($value) {
|
||||||
|
$parameters["site"][$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$parameters["view"] = $this->getParameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCode(): string {
|
||||||
|
return $this->getDocument()->getCode();
|
||||||
|
}
|
||||||
|
}
|
@ -6,13 +6,11 @@ abstract class View extends StaticView {
|
|||||||
|
|
||||||
private Document $document;
|
private Document $document;
|
||||||
private bool $loadView;
|
private bool $loadView;
|
||||||
protected bool $searchable;
|
|
||||||
protected string $title;
|
protected string $title;
|
||||||
protected array $langModules;
|
protected array $langModules;
|
||||||
|
|
||||||
public function __construct(Document $document, bool $loadView = true) {
|
public function __construct(Document $document, bool $loadView = true) {
|
||||||
$this->document = $document;
|
$this->document = $document;
|
||||||
$this->searchable = false;
|
|
||||||
$this->title = "Untitled View";
|
$this->title = "Untitled View";
|
||||||
$this->langModules = array();
|
$this->langModules = array();
|
||||||
$this->loadView = $loadView;
|
$this->loadView = $loadView;
|
||||||
@ -20,7 +18,6 @@ abstract class View extends StaticView {
|
|||||||
|
|
||||||
public function getTitle(): string { return $this->title; }
|
public function getTitle(): string { return $this->title; }
|
||||||
public function getDocument(): Document { return $this->document; }
|
public function getDocument(): Document { return $this->document; }
|
||||||
public function isSearchable(): bool { return $this->searchable; }
|
|
||||||
|
|
||||||
public function getSiteName(): string {
|
public function getSiteName(): string {
|
||||||
return $this->getDocument()->getSettings()->getSiteName();
|
return $this->getDocument()->getSettings()->getSiteName();
|
||||||
|
3
core/External/composer.json
vendored
3
core/External/composer.json
vendored
@ -6,7 +6,8 @@
|
|||||||
"christian-riesen/base32": "^1.6",
|
"christian-riesen/base32": "^1.6",
|
||||||
"spomky-labs/cbor-php": "2.1.0",
|
"spomky-labs/cbor-php": "2.1.0",
|
||||||
"web-auth/cose-lib": "3.3.12",
|
"web-auth/cose-lib": "3.3.12",
|
||||||
"firebase/php-jwt": "^6.2"
|
"firebase/php-jwt": "^6.2",
|
||||||
|
"html2text/html2text": "^4.3"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^9.5"
|
"phpunit/phpunit": "^9.5"
|
||||||
|
@ -12,6 +12,7 @@ use Firebase\JWT\JWT;
|
|||||||
use Objects\DatabaseEntity\Language;
|
use Objects\DatabaseEntity\Language;
|
||||||
use Objects\DatabaseEntity\Session;
|
use Objects\DatabaseEntity\Session;
|
||||||
use Objects\DatabaseEntity\User;
|
use Objects\DatabaseEntity\User;
|
||||||
|
use Objects\Router\Router;
|
||||||
|
|
||||||
class Context {
|
class Context {
|
||||||
|
|
||||||
@ -20,12 +21,14 @@ class Context {
|
|||||||
private ?User $user;
|
private ?User $user;
|
||||||
private Configuration $configuration;
|
private Configuration $configuration;
|
||||||
private Language $language;
|
private Language $language;
|
||||||
|
public ?Router $router;
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
|
|
||||||
$this->sql = null;
|
$this->sql = null;
|
||||||
$this->session = null;
|
$this->session = null;
|
||||||
$this->user = null;
|
$this->user = null;
|
||||||
|
$this->router = null;
|
||||||
$this->configuration = new Configuration();
|
$this->configuration = new Configuration();
|
||||||
$this->setLanguage(Language::DEFAULT_LANGUAGE());
|
$this->setLanguage(Language::DEFAULT_LANGUAGE());
|
||||||
|
|
||||||
|
18
core/Objects/CustomTwigFunctions.class.php
Normal file
18
core/Objects/CustomTwigFunctions.class.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Objects;
|
||||||
|
|
||||||
|
use Twig\Extension\AbstractExtension;
|
||||||
|
use Twig\TwigFunction;
|
||||||
|
|
||||||
|
class CustomTwigFunctions extends AbstractExtension {
|
||||||
|
public function getFunctions(): array {
|
||||||
|
return [
|
||||||
|
new TwigFunction('L', array($this, 'translate')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function translate(string $key): string {
|
||||||
|
return L($key);
|
||||||
|
}
|
||||||
|
}
|
@ -76,6 +76,18 @@ abstract class DatabaseEntity {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function insert(SQL $sql): bool {
|
||||||
|
$handler = self::getHandler($sql);
|
||||||
|
$res = $handler->insert($this);
|
||||||
|
if ($res === false) {
|
||||||
|
return false;
|
||||||
|
} else if ($this->id === null) {
|
||||||
|
$this->id = $res;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public function delete(SQL $sql): bool {
|
public function delete(SQL $sql): bool {
|
||||||
$handler = self::getHandler($sql);
|
$handler = self::getHandler($sql);
|
||||||
if ($this->id === null) {
|
if ($this->id === null) {
|
||||||
|
@ -311,10 +311,7 @@ class DatabaseEntityHandler {
|
|||||||
return $query->execute();
|
return $query->execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function insertOrUpdate(DatabaseEntity $entity, ?array $columns = null) {
|
private function prepareRow(DatabaseEntity $entity, string $action, ?array $columns = null) {
|
||||||
$id = $entity->getId();
|
|
||||||
$action = $id === null ? "insert" : "update";
|
|
||||||
|
|
||||||
$row = [];
|
$row = [];
|
||||||
foreach ($this->columns as $propertyName => $column) {
|
foreach ($this->columns as $propertyName => $column) {
|
||||||
if ($columns && !in_array($column->getName(), $columns)) {
|
if ($columns && !in_array($column->getName(), $columns)) {
|
||||||
@ -331,7 +328,10 @@ class DatabaseEntityHandler {
|
|||||||
} else if (!$this->columns[$propertyName]->notNull()) {
|
} else if (!$this->columns[$propertyName]->notNull()) {
|
||||||
$value = null;
|
$value = null;
|
||||||
} else {
|
} else {
|
||||||
if ($action !== "update") {
|
$defaultValue = self::getAttribute($property, DefaultValue::class);
|
||||||
|
if ($defaultValue) {
|
||||||
|
$value = $defaultValue->getValue();
|
||||||
|
} else if ($action !== "update") {
|
||||||
$this->logger->error("Cannot $action entity: property '$propertyName' was not initialized yet.");
|
$this->logger->error("Cannot $action entity: property '$propertyName' was not initialized yet.");
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
@ -342,29 +342,62 @@ class DatabaseEntityHandler {
|
|||||||
$row[$column->getName()] = $value;
|
$row[$column->getName()] = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(DatabaseEntity $entity, ?array $columns = null) {
|
||||||
|
$row = $this->prepareRow($entity, "update", $columns);
|
||||||
|
if ($row === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$entity->preInsert($row);
|
||||||
|
$query = $this->sql->update($this->tableName)
|
||||||
|
->where(new Compare($this->tableName . ".id", $entity->getId()));
|
||||||
|
|
||||||
|
foreach ($row as $columnName => $value) {
|
||||||
|
$query->set($columnName, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function insert(DatabaseEntity $entity) {
|
||||||
|
$row = $this->prepareRow($entity, "insert");
|
||||||
|
if ($row === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
$entity->preInsert($row);
|
$entity->preInsert($row);
|
||||||
|
|
||||||
|
// insert with id?
|
||||||
|
$entityId = $entity->getId();
|
||||||
|
if ($entityId !== null) {
|
||||||
|
$row["id"] = $entityId;
|
||||||
|
}
|
||||||
|
|
||||||
if ($id === null) {
|
$query = $this->sql->insert($this->tableName, array_keys($row))
|
||||||
$res = $this->sql->insert($this->tableName, array_keys($row))
|
->addRow(...array_values($row));
|
||||||
->addRow(...array_values($row))
|
|
||||||
->returning("id")
|
|
||||||
->execute();
|
|
||||||
|
|
||||||
if ($res !== false) {
|
// return id if its auto-generated
|
||||||
return $this->sql->getLastInsertId();
|
if ($entityId === null) {
|
||||||
} else {
|
$query->returning("id");
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
|
$res = $query->execute();
|
||||||
|
if ($res !== false) {
|
||||||
|
return $this->sql->getLastInsertId();
|
||||||
} else {
|
} else {
|
||||||
$query = $this->sql->update($this->tableName)
|
return false;
|
||||||
->where(new Compare($this->tableName . ".id", $id));
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($row as $columnName => $value) {
|
public function insertOrUpdate(DatabaseEntity $entity, ?array $columns = null) {
|
||||||
$query->set($columnName, $value);
|
$id = $entity->getId();
|
||||||
}
|
if ($id === null) {
|
||||||
|
return $this->insert($entity);
|
||||||
return $query->execute();
|
} else {
|
||||||
|
return $this->update($entity, $columns);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Objects\DatabaseEntity {
|
namespace Objects\DatabaseEntity {
|
||||||
|
|
||||||
|
use Driver\SQL\SQL;
|
||||||
use Objects\DatabaseEntity\Attribute\MaxLength;
|
use Objects\DatabaseEntity\Attribute\MaxLength;
|
||||||
use Objects\DatabaseEntity\Attribute\Transient;
|
use Objects\DatabaseEntity\Attribute\Transient;
|
||||||
use Objects\lang\LanguageModule;
|
use Objects\lang\LanguageModule;
|
||||||
@ -14,15 +15,13 @@ namespace Objects\DatabaseEntity {
|
|||||||
#[MaxLength(5)] private string $code;
|
#[MaxLength(5)] private string $code;
|
||||||
#[MaxLength(32)] private string $name;
|
#[MaxLength(32)] private string $name;
|
||||||
|
|
||||||
#[Transient] private array $modules;
|
#[Transient] private array $modules = [];
|
||||||
#[Transient] protected array $entries;
|
#[Transient] protected array $entries = [];
|
||||||
|
|
||||||
public function __construct(int $id, string $code, string $name) {
|
public function __construct(int $id, string $code, string $name) {
|
||||||
parent::__construct($id);
|
parent::__construct($id);
|
||||||
$this->code = $code;
|
$this->code = $code;
|
||||||
$this->name = $name;
|
$this->name = $name;
|
||||||
$this->entries = array();
|
|
||||||
$this->modules = array();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCode(): string {
|
public function getCode(): string {
|
||||||
@ -42,9 +41,11 @@ namespace Objects\DatabaseEntity {
|
|||||||
$module = new $module();
|
$module = new $module();
|
||||||
}
|
}
|
||||||
|
|
||||||
$moduleEntries = $module->getEntries($this->code);
|
if (!in_array($module, $this->modules)) {
|
||||||
$this->entries = array_merge($this->entries, $moduleEntries);
|
$moduleEntries = $module->getEntries($this->code);
|
||||||
$this->modules[] = $module;
|
$this->entries = array_merge($this->entries, $moduleEntries);
|
||||||
|
$this->modules[] = $module;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function translate(string $key): string {
|
public function translate(string $key): string {
|
||||||
@ -89,6 +90,10 @@ namespace Objects\DatabaseEntity {
|
|||||||
|
|
||||||
return new Language(1, "en_US", "American English");
|
return new Language(1, "en_US", "American English");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getEntries(): array {
|
||||||
|
return $this->entries;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,12 +105,6 @@ class Session extends DatabaseEntity {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function insert(bool $stayLoggedIn = false): bool {
|
|
||||||
$this->stayLoggedIn = $stayLoggedIn;
|
|
||||||
$this->active = true;
|
|
||||||
return $this->update();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function destroy(): bool {
|
public function destroy(): bool {
|
||||||
session_destroy();
|
session_destroy();
|
||||||
$this->active = false;
|
$this->active = false;
|
||||||
|
@ -6,6 +6,8 @@ use Api\Parameter\Parameter;
|
|||||||
|
|
||||||
abstract class AbstractRoute {
|
abstract class AbstractRoute {
|
||||||
|
|
||||||
|
const PARAMETER_PATTERN = "/^{([^:]+)(:(.*?)(\?)?)?}$/";
|
||||||
|
|
||||||
private string $pattern;
|
private string $pattern;
|
||||||
private bool $exact;
|
private bool $exact;
|
||||||
|
|
||||||
@ -64,7 +66,7 @@ abstract class AbstractRoute {
|
|||||||
|
|
||||||
$params = [];
|
$params = [];
|
||||||
for (; $patternOffset < $countPattern; $patternOffset++) {
|
for (; $patternOffset < $countPattern; $patternOffset++) {
|
||||||
if (!preg_match("/^{([^:]+)(:(.*?)(\?)?)?}$/", $patternParts[$patternOffset], $match)) {
|
if (!preg_match(self::PARAMETER_PATTERN, $patternParts[$patternOffset], $match)) {
|
||||||
|
|
||||||
// not a parameter? check if it matches
|
// not a parameter? check if it matches
|
||||||
if ($urlOffset >= $countUrl || $urlParts[$urlOffset] !== $patternParts[$patternOffset]) {
|
if ($urlOffset >= $countUrl || $urlParts[$urlOffset] !== $patternParts[$patternOffset]) {
|
||||||
@ -122,4 +124,30 @@ abstract class AbstractRoute {
|
|||||||
|
|
||||||
return $params;
|
return $params;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getUrl(array $parameters = []): string {
|
||||||
|
$patternParts = explode("/", Router::cleanURL($this->pattern, false));
|
||||||
|
|
||||||
|
foreach ($patternParts as $i => $part) {
|
||||||
|
if (preg_match(self::PARAMETER_PATTERN, $part, $match)) {
|
||||||
|
$paramName = $match[1];
|
||||||
|
$patternParts[$i] = $parameters[$paramName] ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "/" . implode("/", array_filter($patternParts));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getParameterNames(): array {
|
||||||
|
$parameterNames = [];
|
||||||
|
$patternParts = explode("/", Router::cleanURL($this->pattern, false));
|
||||||
|
|
||||||
|
foreach ($patternParts as $part) {
|
||||||
|
if (preg_match(self::PARAMETER_PATTERN, $part, $match)) {
|
||||||
|
$parameterNames[] = $match[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $parameterNames;
|
||||||
|
}
|
||||||
}
|
}
|
@ -21,7 +21,7 @@ class ApiRoute extends AbstractRoute {
|
|||||||
if (empty($params["endpoint"])) {
|
if (empty($params["endpoint"])) {
|
||||||
header("Content-Type: text/html");
|
header("Content-Type: text/html");
|
||||||
$document = new \Elements\TemplateDocument($router, "swagger.twig");
|
$document = new \Elements\TemplateDocument($router, "swagger.twig");
|
||||||
return $document->getCode();
|
return $document->load();
|
||||||
} else if (!preg_match("/[a-zA-Z]+/", $params["endpoint"])) {
|
} else if (!preg_match("/[a-zA-Z]+/", $params["endpoint"])) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
$response = createError("Invalid Method");
|
$response = createError("Invalid Method");
|
||||||
|
@ -3,10 +3,15 @@
|
|||||||
namespace Objects\Router;
|
namespace Objects\Router;
|
||||||
|
|
||||||
use Elements\Document;
|
use Elements\Document;
|
||||||
|
use Objects\Context;
|
||||||
|
use Objects\Search\Searchable;
|
||||||
|
use Objects\Search\SearchQuery;
|
||||||
use ReflectionException;
|
use ReflectionException;
|
||||||
|
|
||||||
class DocumentRoute extends AbstractRoute {
|
class DocumentRoute extends AbstractRoute {
|
||||||
|
|
||||||
|
use Searchable;
|
||||||
|
|
||||||
private string $className;
|
private string $className;
|
||||||
private array $args;
|
private array $args;
|
||||||
private ?\ReflectionClass $reflectionClass;
|
private ?\ReflectionClass $reflectionClass;
|
||||||
@ -31,7 +36,7 @@ class DocumentRoute extends AbstractRoute {
|
|||||||
}
|
}
|
||||||
} catch (ReflectionException $exception) {
|
} catch (ReflectionException $exception) {
|
||||||
$this->reflectionClass = null;
|
$this->reflectionClass = null;
|
||||||
return false;
|
throw $exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->reflectionClass = null;
|
$this->reflectionClass = null;
|
||||||
@ -55,16 +60,32 @@ class DocumentRoute extends AbstractRoute {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function call(Router $router, array $params): string {
|
public function call(Router $router, array $params): string {
|
||||||
if (!$this->loadClass()) {
|
|
||||||
return $router->returnStatusCode(500, [ "message" => "Error loading class: $this->className"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$args = array_merge([$router], $this->args);
|
if (!$this->loadClass()) {
|
||||||
|
return $router->returnStatusCode(500, [ "message" => "Error loading class: $this->className"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$args = array_merge([$router], $this->args, $params);
|
||||||
$document = $this->reflectionClass->newInstanceArgs($args);
|
$document = $this->reflectionClass->newInstanceArgs($args);
|
||||||
return $document->getCode($params);
|
return $document->load($params);
|
||||||
} catch (\ReflectionException $e) {
|
} catch (\ReflectionException $e) {
|
||||||
return $router->returnStatusCode(500, [ "message" => "Error loading class $this->className: " . $e->getMessage()]);
|
return $router->returnStatusCode(500, [ "message" => "Error loading class $this->className: " . $e->getMessage()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function doSearch(Context $context, SearchQuery $query): array {
|
||||||
|
try {
|
||||||
|
if ($this->loadClass()) {
|
||||||
|
$args = array_merge([$context->router], $this->args);
|
||||||
|
$document = $this->reflectionClass->newInstanceArgs($args);
|
||||||
|
if ($document->isSearchable()) {
|
||||||
|
return $document->doSearch($query, $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
} catch (\ReflectionException) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -9,6 +9,8 @@ class Router {
|
|||||||
|
|
||||||
private Context $context;
|
private Context $context;
|
||||||
private Logger $logger;
|
private Logger $logger;
|
||||||
|
private ?AbstractRoute $activeRoute;
|
||||||
|
private ?string $requestedUri;
|
||||||
protected array $routes;
|
protected array $routes;
|
||||||
protected array $statusCodeRoutes;
|
protected array $statusCodeRoutes;
|
||||||
|
|
||||||
@ -16,6 +18,9 @@ class Router {
|
|||||||
$this->context = $context;
|
$this->context = $context;
|
||||||
$this->routes = [];
|
$this->routes = [];
|
||||||
$this->statusCodeRoutes = [];
|
$this->statusCodeRoutes = [];
|
||||||
|
$this->activeRoute = null;
|
||||||
|
$this->requestedUri = null;
|
||||||
|
$this->context->router = $this;
|
||||||
|
|
||||||
$sql = $context->getSQL();
|
$sql = $context->getSQL();
|
||||||
if ($sql) {
|
if ($sql) {
|
||||||
@ -26,14 +31,24 @@ class Router {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getActiveRoute(): ?AbstractRoute {
|
||||||
|
return $this->activeRoute;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRequestedUri(): ?string {
|
||||||
|
return $this->requestedUri;
|
||||||
|
}
|
||||||
|
|
||||||
public function run(string $url): string {
|
public function run(string $url): string {
|
||||||
|
|
||||||
// TODO: do we want a global try cache and return status page 500 on any error?
|
// TODO: do we want a global try cache and return status page 500 on any error?
|
||||||
|
$this->requestedUri = $url;
|
||||||
|
|
||||||
$url = strtok($url, "?");
|
$url = strtok($url, "?");
|
||||||
foreach ($this->routes as $route) {
|
foreach ($this->routes as $route) {
|
||||||
$pathParams = $route->match($url);
|
$pathParams = $route->match($url);
|
||||||
if ($pathParams !== false) {
|
if ($pathParams !== false) {
|
||||||
|
$this->activeRoute = $route;
|
||||||
return $route->call($this, $pathParams);
|
return $route->call($this, $pathParams);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -54,7 +69,6 @@ class Router {
|
|||||||
if ($res) {
|
if ($res) {
|
||||||
return $req->getResult()["html"];
|
return $req->getResult()["html"];
|
||||||
} else {
|
} else {
|
||||||
var_dump($req->getLastError());
|
|
||||||
$description = htmlspecialchars($params["status_description"]);
|
$description = htmlspecialchars($params["status_description"]);
|
||||||
return "<b>$code - $description</b>";
|
return "<b>$code - $description</b>";
|
||||||
}
|
}
|
||||||
@ -62,11 +76,11 @@ class Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function addRoute(AbstractRoute $route) {
|
public function addRoute(AbstractRoute $route) {
|
||||||
if (preg_match("/^\d+$/", $route->getPattern())) {
|
if (preg_match("/^\/(\d+)$/", $route->getPattern(), $re)) {
|
||||||
$this->statusCodeRoutes[$route->getPattern()] = $route;
|
$this->statusCodeRoutes[$re[1]] = $route;
|
||||||
} else {
|
|
||||||
$this->routes[] = $route;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->routes[] = $route;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function writeCache(string $file): bool {
|
public function writeCache(string $file): bool {
|
||||||
@ -134,4 +148,15 @@ class RouterCache extends Router {
|
|||||||
// strip leading slash
|
// strip leading slash
|
||||||
return preg_replace("/^\/+/", "", $url);
|
return preg_replace("/^\/+/", "", $url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getRoutes(bool $includeStatusRoutes = false): array {
|
||||||
|
|
||||||
|
if (!$includeStatusRoutes && !empty($this->statusCodeRoutes)) {
|
||||||
|
return array_filter($this->routes, function ($route) {
|
||||||
|
return !in_array($route, $this->statusCodeRoutes);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->routes;
|
||||||
|
}
|
||||||
}
|
}
|
@ -2,8 +2,15 @@
|
|||||||
|
|
||||||
namespace Objects\Router;
|
namespace Objects\Router;
|
||||||
|
|
||||||
|
use Objects\Context;
|
||||||
|
use Objects\Search\Searchable;
|
||||||
|
use Objects\Search\SearchQuery;
|
||||||
|
use Objects\Search\SearchResult;
|
||||||
|
|
||||||
class StaticFileRoute extends AbstractRoute {
|
class StaticFileRoute extends AbstractRoute {
|
||||||
|
|
||||||
|
use Searchable;
|
||||||
|
|
||||||
private string $path;
|
private string $path;
|
||||||
private int $code;
|
private int $code;
|
||||||
|
|
||||||
@ -15,7 +22,7 @@ class StaticFileRoute extends AbstractRoute {
|
|||||||
|
|
||||||
public function call(Router $router, array $params): string {
|
public function call(Router $router, array $params): string {
|
||||||
http_response_code($this->code);
|
http_response_code($this->code);
|
||||||
$this->serveStatic($this->path, $router);
|
$this->serveStatic($this->getAbsolutePath(), $router);
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,9 +30,11 @@ class StaticFileRoute extends AbstractRoute {
|
|||||||
return array_merge(parent::getArgs(), [$this->path, $this->code]);
|
return array_merge(parent::getArgs(), [$this->path, $this->code]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function serveStatic(string $path, ?Router $router = null) {
|
public function getAbsolutePath(): string {
|
||||||
|
return WEBROOT . DIRECTORY_SEPARATOR . $this->path;
|
||||||
|
}
|
||||||
|
|
||||||
$path = realpath(WEBROOT . DIRECTORY_SEPARATOR . $path);
|
public static function serveStatic(string $path, ?Router $router = null) {
|
||||||
if (!startsWith($path, WEBROOT . DIRECTORY_SEPARATOR)) {
|
if (!startsWith($path, WEBROOT . DIRECTORY_SEPARATOR)) {
|
||||||
http_response_code(406);
|
http_response_code(406);
|
||||||
echo "<b>Access restricted, requested file outside web root:</b> " . htmlspecialchars($path);
|
echo "<b>Access restricted, requested file outside web root:</b> " . htmlspecialchars($path);
|
||||||
@ -72,4 +81,34 @@ class StaticFileRoute extends AbstractRoute {
|
|||||||
downloadFile($handle, $offset, $length);
|
downloadFile($handle, $offset, $length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function doSearch(Context $context, SearchQuery $query): array {
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
$path = $this->getAbsolutePath();
|
||||||
|
if (is_file($path) && is_readable($path)) {
|
||||||
|
$pathInfo = pathinfo($path);
|
||||||
|
$extension = $pathInfo["extension"] ?? "";
|
||||||
|
$fileName = $pathInfo["filename"] ?? "";
|
||||||
|
if ($context->getSettings()->isExtensionAllowed($extension)) {
|
||||||
|
$mimeType = mime_content_type($path);
|
||||||
|
if (startsWith($mimeType, "text/")) {
|
||||||
|
$document = @file_get_contents($path);
|
||||||
|
if ($document) {
|
||||||
|
if ($mimeType === "text/html") {
|
||||||
|
$results = Searchable::searchHtml($document, $query);
|
||||||
|
} else {
|
||||||
|
$results = Searchable::searchText($document, $query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$results = array_map(function ($res) use ($fileName) {
|
||||||
|
return new SearchResult($this->getPattern(), $fileName, $res["text"]);
|
||||||
|
}, $results);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
}
|
}
|
19
core/Objects/Search/SearchQuery.class.php
Normal file
19
core/Objects/Search/SearchQuery.class.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Objects\Search;
|
||||||
|
|
||||||
|
class SearchQuery {
|
||||||
|
|
||||||
|
private string $query;
|
||||||
|
private array $parts;
|
||||||
|
|
||||||
|
public function __construct(string $query) {
|
||||||
|
$this->query = $query;
|
||||||
|
$this->parts = array_unique(array_filter(explode(" ", strtolower($query))));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getQuery(): string {
|
||||||
|
return $this->query;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
30
core/Objects/Search/SearchResult.class.php
Normal file
30
core/Objects/Search/SearchResult.class.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Objects\Search;
|
||||||
|
|
||||||
|
use Objects\ApiObject;
|
||||||
|
|
||||||
|
class SearchResult extends ApiObject {
|
||||||
|
|
||||||
|
private string $url;
|
||||||
|
private string $title;
|
||||||
|
private string $text;
|
||||||
|
|
||||||
|
public function __construct(string $url, string $title, string $text) {
|
||||||
|
$this->url = $url;
|
||||||
|
$this->title = $title;
|
||||||
|
$this->text = $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function jsonSerialize(): array {
|
||||||
|
return [
|
||||||
|
"url" => $this->url,
|
||||||
|
"title" => $this->title,
|
||||||
|
"text" => $this->text
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUrl(string $url) {
|
||||||
|
$this->url = $url;
|
||||||
|
}
|
||||||
|
}
|
91
core/Objects/Search/Searchable.trait.php
Normal file
91
core/Objects/Search/Searchable.trait.php
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Objects\Search;
|
||||||
|
|
||||||
|
use Html2Text\Html2Text;
|
||||||
|
use Objects\Context;
|
||||||
|
use function PHPUnit\Framework\stringContains;
|
||||||
|
|
||||||
|
trait Searchable {
|
||||||
|
|
||||||
|
public static function searchArray(array $arr, SearchQuery $query): array {
|
||||||
|
$results = [];
|
||||||
|
foreach ($arr as $key => $value) {
|
||||||
|
$results = array_merge($results, self::searchHtml($key, $query));
|
||||||
|
if (is_array($value)) {
|
||||||
|
$results = array_merge($results, self::searchArray($value, $query));
|
||||||
|
} else {
|
||||||
|
$results = array_merge($results, self::searchHtml(strval($value), $query));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract function doSearch(Context $context, SearchQuery $query): array;
|
||||||
|
|
||||||
|
public static function searchHtml(string $document, SearchQuery $query): array {
|
||||||
|
if (stringContains($document, "<")) {
|
||||||
|
$converter = new Html2Text($document);
|
||||||
|
$text = trim($converter->getText());
|
||||||
|
} else {
|
||||||
|
$text = $document;
|
||||||
|
}
|
||||||
|
|
||||||
|
$text = trim(preg_replace('!\s+!', ' ', $text));
|
||||||
|
return self::searchText($text, $query);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function searchText(string $content, SearchQuery $query): array {
|
||||||
|
$offset = 0;
|
||||||
|
$searchTerm = $query->getQuery();
|
||||||
|
$stringLength = strlen($searchTerm);
|
||||||
|
$contentLength = strlen($content);
|
||||||
|
$lastPos = 0;
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
do {
|
||||||
|
$pos = stripos($content, $searchTerm, $offset);
|
||||||
|
if ($pos !== false) {
|
||||||
|
if ($lastPos === 0 || $pos > $lastPos + 192 + $stringLength) {
|
||||||
|
$extract = self::viewExtract($content, $pos, $searchTerm);
|
||||||
|
$results[] = array(
|
||||||
|
"text" => $extract,
|
||||||
|
"pos" => $pos,
|
||||||
|
"lastPos" => $lastPos
|
||||||
|
);
|
||||||
|
$lastPos = $pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
$offset = $pos + $stringLength;
|
||||||
|
}
|
||||||
|
} while ($pos !== false && $offset < $contentLength);
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function viewExtract(string $content, int $pos, $string): array|string|null {
|
||||||
|
$length = strlen($string);
|
||||||
|
$start = max(0, $pos - 32);
|
||||||
|
$end = min(strlen($content) - 1, $pos + $length + 192);
|
||||||
|
|
||||||
|
if ($start > 0) {
|
||||||
|
if (!ctype_space($content[$start - 1]) &&
|
||||||
|
!ctype_space($content[$start])) {
|
||||||
|
$start = $start + strpos(substr($content, $start, $end), ' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($end < strlen($content) - 1) {
|
||||||
|
if (!ctype_space($content[$end + 1]) &&
|
||||||
|
!ctype_space($content[$end])) {
|
||||||
|
$end = $start + strrpos(substr($content, $start, $end - $start), ' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$extract = trim(substr($content, $start, $end - $start + 1));
|
||||||
|
if ($start > 0) $extract = ".. " . $extract;
|
||||||
|
if ($end < strlen($content) - 1) $extract .= " ..";
|
||||||
|
return preg_replace("/" . preg_quote($string) . "(?=[^>]*(<|$))/i", "<span class=\"highlight\">\$0</span>", $extract);
|
||||||
|
}
|
||||||
|
}
|
@ -10,7 +10,7 @@ if (is_file($autoLoad)) {
|
|||||||
require_once $autoLoad;
|
require_once $autoLoad;
|
||||||
}
|
}
|
||||||
|
|
||||||
define("WEBBASE_VERSION", "2.0.0-alpha");
|
define("WEBBASE_VERSION", "2.1.0");
|
||||||
|
|
||||||
spl_autoload_extensions(".php");
|
spl_autoload_extensions(".php");
|
||||||
spl_autoload_register(function ($class) {
|
spl_autoload_register(function ($class) {
|
||||||
|
@ -35,7 +35,7 @@ if ($installation) {
|
|||||||
header("Location: /");
|
header("Location: /");
|
||||||
} else {
|
} else {
|
||||||
$document = new Documents\Install(new Router($context));
|
$document = new Documents\Install(new Router($context));
|
||||||
$response = $document->getCode();
|
$response = $document->load();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
28
js/script.js
28
js/script.js
@ -41,7 +41,7 @@ let Core = function () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.getParameters = function () {
|
this.getParameters = function () {
|
||||||
return this.aParameters;
|
return this.parameters;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setTitle = function (title) {
|
this.setTitle = function (title) {
|
||||||
@ -71,27 +71,27 @@ let Core = function () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.removeParameter = function (param) {
|
this.removeParameter = function (param) {
|
||||||
if (typeof this.aParameters[param] !== 'undefined' && this.aParameters.hasOwnProperty(param)) {
|
if (typeof this.parameters[param] !== 'undefined' && this.parameters.hasOwnProperty(param)) {
|
||||||
delete this.aParameters[param];
|
delete this.parameters[param];
|
||||||
}
|
}
|
||||||
this.updateUrl();
|
this.updateUrl();
|
||||||
};
|
};
|
||||||
|
|
||||||
this.getParameter = function (param) {
|
this.getParameter = function (param) {
|
||||||
if (typeof this.aParameters[param] !== 'undefined' && this.aParameters.hasOwnProperty(param))
|
if (typeof this.parameters[param] !== 'undefined' && this.parameters.hasOwnProperty(param))
|
||||||
return this.aParameters[param];
|
return this.parameters[param];
|
||||||
else
|
else
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setParameter = function (param, newvalue) {
|
this.setParameter = function (param, newvalue) {
|
||||||
newvalue = typeof newvalue !== 'undefined' ? newvalue : '';
|
newvalue = typeof newvalue !== 'undefined' ? newvalue : '';
|
||||||
this.aParameters[param] = newvalue;
|
this.parameters[param] = newvalue;
|
||||||
this.updateUrl();
|
this.updateUrl();
|
||||||
};
|
};
|
||||||
|
|
||||||
this.parseParameters = function () {
|
this.parseParameters = function () {
|
||||||
this.aParameters = [];
|
this.parameters = [];
|
||||||
if (this.url.indexOf('?') === -1)
|
if (this.url.indexOf('?') === -1)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -103,30 +103,32 @@ let Core = function () {
|
|||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
var key = param.substr(0, index);
|
var key = param.substr(0, index);
|
||||||
var val = param.substr(index + 1);
|
var val = param.substr(index + 1);
|
||||||
this.aParameters[key] = val;
|
this.parameters[key] = val;
|
||||||
} else
|
} else
|
||||||
this.aParameters[param] = '';
|
this.parameters[param] = '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.updateUrl = function () {
|
this.updateUrl = function () {
|
||||||
this.clearUrl();
|
this.clearUrl();
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (var parameter in this.aParameters) {
|
for (var parameter in this.parameters) {
|
||||||
this.url += (i === 0 ? "?" : "&") + parameter;
|
this.url += (i === 0 ? "?" : "&") + parameter;
|
||||||
if (this.aParameters.hasOwnProperty(parameter) && this.aParameters[parameter].toString().length > 0) {
|
if (this.parameters.hasOwnProperty(parameter) && this.parameters[parameter].toString().length > 0) {
|
||||||
this.url += "=" + this.aParameters[parameter];
|
this.url += "=" + this.parameters[parameter];
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.clearParameters = function () {
|
this.clearParameters = function () {
|
||||||
this.aParameters = [];
|
this.parameters = [];
|
||||||
this.updateUrl();
|
this.updateUrl();
|
||||||
};
|
};
|
||||||
|
|
||||||
this.clearUrl = function () {
|
this.clearUrl = function () {
|
||||||
|
if (this.url.indexOf('#') !== -1)
|
||||||
|
this.url = this.url.substring(0, this.url.indexOf('#'));
|
||||||
if (this.url.indexOf('?') !== -1)
|
if (this.url.indexOf('?') !== -1)
|
||||||
this.url = this.url.substring(0, this.url.indexOf('?'));
|
this.url = this.url.substring(0, this.url.indexOf('?'));
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user