security.txt + bugfixes
This commit is contained in:
parent
5acd13b945
commit
c8965e209b
@ -227,9 +227,9 @@ namespace Core\API\Mail {
|
|||||||
|
|
||||||
$sql = $this->context->getSQL();
|
$sql = $this->context->getSQL();
|
||||||
$mailQueueItems = MailQueueItem::findBy(MailQueueItem::createBuilder($sql, false)
|
$mailQueueItems = MailQueueItem::findBy(MailQueueItem::createBuilder($sql, false)
|
||||||
->whereGt("retryCount", 0)
|
->whereGt("retry_count", 0)
|
||||||
->whereEq("status", "waiting")
|
->whereEq("status", "waiting")
|
||||||
->where(new Compare("nextTry", $sql->now(), "<=")));
|
->where(new Compare("next_try", $sql->now(), "<=")));
|
||||||
|
|
||||||
$this->success = ($mailQueueItems !== false);
|
$this->success = ($mailQueueItems !== false);
|
||||||
$this->lastError = $sql->getLastError();
|
$this->lastError = $sql->getLastError();
|
||||||
|
@ -68,6 +68,7 @@ class Swagger extends Request {
|
|||||||
foreach (self::getApiEndpoints() as $endpoint => $apiClass) {
|
foreach (self::getApiEndpoints() as $endpoint => $apiClass) {
|
||||||
$body = null;
|
$body = null;
|
||||||
$requiredProperties = [];
|
$requiredProperties = [];
|
||||||
|
$endpoint = "/$endpoint";
|
||||||
$apiObject = $apiClass->newInstance($this->context, false);
|
$apiObject = $apiClass->newInstance($this->context, false);
|
||||||
if (!$this->canView($permissions[strtolower($endpoint)] ?? [], $apiObject)) {
|
if (!$this->canView($permissions[strtolower($endpoint)] ?? [], $apiObject)) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -222,7 +222,7 @@ namespace Core\API\User {
|
|||||||
|
|
||||||
public function __construct(Context $context, $externalCall = false) {
|
public function __construct(Context $context, $externalCall = false) {
|
||||||
parent::__construct($context, $externalCall,
|
parent::__construct($context, $externalCall,
|
||||||
self::getPaginationParameters(['id', 'name', 'email', 'groups', 'registeredAt'],
|
self::getPaginationParameters(['id', 'name', 'fullName', 'email', 'groups', 'registeredAt', 'confirmed'],
|
||||||
'id', 'asc')
|
'id', 'asc')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -341,7 +341,9 @@ namespace Core\API\User {
|
|||||||
$this->result["loggedIn"] = true;
|
$this->result["loggedIn"] = true;
|
||||||
$userGroups = array_keys($currentUser->getGroups());
|
$userGroups = array_keys($currentUser->getGroups());
|
||||||
$this->result["user"] = $currentUser->jsonSerialize();
|
$this->result["user"] = $currentUser->jsonSerialize();
|
||||||
$this->result["session"] = $this->context->getSession()->jsonSerialize();
|
$this->result["session"] = $this->context->getSession()->jsonSerialize([
|
||||||
|
"id", "expires", "stayLoggedIn", "data", "csrfToken"
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$sql = $this->context->getSQL();
|
$sql = $this->context->getSQL();
|
||||||
@ -1022,7 +1024,7 @@ namespace Core\API\User {
|
|||||||
|
|
||||||
$userToken = UserToken::findBy(UserToken::createBuilder($sql, true)
|
$userToken = UserToken::findBy(UserToken::createBuilder($sql, true)
|
||||||
->whereFalse("used")
|
->whereFalse("used")
|
||||||
->whereEq("tokenType", UserToken::TYPE_EMAIL_CONFIRM)
|
->whereEq("token_type", UserToken::TYPE_EMAIL_CONFIRM)
|
||||||
->whereEq("user_id", $user->getId()));
|
->whereEq("user_id", $user->getId()));
|
||||||
|
|
||||||
$validHours = 48;
|
$validHours = 48;
|
||||||
|
@ -85,7 +85,6 @@ class CreateDatabase extends DatabaseScript {
|
|||||||
$method = "$className::getHandler";
|
$method = "$className::getHandler";
|
||||||
$handler = call_user_func($method, $sql, null, true);
|
$handler = call_user_func($method, $sql, null, true);
|
||||||
$persistables[$handler->getTableName()] = $handler;
|
$persistables[$handler->getTableName()] = $handler;
|
||||||
|
|
||||||
foreach ($handler->getNMRelations() as $nmTableName => $nmRelation) {
|
foreach ($handler->getNMRelations() as $nmTableName => $nmRelation) {
|
||||||
$persistables[$nmTableName] = $nmRelation;
|
$persistables[$nmTableName] = $nmRelation;
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,7 @@ class Settings {
|
|||||||
public static function loadDefaults(): Settings {
|
public static function loadDefaults(): Settings {
|
||||||
$hostname = $_SERVER["SERVER_NAME"] ?? null;
|
$hostname = $_SERVER["SERVER_NAME"] ?? null;
|
||||||
if (empty($hostname)) {
|
if (empty($hostname)) {
|
||||||
$hostname = $_SERVER["HTTP_HOST"];
|
$hostname = $_SERVER["HTTP_HOST"] ?? null;
|
||||||
if (empty($hostname)) {
|
if (empty($hostname)) {
|
||||||
$hostname = "localhost";
|
$hostname = "localhost";
|
||||||
}
|
}
|
||||||
|
99
Core/Documents/Security.class.php
Normal file
99
Core/Documents/Security.class.php
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Core\Documents;
|
||||||
|
|
||||||
|
|
||||||
|
use Core\Configuration\Settings;
|
||||||
|
use Core\Elements\Document;
|
||||||
|
use Core\Objects\DatabaseEntity\GpgKey;
|
||||||
|
use Core\Objects\DatabaseEntity\Language;
|
||||||
|
use Core\Objects\Router\Router;
|
||||||
|
|
||||||
|
// Source: https://www.rfc-editor.org/rfc/rfc9116
|
||||||
|
class Security extends Document {
|
||||||
|
|
||||||
|
public function __construct(Router $router) {
|
||||||
|
parent::__construct($router);
|
||||||
|
$this->searchable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string {
|
||||||
|
return "security.txt";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCode(array $params = []) {
|
||||||
|
|
||||||
|
$activeRoute = $this->router->getActiveRoute();
|
||||||
|
|
||||||
|
$sql = $this->getContext()->getSQL();
|
||||||
|
$settings = $this->getSettings();
|
||||||
|
$mailSettings = Settings::getAll($sql, "^mail_");
|
||||||
|
|
||||||
|
if ($activeRoute->getPattern() === "/.well-known/security.txt") {
|
||||||
|
|
||||||
|
// The order in which they appear is not an indication of priority; the listed languages are intended to have equal priority.
|
||||||
|
$languageCodes = implode(", ", array_map(function (Language $language) {
|
||||||
|
return $language->getShortCode();
|
||||||
|
}, Language::findAll($sql)));
|
||||||
|
|
||||||
|
$expires = (new \DateTime())->setTime(0, 0, 0)->modify("+3 months");
|
||||||
|
$baseUrl = $settings->getBaseUrl();
|
||||||
|
$gpgKey = null;
|
||||||
|
|
||||||
|
$lines = [
|
||||||
|
"# This project is based on the open-source framework hosted on https://github.com/rhergenreder/web-base",
|
||||||
|
"# Any non-site specific issues can be reported via the github security reporting feature:",
|
||||||
|
"# https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability",
|
||||||
|
"",
|
||||||
|
"Canonical: $baseUrl/.well-known/security.txt",
|
||||||
|
"Preferred-Languages: $languageCodes",
|
||||||
|
"Expires: " . $expires->format(\DateTime::ATOM),
|
||||||
|
"",
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isset($mailSettings["mail_contact"])) {
|
||||||
|
$lines[] = "Contact: " . $mailSettings["mail_contact"];
|
||||||
|
|
||||||
|
if (isset($mailSettings["mail_contact_gpg_key_id"])) {
|
||||||
|
$gpgKey = GpgKey::find($sql, $mailSettings["mail_contact_gpg_key_id"]);
|
||||||
|
if ($gpgKey) {
|
||||||
|
$lines[] = "Encryption: $baseUrl/.well-known/gpg-key.txt";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = implode("\n", $lines);
|
||||||
|
|
||||||
|
if ($gpgKey !== null) {
|
||||||
|
$res = GpgKey::sign($code, $gpgKey->getFingerprint());
|
||||||
|
if ($res["success"]) {
|
||||||
|
$code = $res["data"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $code;
|
||||||
|
} else if ($activeRoute->getPattern() === "/.well-known/gpg-key.txt") {
|
||||||
|
|
||||||
|
if (isset($mailSettings["mail_contact_gpg_key_id"])) {
|
||||||
|
$gpgKey = GpgKey::find($sql, $mailSettings["mail_contact_gpg_key_id"]);
|
||||||
|
if ($gpgKey !== null) {
|
||||||
|
header("Content-Type: text/plain");
|
||||||
|
$res = $gpgKey->_export(true);
|
||||||
|
if ($res["success"]) {
|
||||||
|
return $res["data"];
|
||||||
|
} else {
|
||||||
|
return "Error exporting public key: " . $res["msg"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code(404);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendHeaders(): void {
|
||||||
|
parent::sendHeaders();
|
||||||
|
header("Content-Type: text/plain");
|
||||||
|
}
|
||||||
|
}
|
@ -89,7 +89,7 @@ abstract class Document {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function sendHeaders() {
|
public function sendHeaders(): void {
|
||||||
if ($this->cspEnabled) {
|
if ($this->cspEnabled) {
|
||||||
$cspWhiteList = implode(" ", $this->cspWhitelist);
|
$cspWhiteList = implode(" ", $this->cspWhitelist);
|
||||||
$csp = [
|
$csp = [
|
||||||
@ -98,7 +98,8 @@ abstract class Document {
|
|||||||
"base-uri 'self'",
|
"base-uri 'self'",
|
||||||
"style-src 'self' 'unsafe-inline'",
|
"style-src 'self' 'unsafe-inline'",
|
||||||
"img-src 'self' 'unsafe-inline' data: https:;",
|
"img-src 'self' 'unsafe-inline' data: https:;",
|
||||||
"script-src $cspWhiteList 'nonce-$this->cspNonce'"
|
"script-src $cspWhiteList 'nonce-$this->cspNonce'",
|
||||||
|
"frame-ancestors 'self'",
|
||||||
];
|
];
|
||||||
if ($this->getSettings()->isRecaptchaEnabled()) {
|
if ($this->getSettings()->isRecaptchaEnabled()) {
|
||||||
$csp[] = "frame-src https://www.google.com/ 'self'";
|
$csp[] = "frame-src https://www.google.com/ 'self'";
|
||||||
@ -107,6 +108,15 @@ abstract class Document {
|
|||||||
$compiledCSP = implode("; ", $csp);
|
$compiledCSP = implode("; ", $csp);
|
||||||
header("Content-Security-Policy: $compiledCSP;");
|
header("Content-Security-Policy: $compiledCSP;");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// additional security headers
|
||||||
|
header("X-XSS-Protection: 0"); // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection
|
||||||
|
header("X-Content-Type-Options: nosniff");
|
||||||
|
|
||||||
|
if (getProtocol() === "https") {
|
||||||
|
$maxAge = 365 * 24 * 60 * 60; // 1 year in seconds
|
||||||
|
header("Strict-Transport-Security: max-age=$maxAge; includeSubDomains; preload");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract function getCode(array $params = []);
|
public abstract function getCode(array $params = []);
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
"title" => "Account",
|
"title" => "Account",
|
||||||
|
"users" => "Benutzer",
|
||||||
"login_title" => "Einloggen",
|
"login_title" => "Einloggen",
|
||||||
"login_description" => "Loggen Sie sich in Ihren Account ein",
|
"login_description" => "Loggen Sie sich in Ihren Account ein",
|
||||||
"accept_invite_title" => "Einladung",
|
"accept_invite_title" => "Einladung",
|
||||||
@ -32,6 +33,7 @@ return [
|
|||||||
"remember_me" => "Eingeloggt bleiben",
|
"remember_me" => "Eingeloggt bleiben",
|
||||||
"signing_in" => "Einloggen",
|
"signing_in" => "Einloggen",
|
||||||
"sign_in" => "Einloggen",
|
"sign_in" => "Einloggen",
|
||||||
|
"confirmed" => "Bestätigt",
|
||||||
"forgot_password" => "Passwort vergessen?",
|
"forgot_password" => "Passwort vergessen?",
|
||||||
"change_password" => "Passwort ändern",
|
"change_password" => "Passwort ändern",
|
||||||
"passwords_do_not_match" => "Die Passwörter stimmen nicht überein",
|
"passwords_do_not_match" => "Die Passwörter stimmen nicht überein",
|
||||||
@ -57,4 +59,6 @@ return [
|
|||||||
"error_profile_get" => "Fehler beim Laden des Benutzerprofils",
|
"error_profile_get" => "Fehler beim Laden des Benutzerprofils",
|
||||||
"registered_at" => "Registriert am",
|
"registered_at" => "Registriert am",
|
||||||
"last_online" => "Zuletzt online",
|
"last_online" => "Zuletzt online",
|
||||||
|
"groups" => "Gruppen",
|
||||||
|
"logged_in_as" => "Eingeloggt als",
|
||||||
];
|
];
|
@ -7,6 +7,7 @@ return [
|
|||||||
"go_back" => "Zurück",
|
"go_back" => "Zurück",
|
||||||
"save" => "Speichern",
|
"save" => "Speichern",
|
||||||
"save_only" => "Nur Speichern",
|
"save_only" => "Nur Speichern",
|
||||||
|
"saved" => "Gespeichert",
|
||||||
"saving" => "Speichere",
|
"saving" => "Speichere",
|
||||||
"unsaved_changes" => "Du hast nicht gespeicherte Änderungen",
|
"unsaved_changes" => "Du hast nicht gespeicherte Änderungen",
|
||||||
"new" => "Neu",
|
"new" => "Neu",
|
||||||
@ -19,6 +20,8 @@ return [
|
|||||||
"confirm" => "Bestätigen",
|
"confirm" => "Bestätigen",
|
||||||
"add" => "Hinzufügen",
|
"add" => "Hinzufügen",
|
||||||
"ok" => "OK",
|
"ok" => "OK",
|
||||||
|
"id" => "ID",
|
||||||
|
"user" => "Benutzer",
|
||||||
"language" => "Sprache",
|
"language" => "Sprache",
|
||||||
"loading" => "Laden",
|
"loading" => "Laden",
|
||||||
"logout" => "Ausloggen",
|
"logout" => "Ausloggen",
|
||||||
|
@ -10,6 +10,7 @@ return [
|
|||||||
"token" => "Token",
|
"token" => "Token",
|
||||||
"request_new_key" => "Neuen Schlüssel anfordern",
|
"request_new_key" => "Neuen Schlüssel anfordern",
|
||||||
"show_only_active_keys" => "Zeige nur aktive Schlüssel",
|
"show_only_active_keys" => "Zeige nur aktive Schlüssel",
|
||||||
|
"no_api_key_registered" => "Keine gültigen API-Schlüssel registriert",
|
||||||
|
|
||||||
# GPG Key
|
# GPG Key
|
||||||
"gpg_key_placeholder_text" => "GPG-Key im ASCII format reinziehen oder einfügen...",
|
"gpg_key_placeholder_text" => "GPG-Key im ASCII format reinziehen oder einfügen...",
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
"title" => "Account",
|
"title" => "Account",
|
||||||
|
"users" => "Users",
|
||||||
"login_title" => "Sign In",
|
"login_title" => "Sign In",
|
||||||
"login_description" => "Sign In into your account",
|
"login_description" => "Sign In into your account",
|
||||||
"accept_invite_title" => "Invitation",
|
"accept_invite_title" => "Invitation",
|
||||||
@ -32,6 +33,7 @@ return [
|
|||||||
"remember_me" => "Remember Me",
|
"remember_me" => "Remember Me",
|
||||||
"signing_in" => "Signing in",
|
"signing_in" => "Signing in",
|
||||||
"sign_in" => "Sign In",
|
"sign_in" => "Sign In",
|
||||||
|
"confirmed" => "Confirmed",
|
||||||
"forgot_password" => "Forgot password?",
|
"forgot_password" => "Forgot password?",
|
||||||
"change_password" => "Change password",
|
"change_password" => "Change password",
|
||||||
"register_text" => "Don't have an account? Sign Up",
|
"register_text" => "Don't have an account? Sign Up",
|
||||||
@ -57,4 +59,6 @@ return [
|
|||||||
"error_profile_get" => "Error retrieving user profile",
|
"error_profile_get" => "Error retrieving user profile",
|
||||||
"registered_at" => "Registered At",
|
"registered_at" => "Registered At",
|
||||||
"last_online" => "Last Online",
|
"last_online" => "Last Online",
|
||||||
|
"groups" => "Groups",
|
||||||
|
"logged_in_as" => "Logged in as",
|
||||||
];
|
];
|
@ -31,6 +31,8 @@ return [
|
|||||||
"add" => "Add",
|
"add" => "Add",
|
||||||
"close" => "Close",
|
"close" => "Close",
|
||||||
"ok" => "OK",
|
"ok" => "OK",
|
||||||
|
"id" => "ID",
|
||||||
|
"user" => "User",
|
||||||
"remove" => "Remove",
|
"remove" => "Remove",
|
||||||
"change" => "Change",
|
"change" => "Change",
|
||||||
"reset" => "Reset",
|
"reset" => "Reset",
|
||||||
@ -38,6 +40,7 @@ return [
|
|||||||
"go_back" => "Go Back",
|
"go_back" => "Go Back",
|
||||||
"save" => "Save",
|
"save" => "Save",
|
||||||
"save_only" => "Save Only",
|
"save_only" => "Save Only",
|
||||||
|
"saved" => "Saved",
|
||||||
"saving" => "Saving",
|
"saving" => "Saving",
|
||||||
"delete" => "Delete",
|
"delete" => "Delete",
|
||||||
"info" => "Info",
|
"info" => "Info",
|
||||||
|
@ -10,6 +10,7 @@ return [
|
|||||||
"token" => "Token",
|
"token" => "Token",
|
||||||
"request_new_key" => "Request new Key",
|
"request_new_key" => "Request new Key",
|
||||||
"show_only_active_keys" => "Show only active keys",
|
"show_only_active_keys" => "Show only active keys",
|
||||||
|
"no_api_key_registered" => "No valid API-Keys registered",
|
||||||
|
|
||||||
# GPG Key
|
# GPG Key
|
||||||
"gpg_key_placeholder_text" => "Paste or drag'n'drop your GPG-Key in ASCII format...",
|
"gpg_key_placeholder_text" => "Paste or drag'n'drop your GPG-Key in ASCII format...",
|
||||||
|
16
Core/Objects/DatabaseEntity/Attribute/UsePropertiesOf.php
Normal file
16
Core/Objects/DatabaseEntity/Attribute/UsePropertiesOf.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Core\Objects\DatabaseEntity\Attribute;
|
||||||
|
|
||||||
|
#[\Attribute(\Attribute::TARGET_CLASS)] class UsePropertiesOf {
|
||||||
|
|
||||||
|
private string $class;
|
||||||
|
|
||||||
|
public function __construct(string $class) {
|
||||||
|
$this->class = $class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getClass(): string {
|
||||||
|
return $this->class;
|
||||||
|
}
|
||||||
|
}
|
@ -38,6 +38,7 @@ use Core\Objects\DatabaseEntity\Attribute\MultipleReference;
|
|||||||
use Core\Objects\DatabaseEntity\Attribute\NoFetch;
|
use Core\Objects\DatabaseEntity\Attribute\NoFetch;
|
||||||
use Core\Objects\DatabaseEntity\Attribute\Transient;
|
use Core\Objects\DatabaseEntity\Attribute\Transient;
|
||||||
use Core\Objects\DatabaseEntity\Attribute\Unique;
|
use Core\Objects\DatabaseEntity\Attribute\Unique;
|
||||||
|
use Core\Objects\DatabaseEntity\Attribute\UsePropertiesOf;
|
||||||
|
|
||||||
class DatabaseEntityHandler implements Persistable {
|
class DatabaseEntityHandler implements Persistable {
|
||||||
|
|
||||||
@ -75,14 +76,17 @@ class DatabaseEntityHandler implements Persistable {
|
|||||||
|
|
||||||
public function init() {
|
public function init() {
|
||||||
$className = $this->entityClass->getName();
|
$className = $this->entityClass->getName();
|
||||||
|
$entityClass = $this->entityClass;
|
||||||
|
while ($usePropsOf = self::getAttribute($entityClass, UsePropertiesOf::class)) {
|
||||||
|
$entityClass = new \ReflectionClass($usePropsOf->getClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
$uniqueColumns = self::getAttribute($entityClass, Unique::class);
|
||||||
$uniqueColumns = self::getAttribute($this->entityClass, Unique::class);
|
|
||||||
if ($uniqueColumns) {
|
if ($uniqueColumns) {
|
||||||
$this->constraints[] = new \Core\Driver\SQL\Constraint\Unique($uniqueColumns->getColumns());
|
$this->constraints[] = new \Core\Driver\SQL\Constraint\Unique($uniqueColumns->getColumns());
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($this->entityClass->getProperties() as $property) {
|
foreach ($entityClass->getProperties() as $property) {
|
||||||
$propertyName = $property->getName();
|
$propertyName = $property->getName();
|
||||||
if ($propertyName === "id") {
|
if ($propertyName === "id") {
|
||||||
$this->properties[$propertyName] = $property;
|
$this->properties[$propertyName] = $property;
|
||||||
@ -123,8 +127,8 @@ class DatabaseEntityHandler implements Persistable {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$requestedClass = new \ReflectionClass($extendingClass);
|
$requestedClass = new \ReflectionClass($extendingClass);
|
||||||
if (!$requestedClass->isSubclassOf($this->entityClass)) {
|
if (!$requestedClass->isSubclassOf($entityClass)) {
|
||||||
$this->raiseError("Class '$extendingClass' must be an inheriting from '" . $this->entityClass->getName() . "' for an extending enum");
|
$this->raiseError("Class '$extendingClass' must be an inheriting from '" . $entityClass->getName() . "' for an extending enum");
|
||||||
} else {
|
} else {
|
||||||
$this->extendingClasses[$key] = $requestedClass;
|
$this->extendingClasses[$key] = $requestedClass;
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,10 @@ class GpgKey extends DatabaseEntity {
|
|||||||
$this->added = new \DateTime();
|
$this->added = new \DateTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function _encrypt(string $body): array {
|
||||||
|
return self::encrypt($body, $this->fingerprint);
|
||||||
|
}
|
||||||
|
|
||||||
public static function encrypt(string $body, string $gpgFingerprint): array {
|
public static function encrypt(string $body, string $gpgFingerprint): array {
|
||||||
$gpgFingerprint = escapeshellarg($gpgFingerprint);
|
$gpgFingerprint = escapeshellarg($gpgFingerprint);
|
||||||
$cmd = self::GPG2 . " --encrypt --output - --recipient $gpgFingerprint --trust-model always --batch --armor";
|
$cmd = self::GPG2 . " --encrypt --output - --recipient $gpgFingerprint --trust-model always --batch --armor";
|
||||||
@ -40,6 +44,23 @@ class GpgKey extends DatabaseEntity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function _sign(string $body): array {
|
||||||
|
return self::sign($body, $this->fingerprint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function sign(string $body, string $gpgFingerprint): array {
|
||||||
|
$gpgFingerprint = escapeshellarg($gpgFingerprint);
|
||||||
|
$cmd = self::GPG2 . " --clearsign --output - --local-user $gpgFingerprint --batch --armor";
|
||||||
|
list($out, $err) = self::proc_exec($cmd, $body, true);
|
||||||
|
if ($out === null) {
|
||||||
|
return createError("Error while communicating with GPG agent");
|
||||||
|
} else if ($err) {
|
||||||
|
return createError($err);
|
||||||
|
} else {
|
||||||
|
return ["success" => true, "data" => $out];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static function proc_exec(string $cmd, ?string $stdin = null, bool $raw = false): ?array {
|
private static function proc_exec(string $cmd, ?string $stdin = null, bool $raw = false): ?array {
|
||||||
$descriptorSpec = array(0 => ["pipe", "r"], 1 => ["pipe", "w"], 2 => ["pipe", "w"]);
|
$descriptorSpec = array(0 => ["pipe", "r"], 1 => ["pipe", "w"], 2 => ["pipe", "w"]);
|
||||||
$process = proc_open($cmd, $descriptorSpec, $pipes);
|
$process = proc_open($cmd, $descriptorSpec, $pipes);
|
||||||
@ -101,7 +122,11 @@ class GpgKey extends DatabaseEntity {
|
|||||||
return createError($err);
|
return createError($err);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function export($gpgFingerprint, bool $armored): array {
|
public function _export(bool $armored = true): array {
|
||||||
|
return self::export($this->fingerprint, $armored);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function export(string $gpgFingerprint, bool $armored): array {
|
||||||
$cmd = self::GPG2 . " --export ";
|
$cmd = self::GPG2 . " --export ";
|
||||||
if ($armored) {
|
if ($armored) {
|
||||||
$cmd .= "--armor ";
|
$cmd .= "--armor ";
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace Core\Objects\DatabaseEntity {
|
namespace Core\Objects\DatabaseEntity {
|
||||||
|
|
||||||
use Core\Driver\SQL\SQL;
|
|
||||||
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
|
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
|
||||||
use Core\Objects\DatabaseEntity\Attribute\Transient;
|
use Core\Objects\DatabaseEntity\Attribute\Transient;
|
||||||
use Core\Objects\DatabaseEntity\Controller\DatabaseEntity;
|
use Core\Objects\DatabaseEntity\Controller\DatabaseEntity;
|
||||||
|
@ -249,6 +249,8 @@ abstract class Route extends DatabaseEntity {
|
|||||||
new DocumentRoute("/login", true, \Core\Documents\Account::class, "account/login.twig"),
|
new DocumentRoute("/login", true, \Core\Documents\Account::class, "account/login.twig"),
|
||||||
new DocumentRoute("/resendConfirmEmail", true, \Core\Documents\Account::class, "account/resend_confirm_email.twig"),
|
new DocumentRoute("/resendConfirmEmail", true, \Core\Documents\Account::class, "account/resend_confirm_email.twig"),
|
||||||
new DocumentRoute("/debug", true, \Core\Documents\Info::class),
|
new DocumentRoute("/debug", true, \Core\Documents\Info::class),
|
||||||
|
new DocumentRoute("/.well-known/security.txt", true, \Core\Documents\Security::class),
|
||||||
|
new DocumentRoute("/.well-known/gpg-key.txt", true, \Core\Documents\Security::class),
|
||||||
new StaticFileRoute("/", true, "/static/welcome.html"),
|
new StaticFileRoute("/", true, "/static/welcome.html"),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,16 @@ class Session extends DatabaseEntity {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (is_array($session->data)) {
|
||||||
|
foreach ($session->data as $key => $value) {
|
||||||
|
$_SESSION[$key] = $value;
|
||||||
|
if ($key === "2faAuthenticated" && $value === true) {
|
||||||
|
$tfaToken = $session->getUser()->getTwoFactorToken();
|
||||||
|
$tfaToken?->authenticate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$session->context = $context;
|
$session->context = $context;
|
||||||
return $session;
|
return $session;
|
||||||
}
|
}
|
||||||
@ -66,6 +76,7 @@ class Session extends DatabaseEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function setData(array $data) {
|
public function setData(array $data) {
|
||||||
|
$this->data = $data;
|
||||||
foreach ($data as $key => $value) {
|
foreach ($data as $key => $value) {
|
||||||
$_SESSION[$key] = $value;
|
$_SESSION[$key] = $value;
|
||||||
}
|
}
|
||||||
@ -107,7 +118,7 @@ class Session extends DatabaseEntity {
|
|||||||
|
|
||||||
$sql = $this->context->getSQL();
|
$sql = $this->context->getSQL();
|
||||||
return $this->user->update($sql) &&
|
return $this->user->update($sql) &&
|
||||||
$this->save($sql, ["expires", "data"]);
|
$this->save($sql, ["expires", "data", "os", "browser"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCsrfToken(): string {
|
public function getCsrfToken(): string {
|
||||||
|
@ -2,13 +2,14 @@
|
|||||||
|
|
||||||
{% set view_title = 'account.resend_confirm_email_title' %}
|
{% set view_title = 'account.resend_confirm_email_title' %}
|
||||||
{% set view_icon = 'envelope' %}
|
{% set view_icon = 'envelope' %}
|
||||||
{% set view_description = 'resend_confirm_email_description' %}
|
{% set view_description = 'account.resend_confirm_email_description' %}
|
||||||
|
|
||||||
{% block view_content %}
|
{% block view_content %}
|
||||||
|
<form style="display: flex; flex-direction: column; justify-content: space-between; height: 100%">
|
||||||
|
<div>
|
||||||
<p class='lead'>
|
<p class='lead'>
|
||||||
{{ L("account.resend_confirm_email_form_title") }}
|
{{ L("account.resend_confirm_email_form_title") }}
|
||||||
</p>
|
</p>
|
||||||
<form>
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<span class="input-group-text"><i class="fas fa-at"></i></span>
|
<span class="input-group-text"><i class="fas fa-at"></i></span>
|
||||||
@ -16,7 +17,8 @@
|
|||||||
<input id="email" autocomplete='email' name="email" placeholder="{{ L('account.email') }}"
|
<input id="email" autocomplete='email' name="email" placeholder="{{ L('account.email') }}"
|
||||||
class="form-control" type="email" maxlength="64" />
|
class="form-control" type="email" maxlength="64" />
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group mt-2" style='position: absolute;bottom: 15px'>
|
</div>
|
||||||
|
<div class="input-group mt-2">
|
||||||
<button id='btnResendConfirmEmail' class='btn btn-primary'>
|
<button id='btnResendConfirmEmail' class='btn btn-primary'>
|
||||||
{{ L('general.request') }}
|
{{ L('general.request') }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -40,10 +40,11 @@
|
|||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<form style="display: flex; flex-direction: column; justify-content: space-between; height: 100%">
|
||||||
|
<div>
|
||||||
<p class='lead'>
|
<p class='lead'>
|
||||||
{{ L("account.reset_password_request_form_title") }}
|
{{ L("account.reset_password_request_form_title") }}
|
||||||
</p>
|
</p>
|
||||||
<form>
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<span class="input-group-text"><i class="fas fa-at"></i></span>
|
<span class="input-group-text"><i class="fas fa-at"></i></span>
|
||||||
@ -51,7 +52,8 @@
|
|||||||
<input id="email" autocomplete='email' name="email" placeholder="{{ L('account.email') }}"
|
<input id="email" autocomplete='email' name="email" placeholder="{{ L('account.email') }}"
|
||||||
class="form-control" type="email" maxlength="64" />
|
class="form-control" type="email" maxlength="64" />
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group mt-2" style='position: absolute;bottom: 15px'>
|
</div>
|
||||||
|
<div class="input-group mt-2">
|
||||||
<button id='btnRequestPasswordReset' class='btn btn-primary'>
|
<button id='btnRequestPasswordReset' class='btn btn-primary'>
|
||||||
{{ L('general.submit') }}
|
{{ L('general.submit') }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -15,6 +15,10 @@ RUN apt-get update -y && \
|
|||||||
printf "\n" | pecl install yaml imagick && docker-php-ext-enable yaml imagick && \
|
printf "\n" | pecl install yaml imagick && docker-php-ext-enable yaml imagick && \
|
||||||
docker-php-ext-install gd
|
docker-php-ext-install gd
|
||||||
|
|
||||||
|
# Browscap
|
||||||
|
RUN mkdir -p /usr/local/etc/php/extra/ && \
|
||||||
|
curl -s "https://browscap.org/stream?q=Full_PHP_BrowsCapINI" -o /usr/local/etc/php/extra/browscap.ini
|
||||||
|
|
||||||
# NodeJS
|
# NodeJS
|
||||||
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - && \
|
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - && \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
|
@ -932,7 +932,7 @@ default_socket_timeout = 60
|
|||||||
;extension=ldap
|
;extension=ldap
|
||||||
;extension=mbstring
|
;extension=mbstring
|
||||||
;extension=exif ; Must be after mbstring as it depends on it
|
;extension=exif ; Must be after mbstring as it depends on it
|
||||||
extension=mysqli
|
;extension=mysqli
|
||||||
;extension=oci8_12c ; Use with Oracle Database 12c Instant Client
|
;extension=oci8_12c ; Use with Oracle Database 12c Instant Client
|
||||||
;extension=oci8_19 ; Use with Oracle Database 19 Instant Client
|
;extension=oci8_19 ; Use with Oracle Database 19 Instant Client
|
||||||
;extension=odbc
|
;extension=odbc
|
||||||
@ -1318,7 +1318,7 @@ bcmath.scale = 0
|
|||||||
|
|
||||||
[browscap]
|
[browscap]
|
||||||
; https://php.net/browscap
|
; https://php.net/browscap
|
||||||
;browscap = extra/browscap.ini
|
browscap = /usr/local/etc/php/extra/browscap.ini
|
||||||
|
|
||||||
[Session]
|
[Session]
|
||||||
; Handler used to store/retrieve data.
|
; Handler used to store/retrieve data.
|
||||||
|
@ -52,7 +52,7 @@ $(document).ready(function () {
|
|||||||
$("#password").val("");
|
$("#password").val("");
|
||||||
createdDiv.hide();
|
createdDiv.hide();
|
||||||
if (res.emailConfirmed === false) {
|
if (res.emailConfirmed === false) {
|
||||||
showAlert("danger", res.msg + ' <a href="/resendConfirmation">Click here</a> to resend the confirmation mail.', true);
|
showAlert("danger", res.msg + ' <a href="/resendConfirmEmail">Click here</a> to resend the confirmation mail.', true);
|
||||||
} else {
|
} else {
|
||||||
showAlert("danger", res.msg);
|
showAlert("danger", res.msg);
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ function setState(state) {
|
|||||||
|
|
||||||
case SUCCESSFUL:
|
case SUCCESSFUL:
|
||||||
icon = 'fas fa-check-circle';
|
icon = 'fas fa-check-circle';
|
||||||
text = "Successfull";
|
text = "Successful";
|
||||||
color = "success";
|
color = "success";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -112,7 +112,7 @@ function waitForStatusChange() {
|
|||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
|
||||||
$("#btnSubmit").click(function() {
|
$("#btnSubmit").click(function() {
|
||||||
params = { };
|
let params = { };
|
||||||
let submitButton = $("#btnSubmit");
|
let submitButton = $("#btnSubmit");
|
||||||
let textBefore = submitButton.text();
|
let textBefore = submitButton.text();
|
||||||
submitButton.prop("disabled", true);
|
submitButton.prop("disabled", true);
|
||||||
@ -143,7 +143,7 @@ $(document).ready(function() {
|
|||||||
$("#btnPrev").prop("disabled", true);
|
$("#btnPrev").prop("disabled", true);
|
||||||
sendRequest({ "prev": true }, function(success) {
|
sendRequest({ "prev": true }, function(success) {
|
||||||
if(!success) {
|
if(!success) {
|
||||||
$("#btnPrev").prop("disabled",false);
|
$("#btnPrev").prop("disabled", false);
|
||||||
} else {
|
} else {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,10 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-monospace {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
.data-table-clickable {
|
.data-table-clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
@ -184,6 +184,60 @@ export class StringColumn extends DataColumn {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ArrayColumn extends DataColumn {
|
||||||
|
constructor(label, field = null, params = {}) {
|
||||||
|
super(label, field, params);
|
||||||
|
this.seperator = params.seperator || ", ";
|
||||||
|
}
|
||||||
|
|
||||||
|
renderData(L, entry, index) {
|
||||||
|
let data = super.renderData(L, entry, index);
|
||||||
|
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
data = Object.values(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
data = data.join(this.seperator);
|
||||||
|
|
||||||
|
if (this.params.style) {
|
||||||
|
let style = (typeof this.params.style === 'function'
|
||||||
|
? this.params.style(entry) : this.params.style);
|
||||||
|
data = <span style={style}>{data}</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SecretsColumn extends DataColumn {
|
||||||
|
constructor(label, field = null, params = {}) {
|
||||||
|
super(label, field, params);
|
||||||
|
this.asteriskCount = params.asteriskCount || 8;
|
||||||
|
this.character = params.character || "*";
|
||||||
|
this.canCopy = params.hasOwnProperty("canCopy") ? params.canCopy : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderData(L, entry, index) {
|
||||||
|
let originalData = super.renderData(L, entry, index);
|
||||||
|
if (!originalData) {
|
||||||
|
return "(None)";
|
||||||
|
}
|
||||||
|
|
||||||
|
let properties = this.params.properties || {};
|
||||||
|
properties.className = clsx(properties.className, "font-monospace");
|
||||||
|
|
||||||
|
if (this.canCopy) {
|
||||||
|
properties.title = L("Click to copy");
|
||||||
|
properties.className = clsx(properties.className, "data-table-clickable");
|
||||||
|
properties.onClick = () => {
|
||||||
|
navigator.clipboard.writeText(originalData);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span {...properties}>{this.character.repeat(this.asteriskCount)}</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class NumericColumn extends DataColumn {
|
export class NumericColumn extends DataColumn {
|
||||||
constructor(label, field = null, params = {}) {
|
constructor(label, field = null, params = {}) {
|
||||||
super(label, field, params);
|
super(label, field, params);
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogContentText,
|
DialogContentText,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
Input, List, ListItem, Select, TextField
|
Input, List, ListItem, TextField
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
|
||||||
export default function Dialog(props) {
|
export default function Dialog(props) {
|
||||||
@ -48,8 +48,10 @@ export default function Dialog(props) {
|
|||||||
|
|
||||||
switch (input.type) {
|
switch (input.type) {
|
||||||
case 'text':
|
case 'text':
|
||||||
|
case 'password':
|
||||||
inputElements.push(<TextField
|
inputElements.push(<TextField
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
|
type={input.type}
|
||||||
sx={{marginTop: 1}}
|
sx={{marginTop: 1}}
|
||||||
size={"small"} fullWidth={true}
|
size={"small"} fullWidth={true}
|
||||||
key={"input-" + input.name}
|
key={"input-" + input.name}
|
||||||
|
16
react/shared/hooks/before-unload.js
Normal file
16
react/shared/hooks/before-unload.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import {useCallback, useEffect} from "react";
|
||||||
|
|
||||||
|
export default function useBeforeUnload(modified) {
|
||||||
|
|
||||||
|
const capture = useCallback((event) => {
|
||||||
|
if (modified) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}, [modified]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener("beforeunload", capture, {capture: true});
|
||||||
|
return () => window.removeEventListener("beforeunload", capture, { capture: true });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
}
|
21
react/shared/hooks/editor-navigate.js
Normal file
21
react/shared/hooks/editor-navigate.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import {useNavigate} from "react-router-dom";
|
||||||
|
|
||||||
|
export default function useEditorNavigate(L, showDialog) {
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (uri, modified, options = null) => {
|
||||||
|
|
||||||
|
if (!modified) {
|
||||||
|
navigate(uri, options ?? {});
|
||||||
|
} else {
|
||||||
|
showDialog(
|
||||||
|
"You still have unsaved changes, are you really sure you want to leave this view?",
|
||||||
|
"Unsaved changes",
|
||||||
|
[L("general.cancel"), L("general.leave")],
|
||||||
|
(buttonIndex) => buttonIndex === 1 && navigate(uri, options ?? {})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
@ -297,7 +297,7 @@ export default function LoginForm(props) {
|
|||||||
? <Alert severity="error">
|
? <Alert severity="error">
|
||||||
{error}
|
{error}
|
||||||
{emailConfirmed === false
|
{emailConfirmed === false
|
||||||
? <> <Link href={"/resendConfirmation"}>Click here</Link> to resend the confirmation email.</>
|
? <> <Link href={"/resendConfirmEmail"}>Click here</Link> to resend the confirmation email.</>
|
||||||
: <></>
|
: <></>
|
||||||
}
|
}
|
||||||
</Alert>
|
</Alert>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
User-agent: *
|
User-agent: *
|
||||||
Disallow: /Core
|
Disallow: /Core
|
||||||
Disallow: /Site
|
Disallow: /Site
|
||||||
|
Disallow: /admin
|
Loading…
Reference in New Issue
Block a user