diff --git a/cli.php b/cli.php
index a8dae81..637375b 100644
--- a/cli.php
+++ b/cli.php
@@ -188,7 +188,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", $config->getHost()], $command);
}
$process = proc_open($command, $descriptorSpec, $pipes, null, $env);
diff --git a/core/Api/Search.class.php b/core/Api/Search.class.php
new file mode 100644
index 0000000..b4ecd2b
--- /dev/null
+++ b/core/Api/Search.class.php
@@ -0,0 +1,41 @@
+ 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;
+ }
+}
\ No newline at end of file
diff --git a/core/Configuration/CreateDatabase.class.php b/core/Configuration/CreateDatabase.class.php
index f6fdd21..f59c3fb 100644
--- a/core/Configuration/CreateDatabase.class.php
+++ b/core/Configuration/CreateDatabase.class.php
@@ -198,7 +198,7 @@ class CreateDatabase extends DatabaseScript {
}
}
- private static function loadEntities(&$queries, $sql) {
+ public static function loadEntities(&$queries, $sql) {
$entityDirectory = './core/Objects/DatabaseEntity/';
if (file_exists($entityDirectory) && is_dir($entityDirectory)) {
$scan_arr = scandir($entityDirectory);
diff --git a/core/Documents/Account.class.php b/core/Documents/Account.class.php
index 0960911..1d4d9fe 100644
--- a/core/Documents/Account.class.php
+++ b/core/Documents/Account.class.php
@@ -10,6 +10,8 @@ use Objects\Router\Router;
class Account extends TemplateDocument {
public function __construct(Router $router, string $templateName) {
parent::__construct($router, $templateName);
+ $this->title = "Account";
+ $this->searchable = false;
$this->enableCSP();
}
diff --git a/core/Documents/Admin.class.php b/core/Documents/Admin.class.php
index 3839cfe..1a6ba0f 100644
--- a/core/Documents/Admin.class.php
+++ b/core/Documents/Admin.class.php
@@ -10,6 +10,8 @@ class Admin extends TemplateDocument {
$user = $router->getContext()->getUser();
$template = $user ? "admin.twig" : "redirect.twig";
$params = $user ? [] : ["url" => "/login"];
+ $this->title = "Administration";
+ $this->searchable = false;
parent::__construct($router, $template, $params);
$this->enableCSP();
}
diff --git a/core/Documents/Info.class.php b/core/Documents/Info.class.php
index d775b6d..ea731fd 100644
--- a/core/Documents/Info.class.php
+++ b/core/Documents/Info.class.php
@@ -10,6 +10,7 @@ use Objects\Router\Router;
class Info extends HtmlDocument {
public function __construct(Router $router) {
parent::__construct($router, EmptyHead::class, InfoBody::class);
+ $this->searchable = false;
}
}
diff --git a/core/Elements/Document.class.php b/core/Elements/Document.class.php
index 52099d7..1a0f95a 100644
--- a/core/Elements/Document.class.php
+++ b/core/Elements/Document.class.php
@@ -6,8 +6,13 @@ use Configuration\Settings;
use Driver\Logger\Logger;
use Driver\SQL\SQL;
use Objects\Context;
+use Objects\lang\LanguageModule;
+use Objects\Router\DocumentRoute;
use Objects\Router\Router;
use Objects\DatabaseEntity\User;
+use Objects\Search\Searchable;
+use Objects\Search\SearchQuery;
+use Objects\Search\SearchResult;
abstract class Document {
@@ -18,6 +23,7 @@ abstract class Document {
private ?string $cspNonce;
private array $cspWhitelist;
private string $domain;
+ protected bool $searchable;
public function __construct(Router $router) {
$this->router = $router;
@@ -27,6 +33,13 @@ abstract class Document {
$this->cspWhitelist = [];
$this->domain = $this->getSettings()->getBaseUrl();
$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 {
@@ -66,30 +79,29 @@ abstract class Document {
return $this->router;
}
- protected function addCSPWhitelist(string $path) {
- $this->cspWhitelist[] = $this->domain . $path;
+ public function addCSPWhitelist(string $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 {
- if ($this->databaseRequired) {
- $sql = $this->getSQL();
- if (is_null($sql)) {
- die("Database is not configured yet.");
- } else if (!$sql->isConnected()) {
- die("Database is not connected: " . $sql->getLastError());
- }
- }
+ public function loadLanguageModule(LanguageModule|string $module) {
+ $language = $this->getContext()->getLanguage();
+ $language->loadModule($module);
+ }
+ public function sendHeaders() {
if ($this->cspEnabled) {
-
$cspWhiteList = implode(" ", $this->cspWhitelist);
-
$csp = [
- "default-src 'self'",
+ "default-src $cspWhiteList 'self'",
"object-src 'none'",
"base-uri 'self'",
"style-src 'self' 'unsafe-inline'",
- "img-src 'self' data:",
+ "img-src 'self' 'unsafe-inline' data: https:;",
"script-src $cspWhiteList 'nonce-$this->cspNonce'"
];
if ($this->getSettings()->isRecaptchaEnabled()) {
@@ -99,7 +111,42 @@ abstract class Document {
$compiledCSP = implode("; ", $csp);
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;
}
}
\ No newline at end of file
diff --git a/core/Elements/HtmlDocument.class.php b/core/Elements/HtmlDocument.class.php
index 035f51c..cec2ce6 100644
--- a/core/Elements/HtmlDocument.class.php
+++ b/core/Elements/HtmlDocument.class.php
@@ -6,8 +6,8 @@ use Objects\Router\Router;
class HtmlDocument extends Document {
- protected Head $head;
- protected Body $body;
+ protected ?Head $head;
+ protected ?Body $body;
private ?string $activeView;
public function __construct(Router $router, $headClass, $bodyClass, ?string $view = NULL) {
@@ -17,8 +17,8 @@ class HtmlDocument extends Document {
$this->activeView = $view;
}
- public function getHead(): Head { return $this->head; }
- public function getBody(): Body { return $this->body; }
+ public function getHead(): ?Head { return $this->head; }
+ public function getBody(): ?Body { return $this->body; }
public function getView() : ?View {
@@ -35,24 +35,11 @@ class HtmlDocument extends Document {
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 {
return $this->activeView;
}
function getCode(array $params = []): string {
-
- parent::getCode();
-
// generate body first, so we can modify head
$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";
+ }
}
\ No newline at end of file
diff --git a/core/Elements/TemplateDocument.class.php b/core/Elements/TemplateDocument.class.php
index b9756e4..77004a9 100644
--- a/core/Elements/TemplateDocument.class.php
+++ b/core/Elements/TemplateDocument.class.php
@@ -2,7 +2,12 @@
namespace Elements;
+use Objects\CustomTwigFunctions;
+use Objects\Router\DocumentRoute;
use Objects\Router\Router;
+use Objects\Search\Searchable;
+use Objects\Search\SearchQuery;
+use Objects\Search\SearchResult;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
@@ -11,6 +16,8 @@ use Twig\Loader\FilesystemLoader;
class TemplateDocument extends Document {
+ const TEMPLATE_PATH = WEBROOT . '/core/Templates';
+
private string $templateName;
protected array $parameters;
private Environment $twigEnvironment;
@@ -19,14 +26,19 @@ class TemplateDocument extends Document {
public function __construct(Router $router, string $templateName, array $params = []) {
parent::__construct($router);
- $this->title = "";
+ $this->title = "Untitled Document";
$this->templateName = $templateName;
$this->parameters = $params;
- $this->twigLoader = new FilesystemLoader(WEBROOT . '/core/Templates');
+ $this->twigLoader = new FilesystemLoader(self::TEMPLATE_PATH);
$this->twigEnvironment = new Environment($this->twigLoader, [
'cache' => WEBROOT . '/core/Cache/Templates/',
'auto_reload' => true
]);
+ $this->twigEnvironment->addExtension(new CustomTwigFunctions());
+ }
+
+ public function getTitle(): string {
+ return $this->title;
}
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 {
- parent::getCode($params);
$this->loadParameters();
return $this->renderTemplate($this->templateName, $this->parameters);
}
@@ -48,33 +63,62 @@ class TemplateDocument extends Document {
$context = $this->getContext();
$session = $context->getSession();
- $params["user"] = [
- "lang" => $context->getLanguage()->getShortCode(),
- "loggedIn" => $session !== null,
- "session" => ($session ? [
- "csrfToken" => $session->getCsrfToken()
- ] : null)
- ];
-
$settings = $this->getSettings();
- $params["site"] = [
- "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()
- ]
- ];
+ $language = $context->getLanguage();
+ $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);
} catch (LoaderError | RuntimeError | SyntaxError $e) {
return "Error rendering twig template: " . htmlspecialchars($e->getMessage()) . "";
}
}
+
+ 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);
+ }
}
\ No newline at end of file
diff --git a/core/Elements/TemplateView.class.php b/core/Elements/TemplateView.class.php
new file mode 100644
index 0000000..fc8adc4
--- /dev/null
+++ b/core/Elements/TemplateView.class.php
@@ -0,0 +1,39 @@
+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();
+ }
+}
\ No newline at end of file
diff --git a/core/Elements/View.class.php b/core/Elements/View.class.php
index 42f8eaa..e8727ad 100644
--- a/core/Elements/View.class.php
+++ b/core/Elements/View.class.php
@@ -6,13 +6,11 @@ abstract class View extends StaticView {
private Document $document;
private bool $loadView;
- protected bool $searchable;
protected string $title;
protected array $langModules;
public function __construct(Document $document, bool $loadView = true) {
$this->document = $document;
- $this->searchable = false;
$this->title = "Untitled View";
$this->langModules = array();
$this->loadView = $loadView;
@@ -20,7 +18,6 @@ abstract class View extends StaticView {
public function getTitle(): string { return $this->title; }
public function getDocument(): Document { return $this->document; }
- public function isSearchable(): bool { return $this->searchable; }
public function getSiteName(): string {
return $this->getDocument()->getSettings()->getSiteName();
diff --git a/core/External/composer.json b/core/External/composer.json
index fdbb8b0..6a14ac7 100644
--- a/core/External/composer.json
+++ b/core/External/composer.json
@@ -6,7 +6,8 @@
"christian-riesen/base32": "^1.6",
"spomky-labs/cbor-php": "2.1.0",
"web-auth/cose-lib": "3.3.12",
- "firebase/php-jwt": "^6.2"
+ "firebase/php-jwt": "^6.2",
+ "html2text/html2text": "^4.3"
},
"require-dev": {
"phpunit/phpunit": "^9.5"
diff --git a/core/Objects/Context.class.php b/core/Objects/Context.class.php
index d81320b..68a25cc 100644
--- a/core/Objects/Context.class.php
+++ b/core/Objects/Context.class.php
@@ -12,6 +12,7 @@ use Firebase\JWT\JWT;
use Objects\DatabaseEntity\Language;
use Objects\DatabaseEntity\Session;
use Objects\DatabaseEntity\User;
+use Objects\Router\Router;
class Context {
@@ -20,12 +21,14 @@ class Context {
private ?User $user;
private Configuration $configuration;
private Language $language;
+ public ?Router $router;
public function __construct() {
$this->sql = null;
$this->session = null;
$this->user = null;
+ $this->router = null;
$this->configuration = new Configuration();
$this->setLanguage(Language::DEFAULT_LANGUAGE());
diff --git a/core/Objects/CustomTwigFunctions.class.php b/core/Objects/CustomTwigFunctions.class.php
new file mode 100644
index 0000000..5399993
--- /dev/null
+++ b/core/Objects/CustomTwigFunctions.class.php
@@ -0,0 +1,18 @@
+insert($this);
+ if ($res === false) {
+ return false;
+ } else if ($this->id === null) {
+ $this->id = $res;
+ }
+
+ return true;
+ }
+
public function delete(SQL $sql): bool {
$handler = self::getHandler($sql);
if ($this->id === null) {
diff --git a/core/Objects/DatabaseEntity/DatabaseEntityHandler.php b/core/Objects/DatabaseEntity/DatabaseEntityHandler.php
index 9b50063..12d2366 100644
--- a/core/Objects/DatabaseEntity/DatabaseEntityHandler.php
+++ b/core/Objects/DatabaseEntity/DatabaseEntityHandler.php
@@ -311,10 +311,7 @@ class DatabaseEntityHandler {
return $query->execute();
}
- public function insertOrUpdate(DatabaseEntity $entity, ?array $columns = null) {
- $id = $entity->getId();
- $action = $id === null ? "insert" : "update";
-
+ private function prepareRow(DatabaseEntity $entity, string $action, ?array $columns = null) {
$row = [];
foreach ($this->columns as $propertyName => $column) {
if ($columns && !in_array($column->getName(), $columns)) {
@@ -331,7 +328,10 @@ class DatabaseEntityHandler {
} else if (!$this->columns[$propertyName]->notNull()) {
$value = null;
} 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.");
return false;
} else {
@@ -342,29 +342,62 @@ class DatabaseEntityHandler {
$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);
+ // insert with id?
+ $entityId = $entity->getId();
+ if ($entityId !== null) {
+ $row["id"] = $entityId;
+ }
- if ($id === null) {
- $res = $this->sql->insert($this->tableName, array_keys($row))
- ->addRow(...array_values($row))
- ->returning("id")
- ->execute();
+ $query = $this->sql->insert($this->tableName, array_keys($row))
+ ->addRow(...array_values($row));
- if ($res !== false) {
- return $this->sql->getLastInsertId();
- } else {
- return false;
- }
+ // return id if its auto-generated
+ if ($entityId === null) {
+ $query->returning("id");
+ }
+
+ $res = $query->execute();
+ if ($res !== false) {
+ return $this->sql->getLastInsertId();
} else {
- $query = $this->sql->update($this->tableName)
- ->where(new Compare($this->tableName . ".id", $id));
+ return false;
+ }
+ }
- foreach ($row as $columnName => $value) {
- $query->set($columnName, $value);
- }
-
- return $query->execute();
+ public function insertOrUpdate(DatabaseEntity $entity, ?array $columns = null) {
+ $id = $entity->getId();
+ if ($id === null) {
+ return $this->insert($entity);
+ } else {
+ return $this->update($entity, $columns);
}
}
diff --git a/core/Objects/DatabaseEntity/Language.class.php b/core/Objects/DatabaseEntity/Language.class.php
index 99b7b4a..de68a85 100644
--- a/core/Objects/DatabaseEntity/Language.class.php
+++ b/core/Objects/DatabaseEntity/Language.class.php
@@ -2,6 +2,7 @@
namespace Objects\DatabaseEntity {
+ use Driver\SQL\SQL;
use Objects\DatabaseEntity\Attribute\MaxLength;
use Objects\DatabaseEntity\Attribute\Transient;
use Objects\lang\LanguageModule;
@@ -14,15 +15,13 @@ namespace Objects\DatabaseEntity {
#[MaxLength(5)] private string $code;
#[MaxLength(32)] private string $name;
- #[Transient] private array $modules;
- #[Transient] protected array $entries;
+ #[Transient] private array $modules = [];
+ #[Transient] protected array $entries = [];
public function __construct(int $id, string $code, string $name) {
parent::__construct($id);
$this->code = $code;
$this->name = $name;
- $this->entries = array();
- $this->modules = array();
}
public function getCode(): string {
@@ -42,9 +41,11 @@ namespace Objects\DatabaseEntity {
$module = new $module();
}
- $moduleEntries = $module->getEntries($this->code);
- $this->entries = array_merge($this->entries, $moduleEntries);
- $this->modules[] = $module;
+ if (!in_array($module, $this->modules)) {
+ $moduleEntries = $module->getEntries($this->code);
+ $this->entries = array_merge($this->entries, $moduleEntries);
+ $this->modules[] = $module;
+ }
}
public function translate(string $key): string {
@@ -89,6 +90,10 @@ namespace Objects\DatabaseEntity {
return new Language(1, "en_US", "American English");
}
+
+ public function getEntries(): array {
+ return $this->entries;
+ }
}
}
diff --git a/core/Objects/DatabaseEntity/Session.class.php b/core/Objects/DatabaseEntity/Session.class.php
index 9bc5657..400376e 100644
--- a/core/Objects/DatabaseEntity/Session.class.php
+++ b/core/Objects/DatabaseEntity/Session.class.php
@@ -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 {
session_destroy();
$this->active = false;
diff --git a/core/Objects/Router/AbstractRoute.class.php b/core/Objects/Router/AbstractRoute.class.php
index c142d33..19c0bc1 100644
--- a/core/Objects/Router/AbstractRoute.class.php
+++ b/core/Objects/Router/AbstractRoute.class.php
@@ -6,6 +6,8 @@ use Api\Parameter\Parameter;
abstract class AbstractRoute {
+ const PARAMETER_PATTERN = "/^{([^:]+)(:(.*?)(\?)?)?}$/";
+
private string $pattern;
private bool $exact;
@@ -64,7 +66,7 @@ abstract class AbstractRoute {
$params = [];
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
if ($urlOffset >= $countUrl || $urlParts[$urlOffset] !== $patternParts[$patternOffset]) {
@@ -122,4 +124,30 @@ abstract class AbstractRoute {
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;
+ }
}
\ No newline at end of file
diff --git a/core/Objects/Router/ApiRoute.class.php b/core/Objects/Router/ApiRoute.class.php
index 7e1b715..187e1cc 100644
--- a/core/Objects/Router/ApiRoute.class.php
+++ b/core/Objects/Router/ApiRoute.class.php
@@ -21,7 +21,7 @@ class ApiRoute extends AbstractRoute {
if (empty($params["endpoint"])) {
header("Content-Type: text/html");
$document = new \Elements\TemplateDocument($router, "swagger.twig");
- return $document->getCode();
+ return $document->load();
} else if (!preg_match("/[a-zA-Z]+/", $params["endpoint"])) {
http_response_code(400);
$response = createError("Invalid Method");
diff --git a/core/Objects/Router/DocumentRoute.class.php b/core/Objects/Router/DocumentRoute.class.php
index a24af64..0a3b730 100644
--- a/core/Objects/Router/DocumentRoute.class.php
+++ b/core/Objects/Router/DocumentRoute.class.php
@@ -3,10 +3,15 @@
namespace Objects\Router;
use Elements\Document;
+use Objects\Context;
+use Objects\Search\Searchable;
+use Objects\Search\SearchQuery;
use ReflectionException;
class DocumentRoute extends AbstractRoute {
+ use Searchable;
+
private string $className;
private array $args;
private ?\ReflectionClass $reflectionClass;
@@ -31,7 +36,7 @@ class DocumentRoute extends AbstractRoute {
}
} catch (ReflectionException $exception) {
$this->reflectionClass = null;
- return false;
+ throw $exception;
}
$this->reflectionClass = null;
@@ -55,16 +60,32 @@ class DocumentRoute extends AbstractRoute {
}
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], $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);
- return $document->getCode($params);
+ return $document->load($params);
} catch (\ReflectionException $e) {
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 [];
+ }
+ }
}
\ No newline at end of file
diff --git a/core/Objects/Router/Router.class.php b/core/Objects/Router/Router.class.php
index 5bf3c25..facb057 100644
--- a/core/Objects/Router/Router.class.php
+++ b/core/Objects/Router/Router.class.php
@@ -9,6 +9,8 @@ class Router {
private Context $context;
private Logger $logger;
+ private ?AbstractRoute $activeRoute;
+ private ?string $requestedUri;
protected array $routes;
protected array $statusCodeRoutes;
@@ -16,6 +18,9 @@ class Router {
$this->context = $context;
$this->routes = [];
$this->statusCodeRoutes = [];
+ $this->activeRoute = null;
+ $this->requestedUri = null;
+ $this->context->router = $this;
$sql = $context->getSQL();
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 {
// TODO: do we want a global try cache and return status page 500 on any error?
+ $this->requestedUri = $url;
$url = strtok($url, "?");
foreach ($this->routes as $route) {
$pathParams = $route->match($url);
if ($pathParams !== false) {
+ $this->activeRoute = $route;
return $route->call($this, $pathParams);
}
}
@@ -54,7 +69,6 @@ class Router {
if ($res) {
return $req->getResult()["html"];
} else {
- var_dump($req->getLastError());
$description = htmlspecialchars($params["status_description"]);
return "$code - $description";
}
@@ -62,11 +76,11 @@ class Router {
}
public function addRoute(AbstractRoute $route) {
- if (preg_match("/^\d+$/", $route->getPattern())) {
- $this->statusCodeRoutes[$route->getPattern()] = $route;
- } else {
- $this->routes[] = $route;
+ if (preg_match("/^\/(\d+)$/", $route->getPattern(), $re)) {
+ $this->statusCodeRoutes[$re[1]] = $route;
}
+
+ $this->routes[] = $route;
}
public function writeCache(string $file): bool {
@@ -134,4 +148,15 @@ class RouterCache extends Router {
// strip leading slash
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;
+ }
}
\ No newline at end of file
diff --git a/core/Objects/Router/StaticFileRoute.class.php b/core/Objects/Router/StaticFileRoute.class.php
index bc58eda..a6e58ba 100644
--- a/core/Objects/Router/StaticFileRoute.class.php
+++ b/core/Objects/Router/StaticFileRoute.class.php
@@ -2,8 +2,15 @@
namespace Objects\Router;
+use Objects\Context;
+use Objects\Search\Searchable;
+use Objects\Search\SearchQuery;
+use Objects\Search\SearchResult;
+
class StaticFileRoute extends AbstractRoute {
+ use Searchable;
+
private string $path;
private int $code;
@@ -15,7 +22,7 @@ class StaticFileRoute extends AbstractRoute {
public function call(Router $router, array $params): string {
http_response_code($this->code);
- $this->serveStatic($this->path, $router);
+ $this->serveStatic($this->getAbsolutePath(), $router);
return "";
}
@@ -23,9 +30,11 @@ class StaticFileRoute extends AbstractRoute {
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)) {
http_response_code(406);
echo "Access restricted, requested file outside web root: " . htmlspecialchars($path);
@@ -72,4 +81,34 @@ class StaticFileRoute extends AbstractRoute {
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;
+ }
}
\ No newline at end of file
diff --git a/core/Objects/Search/SearchQuery.class.php b/core/Objects/Search/SearchQuery.class.php
new file mode 100644
index 0000000..16bd172
--- /dev/null
+++ b/core/Objects/Search/SearchQuery.class.php
@@ -0,0 +1,19 @@
+query = $query;
+ $this->parts = array_unique(array_filter(explode(" ", strtolower($query))));
+ }
+
+ public function getQuery(): string {
+ return $this->query;
+ }
+
+}
\ No newline at end of file
diff --git a/core/Objects/Search/SearchResult.class.php b/core/Objects/Search/SearchResult.class.php
new file mode 100644
index 0000000..84ce47f
--- /dev/null
+++ b/core/Objects/Search/SearchResult.class.php
@@ -0,0 +1,30 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/core/Objects/Search/Searchable.trait.php b/core/Objects/Search/Searchable.trait.php
new file mode 100644
index 0000000..121e1b3
--- /dev/null
+++ b/core/Objects/Search/Searchable.trait.php
@@ -0,0 +1,91 @@
+ $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", "\$0", $extract);
+ }
+}
\ No newline at end of file
diff --git a/core/core.php b/core/core.php
index 23baeeb..d180af3 100644
--- a/core/core.php
+++ b/core/core.php
@@ -10,7 +10,7 @@ if (is_file($autoLoad)) {
require_once $autoLoad;
}
-define("WEBBASE_VERSION", "2.0.0-alpha");
+define("WEBBASE_VERSION", "2.1.0");
spl_autoload_extensions(".php");
spl_autoload_register(function ($class) {
diff --git a/index.php b/index.php
index 2043ca7..c6fd99b 100644
--- a/index.php
+++ b/index.php
@@ -35,7 +35,7 @@ if ($installation) {
header("Location: /");
} else {
$document = new Documents\Install(new Router($context));
- $response = $document->getCode();
+ $response = $document->load();
}
} else {
diff --git a/js/script.js b/js/script.js
index 02f00c9..ab8da20 100644
--- a/js/script.js
+++ b/js/script.js
@@ -41,7 +41,7 @@ let Core = function () {
};
this.getParameters = function () {
- return this.aParameters;
+ return this.parameters;
};
this.setTitle = function (title) {
@@ -71,27 +71,27 @@ let Core = function () {
};
this.removeParameter = function (param) {
- if (typeof this.aParameters[param] !== 'undefined' && this.aParameters.hasOwnProperty(param)) {
- delete this.aParameters[param];
+ if (typeof this.parameters[param] !== 'undefined' && this.parameters.hasOwnProperty(param)) {
+ delete this.parameters[param];
}
this.updateUrl();
};
this.getParameter = function (param) {
- if (typeof this.aParameters[param] !== 'undefined' && this.aParameters.hasOwnProperty(param))
- return this.aParameters[param];
+ if (typeof this.parameters[param] !== 'undefined' && this.parameters.hasOwnProperty(param))
+ return this.parameters[param];
else
return null;
};
this.setParameter = function (param, newvalue) {
newvalue = typeof newvalue !== 'undefined' ? newvalue : '';
- this.aParameters[param] = newvalue;
+ this.parameters[param] = newvalue;
this.updateUrl();
};
this.parseParameters = function () {
- this.aParameters = [];
+ this.parameters = [];
if (this.url.indexOf('?') === -1)
return;
@@ -103,30 +103,32 @@ let Core = function () {
if (index !== -1) {
var key = param.substr(0, index);
var val = param.substr(index + 1);
- this.aParameters[key] = val;
+ this.parameters[key] = val;
} else
- this.aParameters[param] = '';
+ this.parameters[param] = '';
}
};
this.updateUrl = function () {
this.clearUrl();
let i = 0;
- for (var parameter in this.aParameters) {
+ for (var parameter in this.parameters) {
this.url += (i === 0 ? "?" : "&") + parameter;
- if (this.aParameters.hasOwnProperty(parameter) && this.aParameters[parameter].toString().length > 0) {
- this.url += "=" + this.aParameters[parameter];
+ if (this.parameters.hasOwnProperty(parameter) && this.parameters[parameter].toString().length > 0) {
+ this.url += "=" + this.parameters[parameter];
}
i++;
}
};
this.clearParameters = function () {
- this.aParameters = [];
+ this.parameters = [];
this.updateUrl();
};
this.clearUrl = function () {
+ if (this.url.indexOf('#') !== -1)
+ this.url = this.url.substring(0, this.url.indexOf('#'));
if (this.url.indexOf('?') !== -1)
this.url = this.url.substring(0, this.url.indexOf('?'));
};