security.txt + bugfixes
This commit is contained in:
parent
5acd13b945
commit
c8965e209b
@ -227,9 +227,9 @@ namespace Core\API\Mail {
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
$mailQueueItems = MailQueueItem::findBy(MailQueueItem::createBuilder($sql, false)
|
||||
->whereGt("retryCount", 0)
|
||||
->whereGt("retry_count", 0)
|
||||
->whereEq("status", "waiting")
|
||||
->where(new Compare("nextTry", $sql->now(), "<=")));
|
||||
->where(new Compare("next_try", $sql->now(), "<=")));
|
||||
|
||||
$this->success = ($mailQueueItems !== false);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
@ -68,6 +68,7 @@ class Swagger extends Request {
|
||||
foreach (self::getApiEndpoints() as $endpoint => $apiClass) {
|
||||
$body = null;
|
||||
$requiredProperties = [];
|
||||
$endpoint = "/$endpoint";
|
||||
$apiObject = $apiClass->newInstance($this->context, false);
|
||||
if (!$this->canView($permissions[strtolower($endpoint)] ?? [], $apiObject)) {
|
||||
continue;
|
||||
|
@ -222,7 +222,7 @@ namespace Core\API\User {
|
||||
|
||||
public function __construct(Context $context, $externalCall = false) {
|
||||
parent::__construct($context, $externalCall,
|
||||
self::getPaginationParameters(['id', 'name', 'email', 'groups', 'registeredAt'],
|
||||
self::getPaginationParameters(['id', 'name', 'fullName', 'email', 'groups', 'registeredAt', 'confirmed'],
|
||||
'id', 'asc')
|
||||
);
|
||||
}
|
||||
@ -341,7 +341,9 @@ namespace Core\API\User {
|
||||
$this->result["loggedIn"] = true;
|
||||
$userGroups = array_keys($currentUser->getGroups());
|
||||
$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();
|
||||
@ -1022,7 +1024,7 @@ namespace Core\API\User {
|
||||
|
||||
$userToken = UserToken::findBy(UserToken::createBuilder($sql, true)
|
||||
->whereFalse("used")
|
||||
->whereEq("tokenType", UserToken::TYPE_EMAIL_CONFIRM)
|
||||
->whereEq("token_type", UserToken::TYPE_EMAIL_CONFIRM)
|
||||
->whereEq("user_id", $user->getId()));
|
||||
|
||||
$validHours = 48;
|
||||
|
@ -85,7 +85,6 @@ class CreateDatabase extends DatabaseScript {
|
||||
$method = "$className::getHandler";
|
||||
$handler = call_user_func($method, $sql, null, true);
|
||||
$persistables[$handler->getTableName()] = $handler;
|
||||
|
||||
foreach ($handler->getNMRelations() as $nmTableName => $nmRelation) {
|
||||
$persistables[$nmTableName] = $nmRelation;
|
||||
}
|
||||
|
@ -107,7 +107,7 @@ class Settings {
|
||||
public static function loadDefaults(): Settings {
|
||||
$hostname = $_SERVER["SERVER_NAME"] ?? null;
|
||||
if (empty($hostname)) {
|
||||
$hostname = $_SERVER["HTTP_HOST"];
|
||||
$hostname = $_SERVER["HTTP_HOST"] ?? null;
|
||||
if (empty($hostname)) {
|
||||
$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) {
|
||||
$cspWhiteList = implode(" ", $this->cspWhitelist);
|
||||
$csp = [
|
||||
@ -98,7 +98,8 @@ abstract class Document {
|
||||
"base-uri 'self'",
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
"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()) {
|
||||
$csp[] = "frame-src https://www.google.com/ 'self'";
|
||||
@ -107,6 +108,15 @@ abstract class Document {
|
||||
$compiledCSP = implode("; ", $csp);
|
||||
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 = []);
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
return [
|
||||
"title" => "Account",
|
||||
"users" => "Benutzer",
|
||||
"login_title" => "Einloggen",
|
||||
"login_description" => "Loggen Sie sich in Ihren Account ein",
|
||||
"accept_invite_title" => "Einladung",
|
||||
@ -32,6 +33,7 @@ return [
|
||||
"remember_me" => "Eingeloggt bleiben",
|
||||
"signing_in" => "Einloggen",
|
||||
"sign_in" => "Einloggen",
|
||||
"confirmed" => "Bestätigt",
|
||||
"forgot_password" => "Passwort vergessen?",
|
||||
"change_password" => "Passwort ändern",
|
||||
"passwords_do_not_match" => "Die Passwörter stimmen nicht überein",
|
||||
@ -57,4 +59,6 @@ return [
|
||||
"error_profile_get" => "Fehler beim Laden des Benutzerprofils",
|
||||
"registered_at" => "Registriert am",
|
||||
"last_online" => "Zuletzt online",
|
||||
"groups" => "Gruppen",
|
||||
"logged_in_as" => "Eingeloggt als",
|
||||
];
|
@ -7,6 +7,7 @@ return [
|
||||
"go_back" => "Zurück",
|
||||
"save" => "Speichern",
|
||||
"save_only" => "Nur Speichern",
|
||||
"saved" => "Gespeichert",
|
||||
"saving" => "Speichere",
|
||||
"unsaved_changes" => "Du hast nicht gespeicherte Änderungen",
|
||||
"new" => "Neu",
|
||||
@ -19,6 +20,8 @@ return [
|
||||
"confirm" => "Bestätigen",
|
||||
"add" => "Hinzufügen",
|
||||
"ok" => "OK",
|
||||
"id" => "ID",
|
||||
"user" => "Benutzer",
|
||||
"language" => "Sprache",
|
||||
"loading" => "Laden",
|
||||
"logout" => "Ausloggen",
|
||||
|
@ -10,6 +10,7 @@ return [
|
||||
"token" => "Token",
|
||||
"request_new_key" => "Neuen Schlüssel anfordern",
|
||||
"show_only_active_keys" => "Zeige nur aktive Schlüssel",
|
||||
"no_api_key_registered" => "Keine gültigen API-Schlüssel registriert",
|
||||
|
||||
# GPG Key
|
||||
"gpg_key_placeholder_text" => "GPG-Key im ASCII format reinziehen oder einfügen...",
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
return [
|
||||
"title" => "Account",
|
||||
"users" => "Users",
|
||||
"login_title" => "Sign In",
|
||||
"login_description" => "Sign In into your account",
|
||||
"accept_invite_title" => "Invitation",
|
||||
@ -32,6 +33,7 @@ return [
|
||||
"remember_me" => "Remember Me",
|
||||
"signing_in" => "Signing in",
|
||||
"sign_in" => "Sign In",
|
||||
"confirmed" => "Confirmed",
|
||||
"forgot_password" => "Forgot password?",
|
||||
"change_password" => "Change password",
|
||||
"register_text" => "Don't have an account? Sign Up",
|
||||
@ -57,4 +59,6 @@ return [
|
||||
"error_profile_get" => "Error retrieving user profile",
|
||||
"registered_at" => "Registered At",
|
||||
"last_online" => "Last Online",
|
||||
"groups" => "Groups",
|
||||
"logged_in_as" => "Logged in as",
|
||||
];
|
@ -31,6 +31,8 @@ return [
|
||||
"add" => "Add",
|
||||
"close" => "Close",
|
||||
"ok" => "OK",
|
||||
"id" => "ID",
|
||||
"user" => "User",
|
||||
"remove" => "Remove",
|
||||
"change" => "Change",
|
||||
"reset" => "Reset",
|
||||
@ -38,6 +40,7 @@ return [
|
||||
"go_back" => "Go Back",
|
||||
"save" => "Save",
|
||||
"save_only" => "Save Only",
|
||||
"saved" => "Saved",
|
||||
"saving" => "Saving",
|
||||
"delete" => "Delete",
|
||||
"info" => "Info",
|
||||
|
@ -10,6 +10,7 @@ return [
|
||||
"token" => "Token",
|
||||
"request_new_key" => "Request new Key",
|
||||
"show_only_active_keys" => "Show only active keys",
|
||||
"no_api_key_registered" => "No valid API-Keys registered",
|
||||
|
||||
# GPG Key
|
||||
"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\Transient;
|
||||
use Core\Objects\DatabaseEntity\Attribute\Unique;
|
||||
use Core\Objects\DatabaseEntity\Attribute\UsePropertiesOf;
|
||||
|
||||
class DatabaseEntityHandler implements Persistable {
|
||||
|
||||
@ -75,14 +76,17 @@ class DatabaseEntityHandler implements Persistable {
|
||||
|
||||
public function init() {
|
||||
$className = $this->entityClass->getName();
|
||||
$entityClass = $this->entityClass;
|
||||
while ($usePropsOf = self::getAttribute($entityClass, UsePropertiesOf::class)) {
|
||||
$entityClass = new \ReflectionClass($usePropsOf->getClass());
|
||||
}
|
||||
|
||||
|
||||
$uniqueColumns = self::getAttribute($this->entityClass, Unique::class);
|
||||
$uniqueColumns = self::getAttribute($entityClass, Unique::class);
|
||||
if ($uniqueColumns) {
|
||||
$this->constraints[] = new \Core\Driver\SQL\Constraint\Unique($uniqueColumns->getColumns());
|
||||
}
|
||||
|
||||
foreach ($this->entityClass->getProperties() as $property) {
|
||||
foreach ($entityClass->getProperties() as $property) {
|
||||
$propertyName = $property->getName();
|
||||
if ($propertyName === "id") {
|
||||
$this->properties[$propertyName] = $property;
|
||||
@ -123,8 +127,8 @@ class DatabaseEntityHandler implements Persistable {
|
||||
|
||||
try {
|
||||
$requestedClass = new \ReflectionClass($extendingClass);
|
||||
if (!$requestedClass->isSubclassOf($this->entityClass)) {
|
||||
$this->raiseError("Class '$extendingClass' must be an inheriting from '" . $this->entityClass->getName() . "' for an extending enum");
|
||||
if (!$requestedClass->isSubclassOf($entityClass)) {
|
||||
$this->raiseError("Class '$extendingClass' must be an inheriting from '" . $entityClass->getName() . "' for an extending enum");
|
||||
} else {
|
||||
$this->extendingClasses[$key] = $requestedClass;
|
||||
}
|
||||
|
@ -27,6 +27,10 @@ class GpgKey extends DatabaseEntity {
|
||||
$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 {
|
||||
$gpgFingerprint = escapeshellarg($gpgFingerprint);
|
||||
$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 {
|
||||
$descriptorSpec = array(0 => ["pipe", "r"], 1 => ["pipe", "w"], 2 => ["pipe", "w"]);
|
||||
$process = proc_open($cmd, $descriptorSpec, $pipes);
|
||||
@ -101,7 +122,11 @@ class GpgKey extends DatabaseEntity {
|
||||
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 ";
|
||||
if ($armored) {
|
||||
$cmd .= "--armor ";
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
namespace Core\Objects\DatabaseEntity {
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
|
||||
use Core\Objects\DatabaseEntity\Attribute\Transient;
|
||||
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("/resendConfirmEmail", true, \Core\Documents\Account::class, "account/resend_confirm_email.twig"),
|
||||
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"),
|
||||
];
|
||||
}
|
||||
|
@ -44,6 +44,16 @@ class Session extends DatabaseEntity {
|
||||
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;
|
||||
return $session;
|
||||
}
|
||||
@ -66,6 +76,7 @@ class Session extends DatabaseEntity {
|
||||
}
|
||||
|
||||
public function setData(array $data) {
|
||||
$this->data = $data;
|
||||
foreach ($data as $key => $value) {
|
||||
$_SESSION[$key] = $value;
|
||||
}
|
||||
@ -107,7 +118,7 @@ class Session extends DatabaseEntity {
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
return $this->user->update($sql) &&
|
||||
$this->save($sql, ["expires", "data"]);
|
||||
$this->save($sql, ["expires", "data", "os", "browser"]);
|
||||
}
|
||||
|
||||
public function getCsrfToken(): string {
|
||||
|
@ -2,21 +2,23 @@
|
||||
|
||||
{% set view_title = 'account.resend_confirm_email_title' %}
|
||||
{% set view_icon = 'envelope' %}
|
||||
{% set view_description = 'resend_confirm_email_description' %}
|
||||
{% set view_description = 'account.resend_confirm_email_description' %}
|
||||
|
||||
{% block view_content %}
|
||||
<p class='lead'>
|
||||
{{ L("account.resend_confirm_email_form_title") }}
|
||||
</p>
|
||||
<form>
|
||||
<div class="input-group">
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text"><i class="fas fa-at"></i></span>
|
||||
<form style="display: flex; flex-direction: column; justify-content: space-between; height: 100%">
|
||||
<div>
|
||||
<p class='lead'>
|
||||
{{ L("account.resend_confirm_email_form_title") }}
|
||||
</p>
|
||||
<div class="input-group">
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text"><i class="fas fa-at"></i></span>
|
||||
</div>
|
||||
<input id="email" autocomplete='email' name="email" placeholder="{{ L('account.email') }}"
|
||||
class="form-control" type="email" maxlength="64" />
|
||||
</div>
|
||||
<input id="email" autocomplete='email' name="email" placeholder="{{ L('account.email') }}"
|
||||
class="form-control" type="email" maxlength="64" />
|
||||
</div>
|
||||
<div class="input-group mt-2" style='position: absolute;bottom: 15px'>
|
||||
<div class="input-group mt-2">
|
||||
<button id='btnResendConfirmEmail' class='btn btn-primary'>
|
||||
{{ L('general.request') }}
|
||||
</button>
|
||||
|
@ -40,18 +40,20 @@
|
||||
</form>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class='lead'>
|
||||
{{ L("account.reset_password_request_form_title") }}
|
||||
</p>
|
||||
<form>
|
||||
<div class="input-group">
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text"><i class="fas fa-at"></i></span>
|
||||
<form style="display: flex; flex-direction: column; justify-content: space-between; height: 100%">
|
||||
<div>
|
||||
<p class='lead'>
|
||||
{{ L("account.reset_password_request_form_title") }}
|
||||
</p>
|
||||
<div class="input-group">
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text"><i class="fas fa-at"></i></span>
|
||||
</div>
|
||||
<input id="email" autocomplete='email' name="email" placeholder="{{ L('account.email') }}"
|
||||
class="form-control" type="email" maxlength="64" />
|
||||
</div>
|
||||
<input id="email" autocomplete='email' name="email" placeholder="{{ L('account.email') }}"
|
||||
class="form-control" type="email" maxlength="64" />
|
||||
</div>
|
||||
<div class="input-group mt-2" style='position: absolute;bottom: 15px'>
|
||||
<div class="input-group mt-2">
|
||||
<button id='btnRequestPasswordReset' class='btn btn-primary'>
|
||||
{{ L('general.submit') }}
|
||||
</button>
|
||||
|
@ -15,6 +15,10 @@ RUN apt-get update -y && \
|
||||
printf "\n" | pecl install yaml imagick && docker-php-ext-enable yaml imagick && \
|
||||
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
|
||||
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - && \
|
||||
apt-get update && \
|
||||
|
@ -932,7 +932,7 @@ default_socket_timeout = 60
|
||||
;extension=ldap
|
||||
;extension=mbstring
|
||||
;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_19 ; Use with Oracle Database 19 Instant Client
|
||||
;extension=odbc
|
||||
@ -1318,7 +1318,7 @@ bcmath.scale = 0
|
||||
|
||||
[browscap]
|
||||
; https://php.net/browscap
|
||||
;browscap = extra/browscap.ini
|
||||
browscap = /usr/local/etc/php/extra/browscap.ini
|
||||
|
||||
[Session]
|
||||
; Handler used to store/retrieve data.
|
||||
|
@ -52,7 +52,7 @@ $(document).ready(function () {
|
||||
$("#password").val("");
|
||||
createdDiv.hide();
|
||||
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 {
|
||||
showAlert("danger", res.msg);
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ function setState(state) {
|
||||
|
||||
case SUCCESSFUL:
|
||||
icon = 'fas fa-check-circle';
|
||||
text = "Successfull";
|
||||
text = "Successful";
|
||||
color = "success";
|
||||
break;
|
||||
|
||||
@ -112,7 +112,7 @@ function waitForStatusChange() {
|
||||
$(document).ready(function() {
|
||||
|
||||
$("#btnSubmit").click(function() {
|
||||
params = { };
|
||||
let params = { };
|
||||
let submitButton = $("#btnSubmit");
|
||||
let textBefore = submitButton.text();
|
||||
submitButton.prop("disabled", true);
|
||||
@ -143,7 +143,7 @@ $(document).ready(function() {
|
||||
$("#btnPrev").prop("disabled", true);
|
||||
sendRequest({ "prev": true }, function(success) {
|
||||
if(!success) {
|
||||
$("#btnPrev").prop("disabled",false);
|
||||
$("#btnPrev").prop("disabled", false);
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
|
@ -23,6 +23,10 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.font-monospace {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.data-table-clickable {
|
||||
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 {
|
||||
constructor(label, field = null, params = {}) {
|
||||
super(label, field, params);
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
Input, List, ListItem, Select, TextField
|
||||
Input, List, ListItem, TextField
|
||||
} from "@mui/material";
|
||||
|
||||
export default function Dialog(props) {
|
||||
@ -48,8 +48,10 @@ export default function Dialog(props) {
|
||||
|
||||
switch (input.type) {
|
||||
case 'text':
|
||||
case 'password':
|
||||
inputElements.push(<TextField
|
||||
{...inputProps}
|
||||
type={input.type}
|
||||
sx={{marginTop: 1}}
|
||||
size={"small"} fullWidth={true}
|
||||
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">
|
||||
{error}
|
||||
{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>
|
||||
|
@ -1,3 +1,4 @@
|
||||
User-agent: *
|
||||
Disallow: /Core
|
||||
Disallow: /Site
|
||||
Disallow: /admin
|
Loading…
Reference in New Issue
Block a user