frontend, localization, bugfix

This commit is contained in:
Roman 2023-01-15 00:32:17 +01:00
parent 0418118841
commit 1d6ff17994
18 changed files with 297 additions and 84 deletions

@ -160,6 +160,9 @@ class Parameter {
if ($value instanceof DateTime) { if ($value instanceof DateTime) {
$this->value = $value; $this->value = $value;
$valid = true; $valid = true;
} else if (is_int($value) || (is_string($value) && preg_match("/^\d+$/", $value))) {
$this->value = (new \DateTime())->setTimestamp(intval($value));
$valid = true;
} else { } else {
$format = $this->getFormat(); $format = $this->getFormat();
$d = DateTime::createFromFormat($format, $value); $d = DateTime::createFromFormat($format, $value);

@ -13,11 +13,15 @@ class StringType extends Parameter {
} }
public function parseParam($value): bool { public function parseParam($value): bool {
if(!is_string($value)) { if (!parent::parseParam($value)) {
return false; return false;
} }
if($this->maxLength > 0 && strlen($value) > $this->maxLength) { if (!is_string($value)) {
return false;
}
if ($this->maxLength > 0 && strlen($value) > $this->maxLength) {
return false; return false;
} }

@ -144,7 +144,7 @@ abstract class Request {
if ($this->externalCall) { if ($this->externalCall) {
$values = $_REQUEST; $values = $_REQUEST;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_SERVER["CONTENT_TYPE"]) && in_array("application/json", explode(";", $_SERVER["CONTENT_TYPE"]))) { if ($_SERVER['REQUEST_METHOD'] === 'POST' && in_array("application/json", explode(";", $_SERVER["CONTENT_TYPE"] ?? ""))) {
$jsonData = json_decode(file_get_contents('php://input'), true); $jsonData = json_decode(file_get_contents('php://input'), true);
if ($jsonData !== null) { if ($jsonData !== null) {
$values = array_merge($values, $jsonData); $values = array_merge($values, $jsonData);
@ -432,4 +432,42 @@ abstract class Request {
return $this->createError("Error processing image: " . $ex->getMessage()); return $this->createError("Error processing image: " . $ex->getMessage());
} }
} }
protected function getFileUpload(string $name, bool $allowMultiple = false, ?array $extensions = null): false|array {
if (!isset($_FILES[$name]) || (is_array($_FILES[$name]["name"]) && empty($_FILES[$name]["name"])) || empty($_FILES[$name]["name"])) {
return $this->createError("Missing form-field '$name'");
}
$files = [];
if (is_array($_FILES[$name]["name"])) {
$numFiles = count($_FILES[$name]["name"]);
if (!$allowMultiple && $numFiles > 1) {
return $this->createError("Only one file allowed for form-field '$name'");
} else {
for ($i = 0; $i < $numFiles; $i++) {
$fileName = $_FILES[$name]["name"][$i];
$filePath = $_FILES[$name]["tmp_name"][$i];
$files[$fileName] = $filePath;
if (!empty($extensions) && !in_array(pathinfo($fileName, PATHINFO_EXTENSION), $extensions)) {
return $this->createError("File '$fileName' has forbidden extension, allowed: " . implode(",", $extensions));
}
}
}
} else {
$fileName = $_FILES[$name]["name"];
$filePath = $_FILES[$name]["tmp_name"];
$files[$fileName] = $filePath;
if (!empty($extensions) && !in_array(pathinfo($fileName, PATHINFO_EXTENSION), $extensions)) {
return $this->createError("File '$fileName' has forbidden extension, allowed: " . implode(",", $extensions));
}
}
if ($allowMultiple) {
return $files;
} else {
$fileName = key($files);
return [$fileName, $files[$fileName]];
}
}
} }

@ -460,6 +460,7 @@ class MySQL extends SQL {
return $query; return $query;
} }
// FIXME: access mysql database instead of configured one
public function tableExists(string $tableName): bool { public function tableExists(string $tableName): bool {
$tableSchema = $this->connectionData->getProperty("database"); $tableSchema = $this->connectionData->getProperty("database");
$res = $this->select(new Count()) $res = $this->select(new Count())

@ -16,20 +16,35 @@ use Twig\Loader\FilesystemLoader;
class TemplateDocument extends Document { class TemplateDocument extends Document {
const TEMPLATE_PATH = WEBROOT . '/Core/Templates';
private string $templateName; private string $templateName;
protected array $parameters; protected array $parameters;
private Environment $twigEnvironment; private Environment $twigEnvironment;
private FilesystemLoader $twigLoader; private FilesystemLoader $twigLoader;
protected string $title; protected string $title;
private static function getTemplatePaths(): array {
return [
implode(DIRECTORY_SEPARATOR, [WEBROOT, 'Site', 'Templates']),
implode(DIRECTORY_SEPARATOR, [WEBROOT, 'Core', 'Templates']),
];
}
private static function getTemplatePath(string $templateName): ?string {
foreach (self::getTemplatePaths() as $path) {
$filePath = implode(DIRECTORY_SEPARATOR, [$path, $templateName]);
if (is_file($filePath)) {
return $filePath;
}
}
return null;
}
public function __construct(Router $router, string $templateName, array $params = []) { public function __construct(Router $router, string $templateName, array $params = []) {
parent::__construct($router); parent::__construct($router);
$this->title = "Untitled Document"; $this->title = "Untitled Document";
$this->templateName = $templateName; $this->templateName = $templateName;
$this->parameters = $params; $this->parameters = $params;
$this->twigLoader = new FilesystemLoader(self::TEMPLATE_PATH); $this->twigLoader = new FilesystemLoader(self::getTemplatePaths());
$this->twigEnvironment = new Environment($this->twigLoader, [ $this->twigEnvironment = new Environment($this->twigLoader, [
'cache' => WEBROOT . '/Site/Cache/Templates/', 'cache' => WEBROOT . '/Site/Cache/Templates/',
'auto_reload' => true 'auto_reload' => true
@ -84,7 +99,7 @@ class TemplateDocument extends Document {
"query" => $urlParts["query"] ?? "", "query" => $urlParts["query"] ?? "",
"fragment" => $urlParts["fragment"] ?? "" "fragment" => $urlParts["fragment"] ?? ""
], ],
"lastModified" => date(L('Y-m-d H:i:s'), @filemtime(implode(DIRECTORY_SEPARATOR, [self::TEMPLATE_PATH, $name]))), "lastModified" => date(L('Y-m-d H:i:s'), @filemtime(self::getTemplatePath($name))),
"registrationEnabled" => $settings->isRegistrationAllowed(), "registrationEnabled" => $settings->isRegistrationAllowed(),
"title" => $this->title, "title" => $this->title,
"recaptcha" => [ "recaptcha" => [

@ -4,15 +4,60 @@ return [
"something_went_wrong" => "Etwas ist schief gelaufen", "something_went_wrong" => "Etwas ist schief gelaufen",
"error_occurred" => "Ein Fehler ist aufgetreten", "error_occurred" => "Ein Fehler ist aufgetreten",
"retry" => "Erneut versuchen", "retry" => "Erneut versuchen",
"Go back" => "Zurück", "go_back" => "Zurück",
"save" => "Speichern",
"saving" => "Speichere",
"unsaved_changes" => "Du hast nicht gespeicherte Änderungen",
"new" => "Neu",
"edit" => "Bearbeiten",
"submitting" => "Übermittle", "submitting" => "Übermittle",
"submit" => "Absenden", "submit" => "Absenden",
"request" => "Anfordern", "request" => "Anfordern",
"cancel" => "Abbrechen",
"confirm" => "Bestätigen",
"language" => "Sprache", "language" => "Sprache",
"loading" => "Laden", "loading" => "Laden",
"logout" => "Ausloggen", "logout" => "Ausloggen",
"noscript" => "Sie müssen Javascript aktivieren um diese Anwendung zu benutzen", "noscript" => "Sie müssen Javascript aktivieren um diese Anwendung zu benutzen",
"name" => "Name",
"type" => "Typ",
"size" => "Größe",
"last_modified" => "Zuletzt geändert",
# dialog / actions
"action" => "Aktion",
"title" => "Titel",
"message" => "Nachricht",
"rename" => "Umbenennen",
"move" => "Verschieben",
"delete" => "Löschen",
"info" => "Info",
"download" => "Herunterladen",
"download_all" => "Alles Herunterladen",
"upload" => "Hochladen",
"uploading" => "Lade hoch",
"overwrite" => "Überschreiben",
"reload" => "Aktualisieren",
# data table # data table
"showing_x_of_y_entries" => "Zeige %d von %d Einträgen", "showing_x_of_y_entries" => "Zeige %d von %d Einträgen",
"controls" => "Steuerung",
# date / time
"date" => "Datum",
"start_date" => "Startdatum",
"end_date" => "Enddatum",
"date_format" => "d.m.Y",
"date_time_format" => "d.m.Y H:i",
"date_time_format_precise" => "d.m.Y H:i:s",
"time_format" => "H:i",
"time_format_precise" => "H:i:s",
"datefns_date_format" => "dd.MM.yyyy",
"datefns_time_format" => "HH:mm",
"datefns_time_format_precise" => "HH:mm:ss",
"datefns_datetime_format" => "dd.MM.yyyy HH:mm",
"datefns_datetime_format_precise" => "dd.MM.yyyy HH:mm:ss",
# API
"no_api_key_registered" => "Kein gültiger API-Schlüssel registriert",
]; ];

@ -3,16 +3,61 @@
return [ return [
"something_went_wrong" => "Something went wrong", "something_went_wrong" => "Something went wrong",
"error_occurred" => "An error occurred", "error_occurred" => "An error occurred",
"retry" => "Retry", "unsaved_changes" => "You have unsaved changed",
"go_back" => "Go Back", "new" => "New",
"submitting" => "Submitting",
"submit" => "Submit",
"request" => "Request",
"language" => "Language", "language" => "Language",
"loading" => "Loading", "loading" => "Loading",
"logout" => "Logout", "logout" => "Logout",
"noscript" => "You need Javascript enabled to run this app", "noscript" => "You need Javascript enabled to run this app",
"name" => "Name",
"type" => "Type",
"size" => "Size",
"last_modified" => "Last Modified",
# dialog / actions
"action" => "Action",
"title" => "Title",
"message" => "Message",
"edit" => "Edit",
"submitting" => "Submitting",
"submit" => "Submit",
"request" => "Request",
"cancel" => "Cancel",
"confirm" => "Confirm",
"retry" => "Retry",
"go_back" => "Go Back",
"save" => "Save",
"saving" => "Saving",
"delete" => "Delete",
"info" => "Info",
"download" => "Download",
"download_all" => "Download All",
"upload" => "Upload",
"uploading" => "Uploading",
"rename" => "Rename",
"move" => "Move",
"overwrite" => "Overwrite",
"reload" => "Reload",
# data table # data table
"showing_x_of_y_entries" => "Showing %d of %d entries", "showing_x_of_y_entries" => "Showing %d of %d entries",
"controls" => "Controls",
# date / time
"date" => "Date",
"start_date" => "Start Date",
"end_date" => "End Date",
"date_format" => "m/d/Y",
"date_time_format" => "m/d/Y G:i A",
"date_time_format_precise" => "m/d/Y G:i:s A",
"time_format" => "G:i A",
"time_format_precise" => "G:i:s A",
"datefns_date_format" => "MM/dd/yyyy",
"datefns_time_format" => "p",
"datefns_time_format_precise" => "pp",
"datefns_datetime_format" => "MM/dd/yyyy p",
"datefns_datetime_format_precise" => "MM/dd/yyyy pp",
# API
"no_api_key_registered" => "No valid API-Key registered",
]; ];

@ -55,18 +55,20 @@ abstract class DatabaseEntity implements ArrayAccess, JsonSerializable {
} }
public function jsonSerialize(?array $propertyNames = null): array { public function jsonSerialize(?array $propertyNames = null): array {
$properties = (new \ReflectionClass(get_called_class()))->getProperties(); $reflectionClass = (new \ReflectionClass(get_called_class()));
$properties = $reflectionClass->getProperties();
$ignoredProperties = ["entityLogConfig", "customData"]; $ignoredProperties = ["entityLogConfig", "customData"];
$jsonArray = []; $jsonArray = [];
foreach ($properties as $property) { foreach ($properties as $property) {
$property->setAccessible(true); $property->setAccessible(true);
$propertyName = $property->getName(); $propertyName = $property->getName();
if (in_array($propertyName, $ignoredProperties)) { if (in_array($propertyName, $ignoredProperties)) {
continue; continue;
} }
if (!empty($property->getAttributes(Transient::class))) { if (DatabaseEntityHandler::getAttribute($property, Transient::class)) {
continue; continue;
} }
@ -93,6 +95,10 @@ abstract class DatabaseEntity implements ArrayAccess, JsonSerializable {
$value = $value->getTimestamp(); $value = $value->getTimestamp();
} else if ($value instanceof DatabaseEntity) { } else if ($value instanceof DatabaseEntity) {
$subPropertyNames = $propertyNames[$propertyName] ?? null; $subPropertyNames = $propertyNames[$propertyName] ?? null;
if ($subPropertyNames === null && $value instanceof $this) {
$subPropertyNames = $propertyNames;
}
$value = $value->jsonSerialize($subPropertyNames); $value = $value->jsonSerialize($subPropertyNames);
} else if (is_array($value)) { } else if (is_array($value)) {
$subPropertyNames = $propertyNames[$propertyName] ?? null; $subPropertyNames = $propertyNames[$propertyName] ?? null;
@ -104,7 +110,7 @@ abstract class DatabaseEntity implements ArrayAccess, JsonSerializable {
}, $value); }, $value);
} }
$jsonArray[$property->getName()] = $value; $jsonArray[$propertyName] = $value;
} }
} }
} }

@ -315,6 +315,10 @@ class DatabaseEntityHandler implements Persistable {
return $this->properties[$property]; return $this->properties[$property];
} }
public function hasProperty(string $propertyName): bool {
return isset($this->properties[$propertyName]);
}
public static function getPrefixedRow(array $row, string $prefix): array { public static function getPrefixedRow(array $row, string $prefix): array {
$rel_row = []; $rel_row = [];
foreach ($row as $relKey => $relValue) { foreach ($row as $relKey => $relValue) {
@ -325,7 +329,7 @@ class DatabaseEntityHandler implements Persistable {
return $rel_row; return $rel_row;
} }
private function getValueFromRow(array $row, string $propertyName, mixed &$value, bool $initEntities = false): bool { private function getValueFromRow(array $row, string $propertyName, mixed &$value, int $fetchEntities = DatabaseEntityQuery::FETCH_NONE): bool {
$column = $this->columns[$propertyName] ?? null; $column = $this->columns[$propertyName] ?? null;
if (!$column) { if (!$column) {
return false; return false;
@ -340,15 +344,22 @@ class DatabaseEntityHandler implements Persistable {
if ($column instanceof DateTimeColumn) { if ($column instanceof DateTimeColumn) {
$value = new \DateTime($value); $value = new \DateTime($value);
} else if ($column instanceof JsonColumn) { } else if ($column instanceof JsonColumn) {
$value = json_decode($value); $value = json_decode($value, true);
} else if (isset($this->relations[$propertyName])) { } else if (isset($this->relations[$propertyName])) {
$relationHandler = $this->relations[$propertyName];
$relColumnPrefix = self::buildColumnName($propertyName) . "_"; $relColumnPrefix = self::buildColumnName($propertyName) . "_";
if (array_key_exists($relColumnPrefix . "id", $row)) { if (array_key_exists($relColumnPrefix . "id", $row)) {
$relId = $row[$relColumnPrefix . "id"]; $relId = $row[$relColumnPrefix . "id"];
if ($relId !== null) { if ($relId !== null) {
if ($initEntities) { if ($fetchEntities !== DatabaseEntityQuery::FETCH_NONE) {
$relationHandler = $this->relations[$propertyName]; if ($this === $relationHandler) {
$value = $relationHandler->entityFromRow(self::getPrefixedRow($row, $relColumnPrefix), [], true); $value = DatabaseEntityQuery::fetchOne($this)
->fetchEntities($fetchEntities === DatabaseEntityQuery::FETCH_RECURSIVE)
->whereEq($this->getColumnName("id"), $relId)
->execute();
} else {
$value = $relationHandler->entityFromRow(self::getPrefixedRow($row, $relColumnPrefix), [], true);
}
} else { } else {
return false; return false;
} }
@ -362,10 +373,17 @@ class DatabaseEntityHandler implements Persistable {
} }
} }
if ($value === null) {
$defaultValue = self::getAttribute($this->properties[$propertyName], DefaultValue::class);
if ($defaultValue) {
$value = $defaultValue->getValue();
}
}
return true; return true;
} }
public function entityFromRow(array $row, array $additionalColumns = [], bool $initEntities = false): ?DatabaseEntity { public function entityFromRow(array $row, array $additionalColumns = [], int $fetchEntities = DatabaseEntityQuery::FETCH_NONE): ?DatabaseEntity {
try { try {
$constructorClass = $this->entityClass; $constructorClass = $this->entityClass;
@ -384,7 +402,8 @@ class DatabaseEntityHandler implements Persistable {
} }
foreach ($this->properties as $property) { foreach ($this->properties as $property) {
if ($this->getValueFromRow($row, $property->getName(), $value, $initEntities)) { $propertyName = $property->getName();
if ($this->getValueFromRow($row, $propertyName, $value, $fetchEntities)) {
$property->setAccessible(true); $property->setAccessible(true);
$property->setValue($entity, $value); $property->setValue($entity, $value);
} }
@ -405,6 +424,7 @@ class DatabaseEntityHandler implements Persistable {
$this->properties["id"]->setAccessible(true); $this->properties["id"]->setAccessible(true);
$this->properties["id"]->setValue($entity, $row["id"]); $this->properties["id"]->setValue($entity, $row["id"]);
$entity->postFetch($this->sql, $row); $entity->postFetch($this->sql, $row);
return $entity; return $entity;
} catch (\Exception $exception) { } catch (\Exception $exception) {
@ -512,9 +532,13 @@ class DatabaseEntityHandler implements Persistable {
return $success; return $success;
} }
public function fetchNMRelations(array $entities, bool $recursive = false) { public function fetchNMRelations(array $entities, int $fetchEntities = DatabaseEntityQuery::FETCH_DIRECT) {
if ($recursive) { if ($fetchEntities === DatabaseEntityQuery::FETCH_NONE) {
return;
}
if ($fetchEntities === DatabaseEntityQuery::FETCH_RECURSIVE) {
foreach ($entities as $entity) { foreach ($entities as $entity) {
foreach ($this->relations as $propertyName => $relHandler) { foreach ($this->relations as $propertyName => $relHandler) {
$property = $this->properties[$propertyName]; $property = $this->properties[$propertyName];
@ -549,10 +573,7 @@ class DatabaseEntityHandler implements Persistable {
->addSelectValue(new Column($thisIdColumn)) ->addSelectValue(new Column($thisIdColumn))
->where(new CondIn(new Column($thisIdColumn), $entityIds)); ->where(new CondIn(new Column($thisIdColumn), $entityIds));
if ($recursive) { $relEntityQuery->fetchEntities($fetchEntities === DatabaseEntityQuery::FETCH_RECURSIVE);
$relEntityQuery->fetchEntities(true);
}
$rows = $relEntityQuery->executeSQL(); $rows = $relEntityQuery->executeSQL();
if (!is_array($rows)) { if (!is_array($rows)) {
$this->logger->error("Error fetching n:m relations from table: '$nmTable': " . $this->sql->getLastError()); $this->logger->error("Error fetching n:m relations from table: '$nmTable': " . $this->sql->getLastError());
@ -563,7 +584,7 @@ class DatabaseEntityHandler implements Persistable {
foreach ($rows as $row) { foreach ($rows as $row) {
$relId = $row["id"]; $relId = $row["id"];
if (!isset($relEntities[$relId])) { if (!isset($relEntities[$relId])) {
$relEntity = $otherHandler->entityFromRow($row, [], $recursive); $relEntity = $otherHandler->entityFromRow($row, [], $fetchEntities);
$relEntities[$relId] = $relEntity; $relEntities[$relId] = $relEntity;
} }
@ -575,6 +596,7 @@ class DatabaseEntityHandler implements Persistable {
$property->setValue($thisEntity, $targetArray); $property->setValue($thisEntity, $targetArray);
} }
} else if ($nmRelation instanceof NMRelationReference) { } else if ($nmRelation instanceof NMRelationReference) {
$otherHandler = $nmRelation->getRelHandler(); $otherHandler = $nmRelation->getRelHandler();
$thisIdColumn = $otherHandler->getColumnName($nmRelation->getThisProperty(), false); $thisIdColumn = $otherHandler->getColumnName($nmRelation->getThisProperty(), false);
$relIdColumn = $otherHandler->getColumnName($nmRelation->getRefProperty(), false); $relIdColumn = $otherHandler->getColumnName($nmRelation->getRefProperty(), false);
@ -582,10 +604,7 @@ class DatabaseEntityHandler implements Persistable {
$relEntityQuery = DatabaseEntityQuery::fetchAll($otherHandler) $relEntityQuery = DatabaseEntityQuery::fetchAll($otherHandler)
->where(new CondIn(new Column($thisIdColumn), $entityIds)); ->where(new CondIn(new Column($thisIdColumn), $entityIds));
if ($recursive) { $relEntityQuery->fetchEntities($fetchEntities === DatabaseEntityQuery::FETCH_RECURSIVE);
$relEntityQuery->fetchEntities(true);
}
$rows = $relEntityQuery->executeSQL(); $rows = $relEntityQuery->executeSQL();
if (!is_array($rows)) { if (!is_array($rows)) {
$this->logger->error("Error fetching n:m relations from table: '$nmTable': " . $this->sql->getLastError()); $this->logger->error("Error fetching n:m relations from table: '$nmTable': " . $this->sql->getLastError());
@ -596,7 +615,7 @@ class DatabaseEntityHandler implements Persistable {
$thisIdProperty->setAccessible(true); $thisIdProperty->setAccessible(true);
foreach ($rows as $row) { foreach ($rows as $row) {
$relEntity = $otherHandler->entityFromRow($row, [], $recursive); $relEntity = $otherHandler->entityFromRow($row, [], $fetchEntities);
$thisEntity = $entities[$row[$thisIdColumn]]; $thisEntity = $entities[$row[$thisIdColumn]];
$thisIdProperty->setValue($relEntity, $thisEntity); $thisIdProperty->setValue($relEntity, $thisEntity);
$targetArray = $property->getValue($thisEntity); $targetArray = $property->getValue($thisEntity);
@ -609,10 +628,11 @@ class DatabaseEntityHandler implements Persistable {
continue; continue;
} }
if ($recursive) { // TODO whats that here lol
if ($fetchEntities === DatabaseEntityQuery::FETCH_RECURSIVE) {
foreach ($entities as $entity) { foreach ($entities as $entity) {
$relEntities = $property->getValue($entity); $relEntities = $property->getValue($entity);
$otherHandler->fetchNMRelations($relEntities); // $otherHandler->fetchNMRelations($relEntities, $fetchEntities);
} }
} }
} }

@ -94,7 +94,9 @@ class DatabaseEntityQuery extends Select {
$relIndex = 1; $relIndex = 1;
foreach ($this->handler->getRelations() as $propertyName => $relationHandler) { foreach ($this->handler->getRelations() as $propertyName => $relationHandler) {
$this->fetchRelation($propertyName, $this->handler->getTableName(), $this->handler, $relationHandler, $relIndex, $recursive); if ($this->handler !== $relationHandler) {
$this->fetchRelation($propertyName, $this->handler->getTableName(), $this->handler, $relationHandler, $relIndex, $recursive);
}
} }
return $this; return $this;
@ -117,7 +119,6 @@ class DatabaseEntityQuery extends Select {
$alias = "t$relIndex"; // t1, t2, t3, ... $alias = "t$relIndex"; // t1, t2, t3, ...
$relIndex++; $relIndex++;
if ($isNullable) { if ($isNullable) {
$this->leftJoin($referencedTable, "$tableName.$foreignColumnName", "$alias.id", $alias); $this->leftJoin($referencedTable, "$tableName.$foreignColumnName", "$alias.id", $alias);
} else { } else {
@ -153,21 +154,21 @@ class DatabaseEntityQuery extends Select {
if ($this->resultType === SQL::FETCH_ALL) { if ($this->resultType === SQL::FETCH_ALL) {
$entities = []; $entities = [];
foreach ($res as $row) { foreach ($res as $row) {
$entity = $this->handler->entityFromRow($row, $this->additionalColumns, $this->fetchSubEntities !== self::FETCH_NONE); $entity = $this->handler->entityFromRow($row, $this->additionalColumns, $this->fetchSubEntities);
if ($entity) { if ($entity) {
$entities[$entity->getId()] = $entity; $entities[$entity->getId()] = $entity;
} }
} }
if ($this->fetchSubEntities !== self::FETCH_NONE) { if ($this->fetchSubEntities !== self::FETCH_NONE) {
$this->handler->fetchNMRelations($entities, $this->fetchSubEntities === self::FETCH_RECURSIVE); $this->handler->fetchNMRelations($entities, $this->fetchSubEntities);
} }
return $entities; return $entities;
} else if ($this->resultType === SQL::FETCH_ONE) { } else if ($this->resultType === SQL::FETCH_ONE) {
$entity = $this->handler->entityFromRow($res, $this->additionalColumns, $this->fetchSubEntities !== self::FETCH_NONE); $entity = $this->handler->entityFromRow($res, $this->additionalColumns, $this->fetchSubEntities);
if ($entity instanceof DatabaseEntity && $this->fetchSubEntities !== self::FETCH_NONE) { if ($entity instanceof DatabaseEntity && $this->fetchSubEntities !== self::FETCH_NONE) {
$this->handler->fetchNMRelations([$entity->getId() => $entity], $this->fetchSubEntities === self::FETCH_RECURSIVE); $this->handler->fetchNMRelations([$entity->getId() => $entity], $this->fetchSubEntities);
} }
return $entity; return $entity;

@ -26,7 +26,7 @@ class DocumentRoute extends Route {
protected function readExtra() { protected function readExtra() {
parent::readExtra(); parent::readExtra();
$this->args = json_decode($this->extra); $this->args = json_decode($this->extra) ?? [];
} }
public function preInsert(array &$row) { public function preInsert(array &$row) {

@ -1,4 +1,5 @@
import {USER_GROUP_ADMIN} from "./constants"; import {USER_GROUP_ADMIN} from "./constants";
import {isInt} from "./util";
export default class API { export default class API {
constructor() { constructor() {
@ -14,13 +15,22 @@ export default class API {
async apiCall(method, params) { async apiCall(method, params) {
params = params || { }; params = params || { };
params.csrfToken = this.csrfToken(); const csrfToken = this.csrfToken();
let response = await fetch("/api/" + method, { const config = {method: 'post'};
method: 'post', if (params instanceof FormData) {
headers: {'Content-Type': 'application/json'}, if (csrfToken) {
body: JSON.stringify(params) params.append("csrfToken", csrfToken);
}); }
config.body = params;
} else {
if (csrfToken) {
params.csrfToken = csrfToken;
}
config.headers = {'Content-Type': 'application/json'};
config.body = JSON.stringify(params);
}
let response = await fetch("/api/" + method, config);
let res = await response.json(); let res = await response.json();
if (!res.success && res.msg === "You are not logged in.") { if (!res.success && res.msg === "You are not logged in.") {
this.loggedIn = false; this.loggedIn = false;
@ -35,7 +45,7 @@ export default class API {
} }
for (const permission of this.permissions) { for (const permission of this.permissions) {
if (method.endsWith("*") && permission.toLowerCase().startsWith(method.toLowerCase().substr(0, method.length - 1))) { if (method.endsWith("*") && permission.toLowerCase().startsWith(method.toLowerCase().substring(0, method.length - 1))) {
return true; return true;
} else if (method.toLowerCase() === permission.toLowerCase()) { } else if (method.toLowerCase() === permission.toLowerCase()) {
return true; return true;
@ -48,7 +58,7 @@ export default class API {
hasGroup(groupIdOrName) { hasGroup(groupIdOrName) {
if (this.loggedIn && this.user?.groups) { if (this.loggedIn && this.user?.groups) {
if (!isNaN(groupIdOrName) && (typeof groupIdOrName === 'string' && groupIdOrName.match(/^\d+$/))) { if (isInt(groupIdOrName)) {
return this.user.groups.hasOwnProperty(groupIdOrName); return this.user.groups.hasOwnProperty(groupIdOrName);
} else { } else {
let userGroups = Object.values(this.user.groups); let userGroups = Object.values(this.user.groups);

@ -0,0 +1,10 @@
.modal-dialog {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 400px;
border: 2px solid #000;
padding: 8px;
background-color: white;
}

@ -1,14 +1,16 @@
import React from "react"; import React from "react";
import clsx from "clsx"; import clsx from "clsx";
import {Box, Modal} from "@mui/material";
import {Button, Typography} from "@material-ui/core";
import "./dialog.css";
export default function Dialog(props) { export default function Dialog(props) {
const show = props.show; const show = props.show;
const classes = ["modal", "fade"];
const style = { paddingRight: "12px", display: (show ? "block" : "none") };
const onClose = props.onClose || function() { }; const onClose = props.onClose || function() { };
const onOption = props.onOption || function() { }; const onOption = props.onOption || function() { };
const options = props.options || ["Close"]; const options = props.options || ["Close"];
const type = props.type || "default";
let buttons = []; let buttons = [];
for (let name of options) { for (let name of options) {
@ -17,31 +19,27 @@ export default function Dialog(props) {
else if(name === "No") type = "danger"; else if(name === "No") type = "danger";
buttons.push( buttons.push(
<button type="button" key={"button-" + name} className={"btn btn-" + type} <Button variant={"outlined"} size={"small"} type="button" key={"button-" + name}
data-dismiss={"modal"} onClick={() => { onClose(); onOption(name); }}> data-dismiss={"modal"} onClick={() => { onClose(); onOption(name); }}>
{name} {name}
</button> </Button>
) )
} }
return ( return <Modal
<div className={clsx(classes, show && "show")} style={style} aria-modal={"true"} onClick={() => onClose()}> open={show}
<div className="modal-dialog" onClick={(e) => e.stopPropagation()}> onClose={onClose}
<div className="modal-content"> aria-labelledby="modal-title"
<div className="modal-header"> aria-describedby="modal-description"
<h4 className="modal-title">{props.title}</h4> >
<button type="button" className="close" data-dismiss="modal" aria-label="Close" onClick={() => onClose()}> <Box className={clsx("modal-dialog", props.className)}>
<span aria-hidden="true">×</span> <Typography id="modal-title" variant="h6" component="h2">
</button> {props.title}
</div> </Typography>
<div className="modal-body"> <Typography id="modal-description" sx={{ mt: 2 }}>
<p>{props.message}</p> {props.message}
</div> </Typography>
<div className="modal-footer"> { buttons }
{ buttons } </Box>
</div> </Modal>
</div>
</div>
</div>
);
} }

@ -1,5 +1,6 @@
import React, {useReducer} from 'react'; import React, {useReducer} from 'react';
import {createContext, useCallback, useState} from "react"; import {createContext, useCallback, useState} from "react";
import { enUS as dateFnsEN, de as dateFnsDE } from 'date-fns/locale';
const LocaleContext = createContext(null); const LocaleContext = createContext(null);
@ -29,11 +30,11 @@ function reducer(entries, action) {
function LocaleProvider(props) { function LocaleProvider(props) {
// const [entries, setEntries] = useState(window.languageEntries || {});
const [entries, dispatch] = useReducer(reducer, window.languageEntries || {}); const [entries, dispatch] = useReducer(reducer, window.languageEntries || {});
const [currentLocale, setCurrentLocale] = useState(window.languageCode || "en_US"); const [currentLocale, setCurrentLocale] = useState(window.languageCode || "en_US");
const translate = useCallback((key, defaultTranslation = null) => { const translate = useCallback((key, defaultTranslation = null) => {
if (currentLocale) { if (currentLocale) {
if (entries.hasOwnProperty(currentLocale)) { if (entries.hasOwnProperty(currentLocale)) {
let [module, variable] = key.split("."); let [module, variable] = key.split(".");
@ -46,7 +47,7 @@ function LocaleProvider(props) {
} }
} }
return defaultTranslation || "[" + key + "]"; return key ? defaultTranslation || "[" + key + "]" : "";
}, [currentLocale, entries]); }, [currentLocale, entries]);
const hasModule = useCallback((code, module) => { const hasModule = useCallback((code, module) => {
@ -61,6 +62,16 @@ function LocaleProvider(props) {
} }
}, [entries]); }, [entries]);
const toDateFns = () => {
switch (currentLocale) {
case 'de_DE':
return dateFnsDE;
case 'en_US':
default:
return dateFnsEN;
}
}
/** API HOOKS **/ /** API HOOKS **/
const setLanguage = useCallback(async (api, params) => { const setLanguage = useCallback(async (api, params) => {
let res = await api.setLanguage(params); let res = await api.setLanguage(params);

@ -50,25 +50,25 @@ const getBaseUrl = () => {
const formatDate = (L, apiDate) => { const formatDate = (L, apiDate) => {
if (!(apiDate instanceof Date)) { if (!(apiDate instanceof Date)) {
if (!isNaN(apiDate)) { if (!isNaN(apiDate)) {
apiDate = new Date(apiDate); apiDate = new Date(apiDate * 1000);
} else { } else {
apiDate = parse(apiDate, API_DATE_FORMAT, new Date()); apiDate = parse(apiDate, API_DATE_FORMAT, new Date());
} }
} }
return format(apiDate, L("general.date_format", "YYY/MM/dd")); return format(apiDate, L("general.datefns_date_format", "YYY/MM/dd"));
} }
const formatDateTime = (L, apiDate) => { const formatDateTime = (L, apiDate) => {
if (!(apiDate instanceof Date)) { if (!(apiDate instanceof Date)) {
if (!isNaN(apiDate)) { if (!isNaN(apiDate)) {
apiDate = new Date(apiDate); apiDate = new Date(apiDate * 1000);
} else { } else {
apiDate = parse(apiDate, API_DATETIME_FORMAT, new Date()); apiDate = parse(apiDate, API_DATETIME_FORMAT, new Date());
} }
} }
return format(apiDate, L("general.date_time_format", "YYY/MM/dd HH:mm:ss")); return format(apiDate, L("general.datefns_date_time_format", "YYY/MM/dd HH:mm:ss"));
} }
const upperFirstChars = (str) => { const upperFirstChars = (str) => {
@ -77,5 +77,11 @@ const upperFirstChars = (str) => {
.join(" "); .join(" ");
} }
const isInt = (value) => {
return !isNaN(value) &&
parseInt(Number(value)) === value &&
!isNaN(parseInt(value, 10));
}
export { humanReadableSize, removeParameter, getParameter, encodeText, decodeText, getBaseUrl, export { humanReadableSize, removeParameter, getParameter, encodeText, decodeText, getBaseUrl,
formatDate, formatDateTime, upperFirstChars }; formatDate, formatDateTime, upperFirstChars, isInt };

@ -17,7 +17,7 @@ class DatabaseEntityTest extends \PHPUnit\Framework\TestCase {
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
parent::setUpBeforeClass(); parent::setUpBeforeClass();
self::$CONTEXT = new Context(); self::$CONTEXT = Context::instance();
if (!self::$CONTEXT->initSQL()) { if (!self::$CONTEXT->initSQL()) {
throw new Exception("Could not establish database connection"); throw new Exception("Could not establish database connection");
} }