Twig, Tests, AES,

This commit is contained in:
2021-12-08 16:53:43 +01:00
parent 25d47f7528
commit 918244125c
74 changed files with 5350 additions and 1515 deletions

View File

@@ -2,69 +2,64 @@
namespace Elements;
use Driver\SQL\SQL;
use Objects\User;
abstract class Document {
protected Head $head;
protected Body $body;
protected User $user;
protected bool $databaseRequired;
private ?string $activeView;
private bool $cspEnabled;
private ?string $cspNonce;
public function __construct(User $user, $headClass, $bodyClass, ?string $view = NULL) {
public function __construct(User $user) {
$this->user = $user;
$this->head = new $headClass($this);
$this->body = new $bodyClass($this);
$this->cspEnabled = false;
$this->cspNonce = null;
$this->databaseRequired = true;
$this->activeView = $view;
}
public function getHead(): Head { return $this->head; }
public function getBody(): Body { return $this->body; }
public function getSQL(): ?\Driver\SQL\SQL { return $this->user->getSQL(); }
public function getUser(): User { return $this->user; }
public function getView() : ?View {
if ($this->activeView === null) {
return null;
}
$view = parseClass($this->activeView);
$file = getClassPath($view);
if(!file_exists($file) || !is_subclass_of($view, View::class)) {
return null;
}
return new $view($this);
public function getSQL(): ?SQL {
return $this->user->getSQL();
}
public function getRequestedView(): string {
return $this->activeView;
public function getUser(): User {
return $this->user;
}
function getCode(): string {
public function getCSPNonce(): ?string {
return $this->cspNonce;
}
public function isCSPEnabled(): bool {
return $this->cspEnabled;
}
public function enableCSP() {
$this->cspEnabled = true;
$this->cspNonce = generateRandomString(16, "base62");
}
public function getCode(): string {
if ($this->databaseRequired) {
$sql = $this->user->getSQL();
if (is_null($sql)) {
die("Database is not configured yet.");
} else if(!$sql->isConnected()) {
} else if (!$sql->isConnected()) {
die("Database is not connected: " . $sql->getLastError());
}
}
$body = $this->body->getCode();
$head = $this->head->getCode();
$lang = $this->user->getLanguage()->getShortCode();
if ($this->cspEnabled) {
$csp = ["default-src 'self'", "object-src 'none'", "base-uri 'self'", "style-src 'self' 'unsafe-inline'", "script-src 'nonce-$this->cspNonce'"];
if ($this->user->getConfiguration()->getSettings()->isRecaptchaEnabled()) {
$csp[] = "frame-src https://www.google.com/ 'self'";
}
$html = "<!DOCTYPE html>";
$html .= "<html lang=\"$lang\">";
$html .= $head;
$html .= $body;
$html .= "</html>";
return $html;
$compiledCSP = implode(";", $csp);
header("Content-Security-Policy: $compiledCSP;");
}
return "";
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace Elements;
use Objects\User;
class HtmlDocument extends Document {
protected Head $head;
protected Body $body;
private ?string $activeView;
public function __construct(User $user, $headClass, $bodyClass, ?string $view = NULL) {
parent::__construct($user);
$this->head = $headClass ? new $headClass($this) : null;
$this->body = $bodyClass ? new $bodyClass($this) : null;
$this->activeView = $view;
}
public function getHead(): Head { return $this->head; }
public function getBody(): Body { return $this->body; }
public function getView() : ?View {
if ($this->activeView === null) {
return null;
}
$view = parseClass($this->activeView);
$file = getClassPath($view);
if(!file_exists($file) || !is_subclass_of($view, View::class)) {
return null;
}
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(): string {
parent::getCode();
// generate body first, so we can modify head
$body = $this->body->getCode();
if ($this->isCSPEnabled()) {
foreach ($this->head->getSources() as $element) {
if ($element instanceof Script || $element instanceof Link) {
$element->setNonce($this->getCSPNonce());
}
}
}
$head = $this->head->getCode();
$lang = $this->user->getLanguage()->getShortCode();
$html = "<!DOCTYPE html>";
$html .= "<html lang=\"$lang\">";
$html .= $head;
$html .= $body;
$html .= "</html>";
return $html;
}
}

View File

@@ -15,15 +15,30 @@ class Link extends StaticView {
private string $type;
private string $rel;
private string $href;
private ?string $nonce;
function __construct($rel, $href, $type = "") {
$this->href = $href;
$this->type = $type;
$this->rel = $rel;
$this->nonce = null;
}
function getCode(): string {
$type = (empty($this->type) ? "" : " type=\"$this->type\"");
return "<link rel=\"$this->rel\" href=\"$this->href\"$type/>";
$attributes = ["rel" => $this->rel, "href" => $this->href];
if (!empty($this->type)) {
$attributes["type"] = $this->type;
}
if (!empty($this->nonce)) {
$attributes["nonce"] = $this->nonce;
}
$attributes = html_attributes($attributes);
return "<link $attributes/>";
}
public function setNonce(string $nonce) {
$this->nonce = $nonce;
}
}

View File

@@ -11,21 +11,35 @@ class Script extends StaticView {
const INSTALL = "/js/install.js";
const BOOTSTRAP = "/js/bootstrap.bundle.min.js";
const ACCOUNT = "/js/account.js";
const SECLAB = "/js/seclab.min.js";
const FONTAWESOME = "/js/fontawesome-all.min.js";
private string $type;
private string $content;
private string $src;
private ?string $nonce;
function __construct($type, $src, $content = "") {
$this->src = $src;
$this->type = $type;
$this->content = $content;
$this->nonce = null;
}
function getCode(): string {
$src = (empty($this->src) ? "" : " src=\"$this->src\"");
return "<script type=\"$this->type\"$src>$this->content</script>";
$attributes = ["type" => $this->type];
if (!empty($this->src)) {
$attributes["src"] = $this->src;
}
if (!empty($this->nonce)) {
$attributes["nonce"] = $this->nonce;
}
$attributes = html_attributes($attributes);
return "<script $attributes>$this->content</script>";
}
public function setNonce(string $nonce) {
$this->nonce = $nonce;
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Elements;
use Objects\User;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
use Twig\Loader\FilesystemLoader;
class TemplateDocument extends Document {
private string $templateName;
protected array $parameters;
private Environment $twigEnvironment;
private FilesystemLoader $twigLoader;
public function __construct(User $user, string $templateName, array $initialParameters = []) {
parent::__construct($user);
$this->templateName = $templateName;
$this->parameters = $initialParameters;
$this->twigLoader = new FilesystemLoader(WEBROOT . '/core/Templates');
$this->twigEnvironment = new Environment($this->twigLoader, [
'cache' => WEBROOT . '/core/TemplateCache',
'auto_reload' => true
]);
}
protected function getTemplateName(): string {
return $this->templateName;
}
protected function loadParameters() {
}
public function getCode(): string {
parent::getCode();
$this->loadParameters();
return $this->renderTemplate($this->templateName, $this->parameters);
}
public function renderTemplate(string $name, array $params = []): string {
try {
$params["user"] = [
"lang" => $this->user->getLanguage()->getShortCode(),
"loggedIn" => $this->user->isLoggedIn(),
];
$settings = $this->user->getConfiguration()->getSettings();
$params["site"] = [
"name" => $settings->getSiteName(),
"baseUrl" => $settings->getBaseUrl(),
"recaptcha" => [
"key" => $settings->isRecaptchaEnabled() ? $settings->getRecaptchaSiteKey() : null,
"enabled" => $settings->isRecaptchaEnabled(),
],
"csp" => [
"nonce" => $this->getCSPNonce(),
"enabled" => $this->isCSPEnabled()
]
];
return $this->twigEnvironment->render($name, $params);
} catch (LoaderError | RuntimeError | SyntaxError $e) {
return "<b>Error rendering twig template: " . htmlspecialchars($e->getMessage()) . "</b>";
}
}
}