From 1d6ff1799448e460265724fc5dec30af3292c284 Mon Sep 17 00:00:00 2001 From: Roman Date: Sun, 15 Jan 2023 00:32:17 +0100 Subject: [PATCH] frontend, localization, bugfix --- Core/API/Parameter/Parameter.class.php | 3 + Core/API/Parameter/StringType.class.php | 8 ++- Core/API/Request.class.php | 40 +++++++++++- Core/Driver/SQL/MySQL.class.php | 1 + Core/Elements/TemplateDocument.class.php | 23 +++++-- Core/Localization/de_DE/general.php | 47 +++++++++++++- Core/Localization/en_US/general.php | 55 ++++++++++++++-- .../Controller/DatabaseEntity.class.php | 12 +++- .../Controller/DatabaseEntityHandler.php | 62 ++++++++++++------- .../Controller/DatabaseEntityQuery.class.php | 13 ++-- Core/Objects/Router/DocumentRoute.class.php | 2 +- Site/Localization/.gitkeep | 0 react/shared/api.js | 26 +++++--- react/shared/elements/dialog.css | 10 +++ react/shared/elements/dialog.jsx | 46 +++++++------- react/shared/locale.js | 15 ++++- react/shared/util.js | 16 +++-- test/DatabaseEntity.test.php | 2 +- 18 files changed, 297 insertions(+), 84 deletions(-) create mode 100644 Site/Localization/.gitkeep create mode 100644 react/shared/elements/dialog.css diff --git a/Core/API/Parameter/Parameter.class.php b/Core/API/Parameter/Parameter.class.php index 7ea8ef1..a85e93a 100644 --- a/Core/API/Parameter/Parameter.class.php +++ b/Core/API/Parameter/Parameter.class.php @@ -160,6 +160,9 @@ class Parameter { if ($value instanceof DateTime) { $this->value = $value; $valid = true; + } else if (is_int($value) || (is_string($value) && preg_match("/^\d+$/", $value))) { + $this->value = (new \DateTime())->setTimestamp(intval($value)); + $valid = true; } else { $format = $this->getFormat(); $d = DateTime::createFromFormat($format, $value); diff --git a/Core/API/Parameter/StringType.class.php b/Core/API/Parameter/StringType.class.php index 7732530..2467416 100644 --- a/Core/API/Parameter/StringType.class.php +++ b/Core/API/Parameter/StringType.class.php @@ -13,11 +13,15 @@ class StringType extends Parameter { } public function parseParam($value): bool { - if(!is_string($value)) { + if (!parent::parseParam($value)) { 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; } diff --git a/Core/API/Request.class.php b/Core/API/Request.class.php index 5f5b9ef..d0e46f9 100644 --- a/Core/API/Request.class.php +++ b/Core/API/Request.class.php @@ -144,7 +144,7 @@ abstract class Request { if ($this->externalCall) { $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); if ($jsonData !== null) { $values = array_merge($values, $jsonData); @@ -432,4 +432,42 @@ abstract class Request { 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]]; + } + } } \ No newline at end of file diff --git a/Core/Driver/SQL/MySQL.class.php b/Core/Driver/SQL/MySQL.class.php index bb7c055..3dea5f9 100644 --- a/Core/Driver/SQL/MySQL.class.php +++ b/Core/Driver/SQL/MySQL.class.php @@ -460,6 +460,7 @@ class MySQL extends SQL { return $query; } + // FIXME: access mysql database instead of configured one public function tableExists(string $tableName): bool { $tableSchema = $this->connectionData->getProperty("database"); $res = $this->select(new Count()) diff --git a/Core/Elements/TemplateDocument.class.php b/Core/Elements/TemplateDocument.class.php index f5f0c7f..17d71e4 100644 --- a/Core/Elements/TemplateDocument.class.php +++ b/Core/Elements/TemplateDocument.class.php @@ -16,20 +16,35 @@ use Twig\Loader\FilesystemLoader; class TemplateDocument extends Document { - const TEMPLATE_PATH = WEBROOT . '/Core/Templates'; - private string $templateName; protected array $parameters; private Environment $twigEnvironment; private FilesystemLoader $twigLoader; 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 = []) { parent::__construct($router); $this->title = "Untitled Document"; $this->templateName = $templateName; $this->parameters = $params; - $this->twigLoader = new FilesystemLoader(self::TEMPLATE_PATH); + $this->twigLoader = new FilesystemLoader(self::getTemplatePaths()); $this->twigEnvironment = new Environment($this->twigLoader, [ 'cache' => WEBROOT . '/Site/Cache/Templates/', 'auto_reload' => true @@ -84,7 +99,7 @@ class TemplateDocument extends Document { "query" => $urlParts["query"] ?? "", "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(), "title" => $this->title, "recaptcha" => [ diff --git a/Core/Localization/de_DE/general.php b/Core/Localization/de_DE/general.php index 74abdf5..9133018 100644 --- a/Core/Localization/de_DE/general.php +++ b/Core/Localization/de_DE/general.php @@ -4,15 +4,60 @@ return [ "something_went_wrong" => "Etwas ist schief gelaufen", "error_occurred" => "Ein Fehler ist aufgetreten", "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", "submit" => "Absenden", "request" => "Anfordern", + "cancel" => "Abbrechen", + "confirm" => "Bestätigen", "language" => "Sprache", "loading" => "Laden", "logout" => "Ausloggen", "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 "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", ]; \ No newline at end of file diff --git a/Core/Localization/en_US/general.php b/Core/Localization/en_US/general.php index ca39bca..961a95a 100644 --- a/Core/Localization/en_US/general.php +++ b/Core/Localization/en_US/general.php @@ -3,16 +3,61 @@ return [ "something_went_wrong" => "Something went wrong", "error_occurred" => "An error occurred", - "retry" => "Retry", - "go_back" => "Go Back", - "submitting" => "Submitting", - "submit" => "Submit", - "request" => "Request", + "unsaved_changes" => "You have unsaved changed", + "new" => "New", "language" => "Language", "loading" => "Loading", "logout" => "Logout", "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 "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", ]; \ No newline at end of file diff --git a/Core/Objects/DatabaseEntity/Controller/DatabaseEntity.class.php b/Core/Objects/DatabaseEntity/Controller/DatabaseEntity.class.php index 3939b7a..78bd82c 100644 --- a/Core/Objects/DatabaseEntity/Controller/DatabaseEntity.class.php +++ b/Core/Objects/DatabaseEntity/Controller/DatabaseEntity.class.php @@ -55,18 +55,20 @@ abstract class DatabaseEntity implements ArrayAccess, JsonSerializable { } 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"]; $jsonArray = []; foreach ($properties as $property) { $property->setAccessible(true); $propertyName = $property->getName(); + if (in_array($propertyName, $ignoredProperties)) { continue; } - if (!empty($property->getAttributes(Transient::class))) { + if (DatabaseEntityHandler::getAttribute($property, Transient::class)) { continue; } @@ -93,6 +95,10 @@ abstract class DatabaseEntity implements ArrayAccess, JsonSerializable { $value = $value->getTimestamp(); } else if ($value instanceof DatabaseEntity) { $subPropertyNames = $propertyNames[$propertyName] ?? null; + if ($subPropertyNames === null && $value instanceof $this) { + $subPropertyNames = $propertyNames; + } + $value = $value->jsonSerialize($subPropertyNames); } else if (is_array($value)) { $subPropertyNames = $propertyNames[$propertyName] ?? null; @@ -104,7 +110,7 @@ abstract class DatabaseEntity implements ArrayAccess, JsonSerializable { }, $value); } - $jsonArray[$property->getName()] = $value; + $jsonArray[$propertyName] = $value; } } } diff --git a/Core/Objects/DatabaseEntity/Controller/DatabaseEntityHandler.php b/Core/Objects/DatabaseEntity/Controller/DatabaseEntityHandler.php index 182dea2..43c40c9 100644 --- a/Core/Objects/DatabaseEntity/Controller/DatabaseEntityHandler.php +++ b/Core/Objects/DatabaseEntity/Controller/DatabaseEntityHandler.php @@ -315,6 +315,10 @@ class DatabaseEntityHandler implements Persistable { return $this->properties[$property]; } + public function hasProperty(string $propertyName): bool { + return isset($this->properties[$propertyName]); + } + public static function getPrefixedRow(array $row, string $prefix): array { $rel_row = []; foreach ($row as $relKey => $relValue) { @@ -325,7 +329,7 @@ class DatabaseEntityHandler implements Persistable { 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; if (!$column) { return false; @@ -340,15 +344,22 @@ class DatabaseEntityHandler implements Persistable { if ($column instanceof DateTimeColumn) { $value = new \DateTime($value); } else if ($column instanceof JsonColumn) { - $value = json_decode($value); + $value = json_decode($value, true); } else if (isset($this->relations[$propertyName])) { + $relationHandler = $this->relations[$propertyName]; $relColumnPrefix = self::buildColumnName($propertyName) . "_"; if (array_key_exists($relColumnPrefix . "id", $row)) { $relId = $row[$relColumnPrefix . "id"]; if ($relId !== null) { - if ($initEntities) { - $relationHandler = $this->relations[$propertyName]; - $value = $relationHandler->entityFromRow(self::getPrefixedRow($row, $relColumnPrefix), [], true); + if ($fetchEntities !== DatabaseEntityQuery::FETCH_NONE) { + if ($this === $relationHandler) { + $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 { 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; } - 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 { $constructorClass = $this->entityClass; @@ -384,7 +402,8 @@ class DatabaseEntityHandler implements Persistable { } 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->setValue($entity, $value); } @@ -405,6 +424,7 @@ class DatabaseEntityHandler implements Persistable { $this->properties["id"]->setAccessible(true); $this->properties["id"]->setValue($entity, $row["id"]); + $entity->postFetch($this->sql, $row); return $entity; } catch (\Exception $exception) { @@ -512,9 +532,13 @@ class DatabaseEntityHandler implements Persistable { 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 ($this->relations as $propertyName => $relHandler) { $property = $this->properties[$propertyName]; @@ -549,10 +573,7 @@ class DatabaseEntityHandler implements Persistable { ->addSelectValue(new Column($thisIdColumn)) ->where(new CondIn(new Column($thisIdColumn), $entityIds)); - if ($recursive) { - $relEntityQuery->fetchEntities(true); - } - + $relEntityQuery->fetchEntities($fetchEntities === DatabaseEntityQuery::FETCH_RECURSIVE); $rows = $relEntityQuery->executeSQL(); if (!is_array($rows)) { $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) { $relId = $row["id"]; if (!isset($relEntities[$relId])) { - $relEntity = $otherHandler->entityFromRow($row, [], $recursive); + $relEntity = $otherHandler->entityFromRow($row, [], $fetchEntities); $relEntities[$relId] = $relEntity; } @@ -575,6 +596,7 @@ class DatabaseEntityHandler implements Persistable { $property->setValue($thisEntity, $targetArray); } } else if ($nmRelation instanceof NMRelationReference) { + $otherHandler = $nmRelation->getRelHandler(); $thisIdColumn = $otherHandler->getColumnName($nmRelation->getThisProperty(), false); $relIdColumn = $otherHandler->getColumnName($nmRelation->getRefProperty(), false); @@ -582,10 +604,7 @@ class DatabaseEntityHandler implements Persistable { $relEntityQuery = DatabaseEntityQuery::fetchAll($otherHandler) ->where(new CondIn(new Column($thisIdColumn), $entityIds)); - if ($recursive) { - $relEntityQuery->fetchEntities(true); - } - + $relEntityQuery->fetchEntities($fetchEntities === DatabaseEntityQuery::FETCH_RECURSIVE); $rows = $relEntityQuery->executeSQL(); if (!is_array($rows)) { $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); foreach ($rows as $row) { - $relEntity = $otherHandler->entityFromRow($row, [], $recursive); + $relEntity = $otherHandler->entityFromRow($row, [], $fetchEntities); $thisEntity = $entities[$row[$thisIdColumn]]; $thisIdProperty->setValue($relEntity, $thisEntity); $targetArray = $property->getValue($thisEntity); @@ -609,10 +628,11 @@ class DatabaseEntityHandler implements Persistable { continue; } - if ($recursive) { + // TODO whats that here lol + if ($fetchEntities === DatabaseEntityQuery::FETCH_RECURSIVE) { foreach ($entities as $entity) { $relEntities = $property->getValue($entity); - $otherHandler->fetchNMRelations($relEntities); + // $otherHandler->fetchNMRelations($relEntities, $fetchEntities); } } } diff --git a/Core/Objects/DatabaseEntity/Controller/DatabaseEntityQuery.class.php b/Core/Objects/DatabaseEntity/Controller/DatabaseEntityQuery.class.php index ef92954..2f3fc2b 100644 --- a/Core/Objects/DatabaseEntity/Controller/DatabaseEntityQuery.class.php +++ b/Core/Objects/DatabaseEntity/Controller/DatabaseEntityQuery.class.php @@ -94,7 +94,9 @@ class DatabaseEntityQuery extends Select { $relIndex = 1; 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; @@ -117,7 +119,6 @@ class DatabaseEntityQuery extends Select { $alias = "t$relIndex"; // t1, t2, t3, ... $relIndex++; - if ($isNullable) { $this->leftJoin($referencedTable, "$tableName.$foreignColumnName", "$alias.id", $alias); } else { @@ -153,21 +154,21 @@ class DatabaseEntityQuery extends Select { if ($this->resultType === SQL::FETCH_ALL) { $entities = []; 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) { $entities[$entity->getId()] = $entity; } } if ($this->fetchSubEntities !== self::FETCH_NONE) { - $this->handler->fetchNMRelations($entities, $this->fetchSubEntities === self::FETCH_RECURSIVE); + $this->handler->fetchNMRelations($entities, $this->fetchSubEntities); } return $entities; } 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) { - $this->handler->fetchNMRelations([$entity->getId() => $entity], $this->fetchSubEntities === self::FETCH_RECURSIVE); + $this->handler->fetchNMRelations([$entity->getId() => $entity], $this->fetchSubEntities); } return $entity; diff --git a/Core/Objects/Router/DocumentRoute.class.php b/Core/Objects/Router/DocumentRoute.class.php index 18a9fe6..906456a 100644 --- a/Core/Objects/Router/DocumentRoute.class.php +++ b/Core/Objects/Router/DocumentRoute.class.php @@ -26,7 +26,7 @@ class DocumentRoute extends Route { protected function readExtra() { parent::readExtra(); - $this->args = json_decode($this->extra); + $this->args = json_decode($this->extra) ?? []; } public function preInsert(array &$row) { diff --git a/Site/Localization/.gitkeep b/Site/Localization/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/react/shared/api.js b/react/shared/api.js index 22b1c08..610421c 100644 --- a/react/shared/api.js +++ b/react/shared/api.js @@ -1,4 +1,5 @@ import {USER_GROUP_ADMIN} from "./constants"; +import {isInt} from "./util"; export default class API { constructor() { @@ -14,13 +15,22 @@ export default class API { async apiCall(method, params) { params = params || { }; - params.csrfToken = this.csrfToken(); - let response = await fetch("/api/" + method, { - method: 'post', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify(params) - }); + const csrfToken = this.csrfToken(); + const config = {method: 'post'}; + if (params instanceof FormData) { + if (csrfToken) { + 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(); if (!res.success && res.msg === "You are not logged in.") { this.loggedIn = false; @@ -35,7 +45,7 @@ export default class API { } 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; } else if (method.toLowerCase() === permission.toLowerCase()) { return true; @@ -48,7 +58,7 @@ export default class API { hasGroup(groupIdOrName) { if (this.loggedIn && this.user?.groups) { - if (!isNaN(groupIdOrName) && (typeof groupIdOrName === 'string' && groupIdOrName.match(/^\d+$/))) { + if (isInt(groupIdOrName)) { return this.user.groups.hasOwnProperty(groupIdOrName); } else { let userGroups = Object.values(this.user.groups); diff --git a/react/shared/elements/dialog.css b/react/shared/elements/dialog.css new file mode 100644 index 0000000..08a7906 --- /dev/null +++ b/react/shared/elements/dialog.css @@ -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; +} \ No newline at end of file diff --git a/react/shared/elements/dialog.jsx b/react/shared/elements/dialog.jsx index 0bc909e..c5d37c2 100644 --- a/react/shared/elements/dialog.jsx +++ b/react/shared/elements/dialog.jsx @@ -1,14 +1,16 @@ import React from "react"; 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) { const show = props.show; - const classes = ["modal", "fade"]; - const style = { paddingRight: "12px", display: (show ? "block" : "none") }; const onClose = props.onClose || function() { }; const onOption = props.onOption || function() { }; const options = props.options || ["Close"]; + const type = props.type || "default"; let buttons = []; for (let name of options) { @@ -17,31 +19,27 @@ export default function Dialog(props) { else if(name === "No") type = "danger"; buttons.push( - + ) } - return ( -
onClose()}> -
e.stopPropagation()}> -
-
-

{props.title}

- -
-
-

{props.message}

-
-
- { buttons } -
-
-
-
- ); + return + + + {props.title} + + + {props.message} + + { buttons } + + } \ No newline at end of file diff --git a/react/shared/locale.js b/react/shared/locale.js index 0d404a7..6db32a1 100644 --- a/react/shared/locale.js +++ b/react/shared/locale.js @@ -1,5 +1,6 @@ import React, {useReducer} from 'react'; import {createContext, useCallback, useState} from "react"; +import { enUS as dateFnsEN, de as dateFnsDE } from 'date-fns/locale'; const LocaleContext = createContext(null); @@ -29,11 +30,11 @@ function reducer(entries, action) { function LocaleProvider(props) { - // const [entries, setEntries] = useState(window.languageEntries || {}); const [entries, dispatch] = useReducer(reducer, window.languageEntries || {}); const [currentLocale, setCurrentLocale] = useState(window.languageCode || "en_US"); const translate = useCallback((key, defaultTranslation = null) => { + if (currentLocale) { if (entries.hasOwnProperty(currentLocale)) { let [module, variable] = key.split("."); @@ -46,7 +47,7 @@ function LocaleProvider(props) { } } - return defaultTranslation || "[" + key + "]"; + return key ? defaultTranslation || "[" + key + "]" : ""; }, [currentLocale, entries]); const hasModule = useCallback((code, module) => { @@ -61,6 +62,16 @@ function LocaleProvider(props) { } }, [entries]); + const toDateFns = () => { + switch (currentLocale) { + case 'de_DE': + return dateFnsDE; + case 'en_US': + default: + return dateFnsEN; + } + } + /** API HOOKS **/ const setLanguage = useCallback(async (api, params) => { let res = await api.setLanguage(params); diff --git a/react/shared/util.js b/react/shared/util.js index 9c92b73..03ab1bc 100644 --- a/react/shared/util.js +++ b/react/shared/util.js @@ -50,25 +50,25 @@ const getBaseUrl = () => { const formatDate = (L, apiDate) => { if (!(apiDate instanceof Date)) { if (!isNaN(apiDate)) { - apiDate = new Date(apiDate); + apiDate = new Date(apiDate * 1000); } else { 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) => { if (!(apiDate instanceof Date)) { if (!isNaN(apiDate)) { - apiDate = new Date(apiDate); + apiDate = new Date(apiDate * 1000); } else { 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) => { @@ -77,5 +77,11 @@ const upperFirstChars = (str) => { .join(" "); } +const isInt = (value) => { + return !isNaN(value) && + parseInt(Number(value)) === value && + !isNaN(parseInt(value, 10)); +} + export { humanReadableSize, removeParameter, getParameter, encodeText, decodeText, getBaseUrl, - formatDate, formatDateTime, upperFirstChars }; \ No newline at end of file + formatDate, formatDateTime, upperFirstChars, isInt }; \ No newline at end of file diff --git a/test/DatabaseEntity.test.php b/test/DatabaseEntity.test.php index b76cb27..016e0c8 100644 --- a/test/DatabaseEntity.test.php +++ b/test/DatabaseEntity.test.php @@ -17,7 +17,7 @@ class DatabaseEntityTest extends \PHPUnit\Framework\TestCase { public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); - self::$CONTEXT = new Context(); + self::$CONTEXT = Context::instance(); if (!self::$CONTEXT->initSQL()) { throw new Exception("Could not establish database connection"); }