From 2ef4de0dba7f46cbc3f52c5f69af912ef75bafc5 Mon Sep 17 00:00:00 2001 From: Roman Date: Sun, 24 Mar 2024 17:36:16 +0100 Subject: [PATCH] Update --- .idea/php.xml | 2 +- Core/API/Search.class.php | 2 +- Core/API/SettingsAPI.class.php | 33 ------- Core/Configuration/Settings.class.php | 83 ------------------ Core/External/composer.json | 1 - Core/Localization/de_DE/general.php | 1 + Core/Localization/en_US/general.php | 1 + Core/Objects/Context.class.php | 21 +---- Core/Objects/DatabaseEntity/Session.class.php | 25 +++--- README.md | 86 +++++++++---------- cli.php | 56 ++++++------ docker-compose.yml | 8 +- docker/nginx/site.conf | 2 +- docker/php/Dockerfile | 4 +- js/script.js | 64 ++++++++------ react/admin-panel/src/AdminDashboard.jsx | 2 +- react/adminPanel.old/src/views/settings.js | 3 +- 17 files changed, 139 insertions(+), 255 deletions(-) diff --git a/.idea/php.xml b/.idea/php.xml index 31b8a3a..97a38d7 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/Core/API/Search.class.php b/Core/API/Search.class.php index eb4661b..e49d3d7 100644 --- a/Core/API/Search.class.php +++ b/Core/API/Search.class.php @@ -9,7 +9,7 @@ use Core\Objects\Search\SearchQuery; class Search extends Request { - public function __construct(Context $context, bool $externalCall = false, array $params = array()) { + public function __construct(Context $context, bool $externalCall = false) { parent::__construct($context, $externalCall, [ "text" => new StringType("text", 32) ]); diff --git a/Core/API/SettingsAPI.class.php b/Core/API/SettingsAPI.class.php index 8c7c847..0f058ec 100644 --- a/Core/API/SettingsAPI.class.php +++ b/Core/API/SettingsAPI.class.php @@ -155,37 +155,4 @@ namespace Core\API\Settings { $insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to modify site settings"); } } - - class GenerateJWT extends SettingsAPI { - - public function __construct(Context $context, bool $externalCall = false) { - parent::__construct($context, $externalCall, [ - "type" => new StringType("type", 32, true, "HS512") - ]); - } - - protected function _execute(): bool { - $algorithm = $this->getParam("type"); - if (!Settings::isJwtAlgorithmSupported($algorithm)) { - return $this->createError("Algorithm is not supported"); - } - - $settings = $this->context->getSettings(); - if (!$settings->generateJwtKey($algorithm)) { - return $this->createError("Error generating JWT-Key: " . $settings->getLogger()->getLastMessage()); - } - - $saveRequest = $settings->saveJwtKey($this->context); - if (!$saveRequest->success()) { - return $this->createError("Error saving JWT-Key: " . $saveRequest->getLastError()); - } - - $this->result["jwt_public_key"] = $settings->getJwtPublicKey(false)?->getKeyMaterial(); - return true; - } - - public static function getDefaultACL(Insert $insert): void { - $insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to regenerate the JWT key. This invalidates all sessions"); - } - } } \ No newline at end of file diff --git a/Core/Configuration/Settings.class.php b/Core/Configuration/Settings.class.php index 6896fab..76e8c15 100644 --- a/Core/Configuration/Settings.class.php +++ b/Core/Configuration/Settings.class.php @@ -26,11 +26,6 @@ class Settings { private array $allowedExtensions; private string $timeZone; - // jwt - private ?string $jwtPublicKey; - private ?string $jwtSecretKey; - private string $jwtAlgorithm; - // recaptcha private bool $recaptchaEnabled; private string $recaptchaPublicKey; @@ -84,22 +79,6 @@ class Settings { } } - public function getJwtPublicKey(bool $allowPrivate = true): ?\Firebase\JWT\Key { - if (empty($this->jwtPublicKey)) { - if ($allowPrivate && $this->jwtSecretKey) { - // we might have a symmetric key, should we instead return the private key? - return new \Firebase\JWT\Key($this->jwtSecretKey, $this->jwtAlgorithm); - } - return null; - } else { - return new \Firebase\JWT\Key($this->jwtPublicKey, $this->jwtAlgorithm); - } - } - - public function getJwtSecretKey(): ?\Firebase\JWT\Key { - return $this->jwtSecretKey ? new \Firebase\JWT\Key($this->jwtSecretKey, $this->jwtAlgorithm) : null; - } - public function isInstalled(): bool { return $this->installationComplete; } @@ -124,11 +103,6 @@ class Settings { $settings->registrationAllowed = false; $settings->timeZone = date_default_timezone_get(); - // JWT - $settings->jwtSecretKey = null; - $settings->jwtPublicKey = null; - $settings->jwtAlgorithm = "HS256"; - // Recaptcha $settings->recaptchaEnabled = false; $settings->recaptchaPublicKey = ""; @@ -143,50 +117,6 @@ class Settings { return $settings; } - public function generateJwtKey(string $algorithm = null): bool { - $this->jwtAlgorithm = $algorithm ?? $this->jwtAlgorithm; - - // TODO: key encryption necessary? - if (in_array($this->jwtAlgorithm, ["HS256", "HS384", "HS512"])) { - $this->jwtSecretKey = generateRandomString(32); - $this->jwtPublicKey = null; - } else if (in_array($this->jwtAlgorithm, ["RS256", "RS384", "RS512"])) { - $bits = intval(substr($this->jwtAlgorithm, 2)); - $private_key = openssl_pkey_new(["private_key_bits" => $bits]); - $this->jwtPublicKey = openssl_pkey_get_details($private_key)['key']; - openssl_pkey_export($private_key, $this->jwtSecretKey); - } else if (in_array($this->jwtAlgorithm, ["ES256", "ES384"])) { - // $ec = new \Elliptic\EC('secp256k1'); ?? - $this->logger->error("JWT algorithm: '$this->jwtAlgorithm' is currently not supported."); - return false; - } else if ($this->jwtAlgorithm == "EdDSA") { - $keyPair = sodium_crypto_sign_keypair(); - $this->jwtSecretKey = base64_encode(sodium_crypto_sign_secretkey($keyPair)); - $this->jwtPublicKey = base64_encode(sodium_crypto_sign_publickey($keyPair)); - } else { - $this->logger->error("Invalid JWT algorithm: '$this->jwtAlgorithm', expected one of: " . - implode(",", array_keys(\Firebase\JWT\JWT::$supported_algs))); - return false; - } - - return true; - } - - public static function isJwtAlgorithmSupported(string $algorithm): bool { - return in_array(strtoupper($algorithm), ["HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "EDDSA"]); - } - - public function saveJwtKey(Context $context): \Core\API\Settings\Set { - $req = new \Core\API\Settings\Set($context); - $req->execute(array("settings" => array( - "jwt_secret_key" => $this->jwtSecretKey, - "jwt_public_key" => $this->jwtSecretKey, - "jwt_algorithm" => $this->jwtAlgorithm, - ))); - - return $req; - } - public function loadFromDatabase(Context $context): bool { $this->logger = new Logger("Settings", $context->getSQL()); $req = new \Core\API\Settings\Get($context); @@ -199,9 +129,6 @@ class Settings { $this->registrationAllowed = $result["user_registration_enabled"] ?? $this->registrationAllowed; $this->installationComplete = $result["installation_completed"] ?? $this->installationComplete; $this->timeZone = $result["time_zone"] ?? $this->timeZone; - $this->jwtSecretKey = $result["jwt_secret_key"] ?? $this->jwtSecretKey; - $this->jwtPublicKey = $result["jwt_public_key"] ?? $this->jwtPublicKey; - $this->jwtAlgorithm = $result["jwt_algorithm"] ?? $this->jwtAlgorithm; $this->recaptchaEnabled = $result["recaptcha_enabled"] ?? $this->recaptchaEnabled; $this->recaptchaPublicKey = $result["recaptcha_public_key"] ?? $this->recaptchaPublicKey; $this->recaptchaPrivateKey = $result["recaptcha_private_key"] ?? $this->recaptchaPrivateKey; @@ -210,13 +137,6 @@ class Settings { $this->mailFooter = $result["mail_footer"] ?? $this->mailFooter; $this->mailAsync = $result["mail_async"] ?? $this->mailAsync; $this->allowedExtensions = explode(",", $result["allowed_extensions"] ?? strtolower(implode(",", $this->allowedExtensions))); - - if (!isset($result["jwt_secret_key"])) { - if ($this->generateJwtKey()) { - $this->saveJwtKey($context); - } - } - date_default_timezone_set($this->timeZone); } @@ -229,9 +149,6 @@ class Settings { ->addRow("user_registration_enabled", $this->registrationAllowed ? "1" : "0", false, false) ->addRow("installation_completed", $this->installationComplete ? "1" : "0", true, true) ->addRow("time_zone", $this->timeZone, false, false) - ->addRow("jwt_secret_key", $this->jwtSecretKey, true, false) - ->addRow("jwt_public_key", $this->jwtPublicKey, false, false) - ->addRow("jwt_algorithm", $this->jwtAlgorithm, false, false) ->addRow("recaptcha_enabled", $this->recaptchaEnabled ? "1" : "0", false, false) ->addRow("recaptcha_public_key", $this->recaptchaPublicKey, false, false) ->addRow("recaptcha_private_key", $this->recaptchaPrivateKey, true, false) diff --git a/Core/External/composer.json b/Core/External/composer.json index 6a14ac7..9243fd5 100644 --- a/Core/External/composer.json +++ b/Core/External/composer.json @@ -6,7 +6,6 @@ "christian-riesen/base32": "^1.6", "spomky-labs/cbor-php": "2.1.0", "web-auth/cose-lib": "3.3.12", - "firebase/php-jwt": "^6.2", "html2text/html2text": "^4.3" }, "require-dev": { diff --git a/Core/Localization/de_DE/general.php b/Core/Localization/de_DE/general.php index d6c554b..b276317 100644 --- a/Core/Localization/de_DE/general.php +++ b/Core/Localization/de_DE/general.php @@ -32,6 +32,7 @@ return [ "not_supported" => "Nicht unterstützt", "yes" => "Ja", "no" => "Nein", + "create_new" => "Erstellen", # dialog / actions "action" => "Aktion", diff --git a/Core/Localization/en_US/general.php b/Core/Localization/en_US/general.php index fe39ed8..2508a67 100644 --- a/Core/Localization/en_US/general.php +++ b/Core/Localization/en_US/general.php @@ -15,6 +15,7 @@ return [ "not_supported" => "Not supported", "yes" => "Yes", "no" => "No", + "create_new" => "Create", # dialog / actions "action" => "Action", diff --git a/Core/Objects/Context.class.php b/Core/Objects/Context.class.php index 5348c4e..f07e6a4 100644 --- a/Core/Objects/Context.class.php +++ b/Core/Objects/Context.class.php @@ -9,7 +9,6 @@ use Core\Driver\SQL\Condition\CondLike; use Core\Driver\SQL\Condition\CondOr; use Core\Driver\SQL\Join\InnerJoin; use Core\Driver\SQL\SQL; -use Firebase\JWT\JWT; use Core\Objects\DatabaseEntity\Language; use Core\Objects\DatabaseEntity\Session; use Core\Objects\DatabaseEntity\User; @@ -99,28 +98,14 @@ class Context { session_write_close(); } - private function loadSession(int $userId, int $sessionId): void { - $this->session = Session::init($this, $userId, $sessionId); + private function loadSession(string $sessionUUID): void { + $this->session = Session::init($this, $sessionUUID); $this->user = $this->session?->getUser(); } public function parseCookies(): void { if (isset($_COOKIE['session']) && is_string($_COOKIE['session']) && !empty($_COOKIE['session'])) { - try { - $token = $_COOKIE['session']; - $settings = $this->configuration->getSettings(); - $jwtKey = $settings->getJwtSecretKey(); - if ($jwtKey) { - $decoded = (array)JWT::decode($token, $jwtKey); - $userId = ($decoded['userId'] ?? NULL); - $sessionId = ($decoded['sessionId'] ?? NULL); - if (!is_null($userId) && !is_null($sessionId)) { - $this->loadSession($userId, $sessionId); - } - } - } catch (\Exception $e) { - // ignored - } + $this->loadSession($_COOKIE['session']); } // set language by priority: 1. GET parameter, 2. cookie, 3. user's settings diff --git a/Core/Objects/DatabaseEntity/Session.class.php b/Core/Objects/DatabaseEntity/Session.class.php index 8456c7a..d40eeb9 100644 --- a/Core/Objects/DatabaseEntity/Session.class.php +++ b/Core/Objects/DatabaseEntity/Session.class.php @@ -4,7 +4,6 @@ namespace Core\Objects\DatabaseEntity; use DateTime; use Exception; -use Firebase\JWT\JWT; use Core\Objects\Context; use Core\Objects\DatabaseEntity\Attribute\DefaultValue; use Core\Objects\DatabaseEntity\Attribute\Json; @@ -21,6 +20,7 @@ class Session extends DatabaseEntity { private User $user; private DateTime $expires; #[MaxLength(45)] private string $ipAddress; + #[MaxLength(36)] private string $uuid; #[DefaultValue(true)] private bool $active; #[MaxLength(64)] private ?string $os; #[MaxLength(64)] private ?string $browser; @@ -32,15 +32,21 @@ class Session extends DatabaseEntity { parent::__construct(); $this->context = $context; $this->user = $user; + $this->uuid = uuidv4(); $this->stayLoggedIn = false; $this->csrfToken = $csrfToken ?? generateRandomString(16); $this->expires = (new DateTime())->modify(sprintf("+%d second", Session::DURATION)); $this->active = true; } - public static function init(Context $context, int $userId, int $sessionId): ?Session { - $session = Session::find($context->getSQL(), $sessionId, true, true); - if (!$session || !$session->active || $session->user->getId() !== $userId) { + public static function init(Context $context, string $sessionUUID): ?Session { + $sql = $context->getSQL(); + $session = Session::findBy(Session::createBuilder($sql, true) + ->fetchEntities(true) + ->whereEq("Session.uuid", $sessionUUID) + ->whereTrue("Session.active") + ->whereGt("Session.expires", $sql->now())); + if (!$session) { return null; } @@ -82,18 +88,13 @@ class Session extends DatabaseEntity { } } - public function getCookie(): string { - $this->updateMetaData(); - $settings = $this->context->getSettings(); - $token = ['userId' => $this->user->getId(), 'sessionId' => $this->getId()]; - $jwtPublicKey = $settings->getJwtPublicKey(); - return JWT::encode($token, $jwtPublicKey->getKeyMaterial(), $jwtPublicKey->getAlgorithm()); + public function getUUID(): string { + return $this->uuid; } public function sendCookie(string $domain) { - $sessionCookie = $this->getCookie(); $secure = strcmp(getProtocol(), "https") === 0; - setcookie('session', $sessionCookie, $this->getExpiresTime(), "/", $domain, $secure, true); + setcookie('session', $this->uuid, $this->getExpiresTime(), "/", $domain, $secure, true); } public function getExpiresTime(): int { diff --git a/README.md b/README.md index b95d79e..7464f9a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Web-Base is a php framework which provides basic web functionalities and a modern ReactJS Admin Dashboard. ### Requirements -- PHP >= 7.4 +- PHP >= 8.1 - One of these php extensions: mysqli, postgres - Apache/nginx or docker @@ -31,16 +31,23 @@ I actually don't know what i want to implement here. There are quite to many CMS 3. Open the webapp in your browser and follow the installation guide ### Docker Installation -1. `docker-compose up` -2. Open the webapp in your browser and follow the installation guide +1. `docker-compose build` +2. `docker-compose up` +3. Open the webapp in your browser and follow the installation guide ### Afterwards -For any changes made in [/adminPanel](/adminPanel), run: -1. once: `npm i` -2. build: `npm run build` +For any changes made in [/react](/react), run: +1. once: `yarn i` +2. build: `yarn run build` The compiled dist files will be automatically moved to `/js`. +To spawn a temporary development server, run: +```bash +cd react +yarn workspace $project run dev +``` + To fulfill the requirements of data deletion for **GDPR**, add the following line to your `/etc/crontab` or any other cron file: ``` @@ -51,29 +58,25 @@ or any other cron file: ### Adding API-Endpoints -Each API endpoint has usually one overlying category, for example all user and authorization endpoints belong to the [UserAPI](/core/Api/UserAPI.class.php). +Each API endpoint has usually one overlying category, for example all user and authorization endpoints belong to the [UserAPI](/Core/API/UserAPI.class.php). These endpoints can be accessed by requesting URLs starting with `/api/user`, for example: `/api/user/login`. There are also endpoints, which don't have -a category, e.g. [VerifyCaptcha](/core/Api/VerifyCaptcha.class.php). These functions can be called directly, for example with `/api/verifyCaptcha`. Both methods have one thing in common: -Each endpoint is represented by a class inheriting the [Request Class](/core/Api/Request.class.php). An example endpoint looks like this: +a category, e.g. [VerifyCaptcha](/Core/API/VerifyCaptcha.class.php). These functions can be called directly, for example with `/api/verifyCaptcha`. Both methods have one thing in common: +Each endpoint is represented by a class inheriting the [Request Class](/Core/API/Request.class.php). An example endpoint looks like this: ```php -namespace Api; -use Core\API\Parameter\Parameter; -use Core\Objects\DatabaseEntity\User; +namespace Core\API; +use Core\Objects\Context; class SingleEndpoint extends Request { - public function __construct(User $user, bool $externalCall = false) { - parent::__construct($user, $externalCall, array( + public function __construct(Context $context, bool $externalCall = false) { + parent::__construct($context, $externalCall, array( "someParameter" => new Parameter("someParameter", Parameter::TYPE_INT, true, 100) )); $this->forbidMethod("POST"); } - public function execute($values = array()): bool { - if (!parent::execute($values)) { - return false; - } + public function _execute(): bool { $this->result['someAttribute'] = $this->getParam("someParameter") * 2; return true; } @@ -87,7 +90,7 @@ An endpoint consists of two important functions: To create an API category containing multiple endpoints, a parent class inheriting from `Request`, e.g. `class MultipleAPI extends Request` is required. All endpoints inside this category then inherit from the `MultipleAPI` class. -The classes must be present inside the [Api](/core/Api) directory according to the other endpoints. +The classes must be present inside the [API](/Core/API) directory according to the other endpoints. ### Access Control @@ -100,7 +103,7 @@ By default, and if not further specified or restricted, all endpoints have the f 6. All user groups can access the method (Database, Table: `ApiPermission`) The first five restrictions can be modified inside the constructor, while the group permissions are changed using -the [Admin Dashboard](/adminPanel). It's default values are set inside the [database script](/core/Configuration/CreateDatabase.class.php). +the [Admin Dashboard](/react/admin-panel). It's default values are set inside the [database script](/Core/Configuration/CreateDatabase.class.php). ### Using the API internally @@ -109,11 +112,11 @@ can be used by creating the desired request object, and calling the execute func ```php $req = new \Core\API\Mail\Send($context); -$success = $req->execute(array( +$success = $req->execute([ "to" => "mail@example.org", "subject" => "Example Mail", "body" => "This is an example mail" -)); +]); if (!$success) { echo $req->getLastError(); @@ -127,9 +130,9 @@ If any result is expected from the api call, the `$req->getResult()` method can This step is not really required, as and changes made to the database must not be presented inside the code. On the other hand, it is recommended to keep track of any modifications for later use or to deploy the application -to other systems. Therefore, either the [default installation script](/core/Configuration/CreateDatabase.class.php) or +to other systems. Therefore, either the [default installation script](/Core/Configuration/CreateDatabase.class.php) or an additional patch file, which can be executed using the [CLI](#CLI), can be created. The patch files are usually -located in [/core/Configuration/Patch](/core/Configuration/Patch) and have the following structure: +located in [/Core/Configuration/Patch](/Core/Configuration/Patch) and have the following structure: ```php namespace Core\Configuration\Patch; @@ -175,7 +178,7 @@ Secondly, it passes the second group (`$2`), which is all the text after the las A frontend page consists of a document, which again consists of a head and a body. Furthermore, a document can have various views, which have to be implemented programmatically. Usually, all pages inside a document look somehow similar, for example share a common side- or navbar, a header or a footer. If we think of a web-shop, we could have one document, when showing different articles and products, and a view for various pages, e.g. the dashboard with all the products, a single product view and so on. -To create a new document, a class inside [/core/Documents](/core/Documents) is created with the following scheme: +To create a new document, a class inside [/Core/Documents](/Core/Documents) is created with the following scheme: ```php namespace Documents { @@ -242,38 +245,29 @@ Of course, the head and body classes can be placed in any file, as the code migh ### Localization Currently, there are two languages specified, which are stored in the database: `en_US` and `de_DE`. -A language is dynamically loaded according to the sent `Accept-Language`-Header, but can also be set using the `lang` parameter -or [/api/language/set](/core/Api/LanguageAPI.class.php) endpoint. Localization of strings can be achieved using the [LanguageModule](/core/Objects/lang/LanguageModule.php)-Class. -Let's look at this example: +A language is dynamically loaded according to the `Accept-Language`-Header received, but can also be set using the `lang` parameter +or [/api/language/set](/Core/API/LanguageAPI.class.php) endpoint. -```php -class ExampleLangModule extends \Objects\lang\LanguageModule { - public function getEntries(string $langCode) { - $entries = array(); - switch ($langCode) { - case 'de_DE': - $entries["EXAMPLE_KEY"] = "Das ist eine Beispielübersetzung"; - $entries["Welcome"] = "Willkommen"; - break; - default: - $entries["EXAMPLE_KEY"] = "This is an example translation"; - break; - } - return $entries; - } -} -``` If any translation key is not defined, the key is returned, which means, we don't need to specify the string `Welcome` again. To access the translations, we firstly have to load the module. This is done by adding the class, or the object inside the constructor. To translate the defined strings, we can use the global `L()` function. The following code snipped shows the use of our sample language module: +**/Core/Localization/de_DE/example.php**: +```php + "Willkommen", + "EXAMPLE_KEY" => "Beispielübersetzung", +]; +``` + ```php class SomeView extends \Elements\View { public function __construct(\Elements\Document $document) { parent::__construct($document); - $this->langModules[] = ExampleModule::class; + $this->langModules[] = "example"; } public function getCode() : string{ diff --git a/cli.php b/cli.php index 633d194..3312d3b 100644 --- a/cli.php +++ b/cli.php @@ -13,12 +13,14 @@ use Core\Driver\SQL\Condition\CondIn; use Core\Driver\SQL\Expression\DateSub; use Core\Driver\SQL\SQL; use Core\Objects\ConnectionData; +use JetBrains\PhpStorm\NoReturn; -function printLine(string $line = "") { +function printLine(string $line = ""): void { echo $line . PHP_EOL; } -function _exit(string $line = "") { +#[NoReturn] +function _exit(string $line = ""): void { printLine($line); die(); } @@ -49,10 +51,11 @@ if ($database->getProperty("isDocker", false) && !is_file("/.dockerenv")) { } if ($database->getProperty("isDocker", false) && !is_file("/.dockerenv")) { + printLine("Detected docker environment in config, running docker exec..."); if (count($argv) < 3 || $argv[1] !== "db" || !in_array($argv[2], ["shell", "import", "export"])) { $containerName = $dockerYaml["services"]["php"]["container_name"]; $command = array_merge(["docker", "exec", "-it", $containerName, "php"], $argv); - $proc = proc_open($command, [1 => STDOUT, 2 => STDERR], $pipes, "/application"); + $proc = proc_open($command, [1 => STDOUT, 2 => STDERR], $pipes); exit(proc_close($proc)); } } @@ -68,8 +71,10 @@ function connectSQL(): ?SQL { return $sql; } -function printHelp() { - // TODO: help +function printHelp(array $argv): void { + printLine("=== WebBase CLI tool ==="); + printLine("Usage: "); + var_dump($argv); } function applyPatch(\Core\Driver\SQL\SQL $sql, string $patchName): bool { @@ -99,13 +104,13 @@ function applyPatch(\Core\Driver\SQL\SQL $sql, string $patchName): bool { return true; } -function handleDatabase(array $argv) { +function handleDatabase(array $argv): void { global $dockerYaml; $action = $argv[2] ?? ""; if ($action === "migrate") { $sql = connectSQL() or die(); - + _exit("Not implemented: migrate"); } else if (in_array($action, ["export", "import", "shell"])) { // database config @@ -257,7 +262,7 @@ function findPullBranch(array $output): ?string { return null; } -function onMaintenance(array $argv) { +function onMaintenance(array $argv): void { $action = $argv[2] ?? "status"; $maintenanceFile = "MAINTENANCE"; $isMaintenanceEnabled = file_exists($maintenanceFile); @@ -376,7 +381,7 @@ function getConsoleWidth(): int { return intval($width); } -function printTable(array $head, array $body) { +function printTable(array $head, array $body): void { $columns = []; foreach ($head as $key) { @@ -409,7 +414,7 @@ function printTable(array $head, array $body) { } } -function onSettings(array $argv) { +function onSettings(array $argv): void { global $context; connectSQL() or die(); $action = $argv[2] ?? "list"; @@ -455,7 +460,7 @@ function onSettings(array $argv) { } } -function onRoutes(array $argv) { +function onRoutes(array $argv): void { global $context; connectSQL() or die(); $action = $argv[2] ?? "list"; @@ -553,7 +558,7 @@ function onRoutes(array $argv) { } } -function onTest($argv) { +function onTest($argv): void { $files = glob(WEBROOT . '/test/*.test.php'); $requestedTests = array_filter(array_slice($argv, 2), function ($t) { return !startsWith($t, "-"); @@ -569,11 +574,11 @@ function onTest($argv) { $className = $baseName . "Test"; if (class_exists($className)) { - echo "=== Running $className ===" . PHP_EOL; + printLine("=== Running $className ==="); $testClass = new \PHPUnit\Framework\TestSuite(); $testClass->addTestSuite($className); $result = $testClass->run(); - echo "Done after " . $result->time() . "s" . PHP_EOL; + printLine("Done after " . $result->time() . "s"); $stats = [ "total" => $result->count(), "skipped" => $result->skippedCount(), @@ -583,16 +588,19 @@ function onTest($argv) { ]; // Summary - echo implode(", ", array_map(function ($key) use ($stats) { + printLine( + implode(", ", array_map(function ($key) use ($stats) { return "$key: " . $stats[$key]; - }, array_keys($stats))) . PHP_EOL; + }, array_keys($stats))) + ); $reports = array_merge($result->errors(), $result->failures()); foreach ($reports as $error) { $exception = $error->thrownException(); echo $error->toString(); if ($verbose) { - echo ". Stacktrace:" . PHP_EOL . $exception->getTraceAsString() . PHP_EOL; + printLine(". Stacktrace:"); + printLine($exception->getTraceAsString()); } else { $location = array_filter($exception->getTrace(), function ($t) use ($file) { return isset($t["file"]) && $t["file"] === $file; @@ -600,9 +608,9 @@ function onTest($argv) { $location = array_reverse($location); $location = array_pop($location); if ($location) { - echo " in " . substr($location["file"], strlen(WEBROOT)) . "#" . $location["line"] . PHP_EOL; + printLine(" in " . substr($location["file"], strlen(WEBROOT)) . "#" . $location["line"]); } else { - echo PHP_EOL; + printLine(); } } } @@ -610,7 +618,7 @@ function onTest($argv) { } } -function onMail($argv) { +function onMail($argv): void { global $context; $action = $argv[2] ?? null; if ($action === "send_queue") { @@ -625,7 +633,7 @@ function onMail($argv) { } } -function onImpersonate($argv) { +function onImpersonate($argv): void { global $context; if (count($argv) < 3) { @@ -651,7 +659,7 @@ function onImpersonate($argv) { $session = new \Core\Objects\DatabaseEntity\Session($context, $user); $session->setData(["2faAuthenticated" => true]); $session->update(); - echo "session=" . $session->getCookie() . PHP_EOL; + echo "session=" . $session->getUUID() . PHP_EOL; } $argv = $_SERVER['argv']; @@ -662,7 +670,7 @@ if (count($argv) < 2) { $command = $argv[1]; switch ($command) { case 'help': - printHelp(); + printHelp($argv); exit; case 'db': handleDatabase($argv); @@ -688,6 +696,6 @@ switch ($command) { default: printLine("Unknown command '$command'"); printLine(); - printHelp(); + printHelp($argv); exit; } \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 9631897..0b2b8c2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: "3.9" services: web: - container_name: web + container_name: webbase-web ports: - "80:80" volumes: @@ -13,7 +13,7 @@ services: - db - php db: - container_name: db + container_name: webbase-db image: mariadb:latest ports: - '3306:3306' @@ -21,7 +21,7 @@ services: - "MYSQL_ROOT_PASSWORD=webbasedb" - "MYSQL_DATABASE=webbase" php: - container_name: php + container_name: webbase-php volumes: - .:/application:rw - ./docker/php/php.ini:/usr/local/etc/php/php.ini:ro @@ -29,5 +29,3 @@ services: context: './docker/php/' links: - db - - diff --git a/docker/nginx/site.conf b/docker/nginx/site.conf index 2b17751..2d8d676 100644 --- a/docker/nginx/site.conf +++ b/docker/nginx/site.conf @@ -54,7 +54,7 @@ server { location ~ \.php$ { try_files $uri =404; fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass php:9000; + fastcgi_pass webbase-php:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index 3b308f3..7cfda89 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -23,7 +23,9 @@ RUN mkdir -p /usr/local/etc/php/extra/ && \ RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - && \ apt-get update && \ apt-get -y install nodejs && \ - npm install --global yarn + curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /usr/share/keyrings/yarnkey.gpg >/dev/null && \ + echo "deb [signed-by=/usr/share/keyrings/yarnkey.gpg] https://dl.yarnpkg.com/debian stable main" | tee /etc/apt/sources.list.d/yarn.list && \ + apt-get update && apt-get -y install yarn # Runkit for unit testing (no stable release available) RUN pecl install runkit7-4.0.0a6 && docker-php-ext-enable runkit7 && \ diff --git a/js/script.js b/js/script.js index 5faae0c..553f83a 100644 --- a/js/script.js +++ b/js/script.js @@ -8,24 +8,36 @@ let Core = function () { this.apiCall = function (func, params, callback) { params = typeof params !== 'undefined' ? params : {}; callback = typeof callback !== 'undefined' ? callback : function (data) {}; + let config = { method: "POST" }; + if (params instanceof FormData) { + config.body = params; + } else { + config.headers = { "Content-Type": "application/json" }; + config.body = JSON.stringify(params); + } + + const self = this; const path = '/api' + (func.startsWith('/') ? '' : '/') + func; - $.post(path, params, function (data) { - console.log(func + "(): success=" + data.success + " msg=" + data.msg); - callback.call(this, data); - }, "json").fail(function (jqXHR, textStatus, errorThrown) { - let msg = func + " Status: " + textStatus + " error thrown: " + errorThrown; - console.log("API-Function Error: " + msg); - callback.call(this, {success: false, msg: "An error occurred. API-Function: " + msg }); + fetch(path, config).then((data) => { + data.json().then(data => { + callback.call(self, data); + }).catch(reason => { + console.log("API-Function Error: " + reason); + callback.call(self, {success: false, msg: "An error occurred. API-Function: " + reason }); + }) + }).catch(reason => { + console.log("API-Function Error: " + reason); + callback.call(self, {success: false, msg: "An error occurred. API-Function: " + reason }); }); }; this.getCookie = function (cname) { - var name = cname + "="; - var decodedCookie = decodeURIComponent(document.cookie); - var ca = decodedCookie.split(";"); - for (var i = 0; i < ca.length; i++) { - var c = ca[i]; + let name = cname + "="; + let decodedCookie = decodeURIComponent(document.cookie); + let ca = decodedCookie.split(";"); + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; while (c.charAt(0) === ' ') { c = c.substring(1); } @@ -84,9 +96,9 @@ let Core = function () { return null; }; - this.setParameter = function (param, newvalue) { - newvalue = typeof newvalue !== 'undefined' ? newvalue : ''; - this.parameters[param] = newvalue; + this.setParameter = function (param, newValue) { + newValue = typeof newValue !== 'undefined' ? newValue : ''; + this.parameters[param] = newValue; this.updateUrl(); }; @@ -95,15 +107,13 @@ let Core = function () { if (this.url.indexOf('?') === -1) return; - var paramString = this.url.substring(this.url.indexOf('?') + 1); - var split = paramString.split('&'); - for (var i = 0; i < split.length; i++) { - var param = split[i]; - var index = param.indexOf('='); + let paramString = this.url.substring(this.url.indexOf('?') + 1); + let split = paramString.split('&'); + for (let i = 0; i < split.length; i++) { + let param = split[i]; + let index = param.indexOf('='); if (index !== -1) { - var key = param.substring(0, index); - var val = param.substring(index + 1); - this.parameters[key] = val; + this.parameters[param.substring(0, index)] = param.substring(index + 1); } else this.parameters[param] = ''; } @@ -112,7 +122,7 @@ let Core = function () { this.updateUrl = function () { this.clearUrl(); let i = 0; - for (var parameter in this.parameters) { + for (let parameter in this.parameters) { this.url += (i === 0 ? "?" : "&") + parameter; if (this.parameters.hasOwnProperty(parameter) && this.parameters[parameter].toString().length > 0) { this.url += "=" + this.parameters[parameter]; @@ -127,10 +137,12 @@ let Core = function () { }; this.clearUrl = function () { - if (this.url.indexOf('#') !== -1) + if (this.url.indexOf('#') !== -1) { this.url = this.url.substring(0, this.url.indexOf('#')); - if (this.url.indexOf('?') !== -1) + } + if (this.url.indexOf('?') !== -1) { this.url = this.url.substring(0, this.url.indexOf('?')); + } }; this.getJsonDateTime = function (date) { diff --git a/react/admin-panel/src/AdminDashboard.jsx b/react/admin-panel/src/AdminDashboard.jsx index d280da5..155b20b 100644 --- a/react/admin-panel/src/AdminDashboard.jsx +++ b/react/admin-panel/src/AdminDashboard.jsx @@ -43,7 +43,7 @@ export default function AdminDashboard(props) { }, []); useEffect(() => { - requestModules(api, ["general", "admin"], currentLocale).then(data => { + requestModules(api, ["general", "admin", "account"], currentLocale).then(data => { if (!data.success) { alert(data.msg); } diff --git a/react/adminPanel.old/src/views/settings.js b/react/adminPanel.old/src/views/settings.js index 7b68b43..9016e57 100644 --- a/react/adminPanel.old/src/views/settings.js +++ b/react/adminPanel.old/src/views/settings.js @@ -54,8 +54,7 @@ export default class Settings extends React.Component { this.hiddenKeys = [ "recaptcha_private_key", - "mail_password", - "jwt_secret" + "mail_password" ]; }