This commit is contained in:
Roman 2024-03-24 17:36:16 +01:00
parent aece0cb92a
commit 2ef4de0dba
17 changed files with 139 additions and 255 deletions

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="PhpProjectSharedConfiguration" php_language_level="8.0"> <component name="PhpProjectSharedConfiguration" php_language_level="8.1">
<option name="suggestChangeDefaultLanguageLevel" value="false" /> <option name="suggestChangeDefaultLanguageLevel" value="false" />
</component> </component>
</project> </project>

@ -9,7 +9,7 @@ use Core\Objects\Search\SearchQuery;
class Search extends Request { 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, [ parent::__construct($context, $externalCall, [
"text" => new StringType("text", 32) "text" => new StringType("text", 32)
]); ]);

@ -155,37 +155,4 @@ namespace Core\API\Settings {
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to modify site 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");
}
}
} }

@ -26,11 +26,6 @@ class Settings {
private array $allowedExtensions; private array $allowedExtensions;
private string $timeZone; private string $timeZone;
// jwt
private ?string $jwtPublicKey;
private ?string $jwtSecretKey;
private string $jwtAlgorithm;
// recaptcha // recaptcha
private bool $recaptchaEnabled; private bool $recaptchaEnabled;
private string $recaptchaPublicKey; 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 { public function isInstalled(): bool {
return $this->installationComplete; return $this->installationComplete;
} }
@ -124,11 +103,6 @@ class Settings {
$settings->registrationAllowed = false; $settings->registrationAllowed = false;
$settings->timeZone = date_default_timezone_get(); $settings->timeZone = date_default_timezone_get();
// JWT
$settings->jwtSecretKey = null;
$settings->jwtPublicKey = null;
$settings->jwtAlgorithm = "HS256";
// Recaptcha // Recaptcha
$settings->recaptchaEnabled = false; $settings->recaptchaEnabled = false;
$settings->recaptchaPublicKey = ""; $settings->recaptchaPublicKey = "";
@ -143,50 +117,6 @@ class Settings {
return $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 { public function loadFromDatabase(Context $context): bool {
$this->logger = new Logger("Settings", $context->getSQL()); $this->logger = new Logger("Settings", $context->getSQL());
$req = new \Core\API\Settings\Get($context); $req = new \Core\API\Settings\Get($context);
@ -199,9 +129,6 @@ class Settings {
$this->registrationAllowed = $result["user_registration_enabled"] ?? $this->registrationAllowed; $this->registrationAllowed = $result["user_registration_enabled"] ?? $this->registrationAllowed;
$this->installationComplete = $result["installation_completed"] ?? $this->installationComplete; $this->installationComplete = $result["installation_completed"] ?? $this->installationComplete;
$this->timeZone = $result["time_zone"] ?? $this->timeZone; $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->recaptchaEnabled = $result["recaptcha_enabled"] ?? $this->recaptchaEnabled;
$this->recaptchaPublicKey = $result["recaptcha_public_key"] ?? $this->recaptchaPublicKey; $this->recaptchaPublicKey = $result["recaptcha_public_key"] ?? $this->recaptchaPublicKey;
$this->recaptchaPrivateKey = $result["recaptcha_private_key"] ?? $this->recaptchaPrivateKey; $this->recaptchaPrivateKey = $result["recaptcha_private_key"] ?? $this->recaptchaPrivateKey;
@ -210,13 +137,6 @@ class Settings {
$this->mailFooter = $result["mail_footer"] ?? $this->mailFooter; $this->mailFooter = $result["mail_footer"] ?? $this->mailFooter;
$this->mailAsync = $result["mail_async"] ?? $this->mailAsync; $this->mailAsync = $result["mail_async"] ?? $this->mailAsync;
$this->allowedExtensions = explode(",", $result["allowed_extensions"] ?? strtolower(implode(",", $this->allowedExtensions))); $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); date_default_timezone_set($this->timeZone);
} }
@ -229,9 +149,6 @@ class Settings {
->addRow("user_registration_enabled", $this->registrationAllowed ? "1" : "0", false, false) ->addRow("user_registration_enabled", $this->registrationAllowed ? "1" : "0", false, false)
->addRow("installation_completed", $this->installationComplete ? "1" : "0", true, true) ->addRow("installation_completed", $this->installationComplete ? "1" : "0", true, true)
->addRow("time_zone", $this->timeZone, false, false) ->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_enabled", $this->recaptchaEnabled ? "1" : "0", false, false)
->addRow("recaptcha_public_key", $this->recaptchaPublicKey, false, false) ->addRow("recaptcha_public_key", $this->recaptchaPublicKey, false, false)
->addRow("recaptcha_private_key", $this->recaptchaPrivateKey, true, false) ->addRow("recaptcha_private_key", $this->recaptchaPrivateKey, true, false)

@ -6,7 +6,6 @@
"christian-riesen/base32": "^1.6", "christian-riesen/base32": "^1.6",
"spomky-labs/cbor-php": "2.1.0", "spomky-labs/cbor-php": "2.1.0",
"web-auth/cose-lib": "3.3.12", "web-auth/cose-lib": "3.3.12",
"firebase/php-jwt": "^6.2",
"html2text/html2text": "^4.3" "html2text/html2text": "^4.3"
}, },
"require-dev": { "require-dev": {

@ -32,6 +32,7 @@ return [
"not_supported" => "Nicht unterstützt", "not_supported" => "Nicht unterstützt",
"yes" => "Ja", "yes" => "Ja",
"no" => "Nein", "no" => "Nein",
"create_new" => "Erstellen",
# dialog / actions # dialog / actions
"action" => "Aktion", "action" => "Aktion",

@ -15,6 +15,7 @@ return [
"not_supported" => "Not supported", "not_supported" => "Not supported",
"yes" => "Yes", "yes" => "Yes",
"no" => "No", "no" => "No",
"create_new" => "Create",
# dialog / actions # dialog / actions
"action" => "Action", "action" => "Action",

@ -9,7 +9,6 @@ use Core\Driver\SQL\Condition\CondLike;
use Core\Driver\SQL\Condition\CondOr; use Core\Driver\SQL\Condition\CondOr;
use Core\Driver\SQL\Join\InnerJoin; use Core\Driver\SQL\Join\InnerJoin;
use Core\Driver\SQL\SQL; use Core\Driver\SQL\SQL;
use Firebase\JWT\JWT;
use Core\Objects\DatabaseEntity\Language; use Core\Objects\DatabaseEntity\Language;
use Core\Objects\DatabaseEntity\Session; use Core\Objects\DatabaseEntity\Session;
use Core\Objects\DatabaseEntity\User; use Core\Objects\DatabaseEntity\User;
@ -99,28 +98,14 @@ class Context {
session_write_close(); session_write_close();
} }
private function loadSession(int $userId, int $sessionId): void { private function loadSession(string $sessionUUID): void {
$this->session = Session::init($this, $userId, $sessionId); $this->session = Session::init($this, $sessionUUID);
$this->user = $this->session?->getUser(); $this->user = $this->session?->getUser();
} }
public function parseCookies(): void { public function parseCookies(): void {
if (isset($_COOKIE['session']) && is_string($_COOKIE['session']) && !empty($_COOKIE['session'])) { if (isset($_COOKIE['session']) && is_string($_COOKIE['session']) && !empty($_COOKIE['session'])) {
try { $this->loadSession($_COOKIE['session']);
$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
}
} }
// set language by priority: 1. GET parameter, 2. cookie, 3. user's settings // set language by priority: 1. GET parameter, 2. cookie, 3. user's settings

@ -4,7 +4,6 @@ namespace Core\Objects\DatabaseEntity;
use DateTime; use DateTime;
use Exception; use Exception;
use Firebase\JWT\JWT;
use Core\Objects\Context; use Core\Objects\Context;
use Core\Objects\DatabaseEntity\Attribute\DefaultValue; use Core\Objects\DatabaseEntity\Attribute\DefaultValue;
use Core\Objects\DatabaseEntity\Attribute\Json; use Core\Objects\DatabaseEntity\Attribute\Json;
@ -21,6 +20,7 @@ class Session extends DatabaseEntity {
private User $user; private User $user;
private DateTime $expires; private DateTime $expires;
#[MaxLength(45)] private string $ipAddress; #[MaxLength(45)] private string $ipAddress;
#[MaxLength(36)] private string $uuid;
#[DefaultValue(true)] private bool $active; #[DefaultValue(true)] private bool $active;
#[MaxLength(64)] private ?string $os; #[MaxLength(64)] private ?string $os;
#[MaxLength(64)] private ?string $browser; #[MaxLength(64)] private ?string $browser;
@ -32,15 +32,21 @@ class Session extends DatabaseEntity {
parent::__construct(); parent::__construct();
$this->context = $context; $this->context = $context;
$this->user = $user; $this->user = $user;
$this->uuid = uuidv4();
$this->stayLoggedIn = false; $this->stayLoggedIn = false;
$this->csrfToken = $csrfToken ?? generateRandomString(16); $this->csrfToken = $csrfToken ?? generateRandomString(16);
$this->expires = (new DateTime())->modify(sprintf("+%d second", Session::DURATION)); $this->expires = (new DateTime())->modify(sprintf("+%d second", Session::DURATION));
$this->active = true; $this->active = true;
} }
public static function init(Context $context, int $userId, int $sessionId): ?Session { public static function init(Context $context, string $sessionUUID): ?Session {
$session = Session::find($context->getSQL(), $sessionId, true, true); $sql = $context->getSQL();
if (!$session || !$session->active || $session->user->getId() !== $userId) { $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; return null;
} }
@ -82,18 +88,13 @@ class Session extends DatabaseEntity {
} }
} }
public function getCookie(): string { public function getUUID(): string {
$this->updateMetaData(); return $this->uuid;
$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 sendCookie(string $domain) { public function sendCookie(string $domain) {
$sessionCookie = $this->getCookie();
$secure = strcmp(getProtocol(), "https") === 0; $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 { public function getExpiresTime(): int {

@ -3,7 +3,7 @@
Web-Base is a php framework which provides basic web functionalities and a modern ReactJS Admin Dashboard. Web-Base is a php framework which provides basic web functionalities and a modern ReactJS Admin Dashboard.
### Requirements ### Requirements
- PHP >= 7.4 - PHP >= 8.1
- One of these php extensions: mysqli, postgres - One of these php extensions: mysqli, postgres
- Apache/nginx or docker - 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 3. Open the webapp in your browser and follow the installation guide
### Docker Installation ### Docker Installation
1. `docker-compose up` 1. `docker-compose build`
2. Open the webapp in your browser and follow the installation guide 2. `docker-compose up`
3. Open the webapp in your browser and follow the installation guide
### Afterwards ### Afterwards
For any changes made in [/adminPanel](/adminPanel), run: For any changes made in [/react](/react), run:
1. once: `npm i` 1. once: `yarn i`
2. build: `npm run build` 2. build: `yarn run build`
The compiled dist files will be automatically moved to `/js`. 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` To fulfill the requirements of data deletion for **GDPR**, add the following line to your `/etc/crontab`
or any other cron file: or any other cron file:
``` ```
@ -51,29 +58,25 @@ or any other cron file:
### Adding API-Endpoints ### 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 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: 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: Each endpoint is represented by a class inheriting the [Request Class](/Core/API/Request.class.php). An example endpoint looks like this:
```php ```php
namespace Api; namespace Core\API;
use Core\API\Parameter\Parameter; use Core\Objects\Context;
use Core\Objects\DatabaseEntity\User;
class SingleEndpoint extends Request { class SingleEndpoint extends Request {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($context, $externalCall, array(
"someParameter" => new Parameter("someParameter", Parameter::TYPE_INT, true, 100) "someParameter" => new Parameter("someParameter", Parameter::TYPE_INT, true, 100)
)); ));
$this->forbidMethod("POST"); $this->forbidMethod("POST");
} }
public function execute($values = array()): bool { public function _execute(): bool {
if (!parent::execute($values)) {
return false;
}
$this->result['someAttribute'] = $this->getParam("someParameter") * 2; $this->result['someAttribute'] = $this->getParam("someParameter") * 2;
return true; 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. 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. 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 ### 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`) 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 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 ### Using the API internally
@ -109,11 +112,11 @@ can be used by creating the desired request object, and calling the execute func
```php ```php
$req = new \Core\API\Mail\Send($context); $req = new \Core\API\Mail\Send($context);
$success = $req->execute(array( $success = $req->execute([
"to" => "mail@example.org", "to" => "mail@example.org",
"subject" => "Example Mail", "subject" => "Example Mail",
"body" => "This is an example mail" "body" => "This is an example mail"
)); ]);
if (!$success) { if (!$success) {
echo $req->getLastError(); 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. 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 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 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 ```php
namespace Core\Configuration\Patch; 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 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, 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. 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 ```php
namespace Documents { namespace Documents {
@ -242,38 +245,29 @@ Of course, the head and body classes can be placed in any file, as the code migh
### Localization ### Localization
Currently, there are two languages specified, which are stored in the database: `en_US` and `de_DE`. 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 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. Localization of strings can be achieved using the [LanguageModule](/core/Objects/lang/LanguageModule.php)-Class. or [/api/language/set](/Core/API/LanguageAPI.class.php) endpoint.
Let's look at this example:
```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, 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. 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 To translate the defined strings, we can use the global `L()` function. The following code snipped shows the use of
our sample language module: our sample language module:
**/Core/Localization/de_DE/example.php**:
```php
<?php
return [
"Welcome" => "Willkommen",
"EXAMPLE_KEY" => "Beispielübersetzung",
];
```
```php ```php
class SomeView extends \Elements\View { class SomeView extends \Elements\View {
public function __construct(\Elements\Document $document) { public function __construct(\Elements\Document $document) {
parent::__construct($document); parent::__construct($document);
$this->langModules[] = ExampleModule::class; $this->langModules[] = "example";
} }
public function getCode() : string{ public function getCode() : string{

56
cli.php

@ -13,12 +13,14 @@ use Core\Driver\SQL\Condition\CondIn;
use Core\Driver\SQL\Expression\DateSub; use Core\Driver\SQL\Expression\DateSub;
use Core\Driver\SQL\SQL; use Core\Driver\SQL\SQL;
use Core\Objects\ConnectionData; use Core\Objects\ConnectionData;
use JetBrains\PhpStorm\NoReturn;
function printLine(string $line = "") { function printLine(string $line = ""): void {
echo $line . PHP_EOL; echo $line . PHP_EOL;
} }
function _exit(string $line = "") { #[NoReturn]
function _exit(string $line = ""): void {
printLine($line); printLine($line);
die(); die();
} }
@ -49,10 +51,11 @@ if ($database->getProperty("isDocker", false) && !is_file("/.dockerenv")) {
} }
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"])) { if (count($argv) < 3 || $argv[1] !== "db" || !in_array($argv[2], ["shell", "import", "export"])) {
$containerName = $dockerYaml["services"]["php"]["container_name"]; $containerName = $dockerYaml["services"]["php"]["container_name"];
$command = array_merge(["docker", "exec", "-it", $containerName, "php"], $argv); $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)); exit(proc_close($proc));
} }
} }
@ -68,8 +71,10 @@ function connectSQL(): ?SQL {
return $sql; return $sql;
} }
function printHelp() { function printHelp(array $argv): void {
// TODO: help printLine("=== WebBase CLI tool ===");
printLine("Usage: ");
var_dump($argv);
} }
function applyPatch(\Core\Driver\SQL\SQL $sql, string $patchName): bool { 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; return true;
} }
function handleDatabase(array $argv) { function handleDatabase(array $argv): void {
global $dockerYaml; global $dockerYaml;
$action = $argv[2] ?? ""; $action = $argv[2] ?? "";
if ($action === "migrate") { if ($action === "migrate") {
$sql = connectSQL() or die(); $sql = connectSQL() or die();
_exit("Not implemented: migrate");
} else if (in_array($action, ["export", "import", "shell"])) { } else if (in_array($action, ["export", "import", "shell"])) {
// database config // database config
@ -257,7 +262,7 @@ function findPullBranch(array $output): ?string {
return null; return null;
} }
function onMaintenance(array $argv) { function onMaintenance(array $argv): void {
$action = $argv[2] ?? "status"; $action = $argv[2] ?? "status";
$maintenanceFile = "MAINTENANCE"; $maintenanceFile = "MAINTENANCE";
$isMaintenanceEnabled = file_exists($maintenanceFile); $isMaintenanceEnabled = file_exists($maintenanceFile);
@ -376,7 +381,7 @@ function getConsoleWidth(): int {
return intval($width); return intval($width);
} }
function printTable(array $head, array $body) { function printTable(array $head, array $body): void {
$columns = []; $columns = [];
foreach ($head as $key) { 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; global $context;
connectSQL() or die(); connectSQL() or die();
$action = $argv[2] ?? "list"; $action = $argv[2] ?? "list";
@ -455,7 +460,7 @@ function onSettings(array $argv) {
} }
} }
function onRoutes(array $argv) { function onRoutes(array $argv): void {
global $context; global $context;
connectSQL() or die(); connectSQL() or die();
$action = $argv[2] ?? "list"; $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'); $files = glob(WEBROOT . '/test/*.test.php');
$requestedTests = array_filter(array_slice($argv, 2), function ($t) { $requestedTests = array_filter(array_slice($argv, 2), function ($t) {
return !startsWith($t, "-"); return !startsWith($t, "-");
@ -569,11 +574,11 @@ function onTest($argv) {
$className = $baseName . "Test"; $className = $baseName . "Test";
if (class_exists($className)) { if (class_exists($className)) {
echo "=== Running $className ===" . PHP_EOL; printLine("=== Running $className ===");
$testClass = new \PHPUnit\Framework\TestSuite(); $testClass = new \PHPUnit\Framework\TestSuite();
$testClass->addTestSuite($className); $testClass->addTestSuite($className);
$result = $testClass->run(); $result = $testClass->run();
echo "Done after " . $result->time() . "s" . PHP_EOL; printLine("Done after " . $result->time() . "s");
$stats = [ $stats = [
"total" => $result->count(), "total" => $result->count(),
"skipped" => $result->skippedCount(), "skipped" => $result->skippedCount(),
@ -583,16 +588,19 @@ function onTest($argv) {
]; ];
// Summary // Summary
echo implode(", ", array_map(function ($key) use ($stats) { printLine(
implode(", ", array_map(function ($key) use ($stats) {
return "$key: " . $stats[$key]; return "$key: " . $stats[$key];
}, array_keys($stats))) . PHP_EOL; }, array_keys($stats)))
);
$reports = array_merge($result->errors(), $result->failures()); $reports = array_merge($result->errors(), $result->failures());
foreach ($reports as $error) { foreach ($reports as $error) {
$exception = $error->thrownException(); $exception = $error->thrownException();
echo $error->toString(); echo $error->toString();
if ($verbose) { if ($verbose) {
echo ". Stacktrace:" . PHP_EOL . $exception->getTraceAsString() . PHP_EOL; printLine(". Stacktrace:");
printLine($exception->getTraceAsString());
} else { } else {
$location = array_filter($exception->getTrace(), function ($t) use ($file) { $location = array_filter($exception->getTrace(), function ($t) use ($file) {
return isset($t["file"]) && $t["file"] === $file; return isset($t["file"]) && $t["file"] === $file;
@ -600,9 +608,9 @@ function onTest($argv) {
$location = array_reverse($location); $location = array_reverse($location);
$location = array_pop($location); $location = array_pop($location);
if ($location) { if ($location) {
echo " in " . substr($location["file"], strlen(WEBROOT)) . "#" . $location["line"] . PHP_EOL; printLine(" in " . substr($location["file"], strlen(WEBROOT)) . "#" . $location["line"]);
} else { } else {
echo PHP_EOL; printLine();
} }
} }
} }
@ -610,7 +618,7 @@ function onTest($argv) {
} }
} }
function onMail($argv) { function onMail($argv): void {
global $context; global $context;
$action = $argv[2] ?? null; $action = $argv[2] ?? null;
if ($action === "send_queue") { if ($action === "send_queue") {
@ -625,7 +633,7 @@ function onMail($argv) {
} }
} }
function onImpersonate($argv) { function onImpersonate($argv): void {
global $context; global $context;
if (count($argv) < 3) { if (count($argv) < 3) {
@ -651,7 +659,7 @@ function onImpersonate($argv) {
$session = new \Core\Objects\DatabaseEntity\Session($context, $user); $session = new \Core\Objects\DatabaseEntity\Session($context, $user);
$session->setData(["2faAuthenticated" => true]); $session->setData(["2faAuthenticated" => true]);
$session->update(); $session->update();
echo "session=" . $session->getCookie() . PHP_EOL; echo "session=" . $session->getUUID() . PHP_EOL;
} }
$argv = $_SERVER['argv']; $argv = $_SERVER['argv'];
@ -662,7 +670,7 @@ if (count($argv) < 2) {
$command = $argv[1]; $command = $argv[1];
switch ($command) { switch ($command) {
case 'help': case 'help':
printHelp(); printHelp($argv);
exit; exit;
case 'db': case 'db':
handleDatabase($argv); handleDatabase($argv);
@ -688,6 +696,6 @@ switch ($command) {
default: default:
printLine("Unknown command '$command'"); printLine("Unknown command '$command'");
printLine(); printLine();
printHelp(); printHelp($argv);
exit; exit;
} }

@ -1,7 +1,7 @@
version: "3.9" version: "3.9"
services: services:
web: web:
container_name: web container_name: webbase-web
ports: ports:
- "80:80" - "80:80"
volumes: volumes:
@ -13,7 +13,7 @@ services:
- db - db
- php - php
db: db:
container_name: db container_name: webbase-db
image: mariadb:latest image: mariadb:latest
ports: ports:
- '3306:3306' - '3306:3306'
@ -21,7 +21,7 @@ services:
- "MYSQL_ROOT_PASSWORD=webbasedb" - "MYSQL_ROOT_PASSWORD=webbasedb"
- "MYSQL_DATABASE=webbase" - "MYSQL_DATABASE=webbase"
php: php:
container_name: php container_name: webbase-php
volumes: volumes:
- .:/application:rw - .:/application:rw
- ./docker/php/php.ini:/usr/local/etc/php/php.ini:ro - ./docker/php/php.ini:/usr/local/etc/php/php.ini:ro
@ -29,5 +29,3 @@ services:
context: './docker/php/' context: './docker/php/'
links: links:
- db - db

@ -54,7 +54,7 @@ server {
location ~ \.php$ { location ~ \.php$ {
try_files $uri =404; try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php:9000; fastcgi_pass webbase-php:9000;
fastcgi_index index.php; fastcgi_index index.php;
include fastcgi_params; include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

@ -23,7 +23,9 @@ RUN mkdir -p /usr/local/etc/php/extra/ && \
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - && \ RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - && \
apt-get update && \ apt-get update && \
apt-get -y install nodejs && \ 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) # Runkit for unit testing (no stable release available)
RUN pecl install runkit7-4.0.0a6 && docker-php-ext-enable runkit7 && \ RUN pecl install runkit7-4.0.0a6 && docker-php-ext-enable runkit7 && \

@ -8,24 +8,36 @@ let Core = function () {
this.apiCall = function (func, params, callback) { this.apiCall = function (func, params, callback) {
params = typeof params !== 'undefined' ? params : {}; params = typeof params !== 'undefined' ? params : {};
callback = typeof callback !== 'undefined' ? callback : function (data) {}; 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; const path = '/api' + (func.startsWith('/') ? '' : '/') + func;
$.post(path, params, function (data) { fetch(path, config).then((data) => {
console.log(func + "(): success=" + data.success + " msg=" + data.msg); data.json().then(data => {
callback.call(this, data); callback.call(self, data);
}, "json").fail(function (jqXHR, textStatus, errorThrown) { }).catch(reason => {
let msg = func + " Status: " + textStatus + " error thrown: " + errorThrown; console.log("API-Function Error: " + reason);
console.log("API-Function Error: " + msg); callback.call(self, {success: false, msg: "An error occurred. API-Function: " + reason });
callback.call(this, {success: false, msg: "An error occurred. API-Function: " + msg }); })
}).catch(reason => {
console.log("API-Function Error: " + reason);
callback.call(self, {success: false, msg: "An error occurred. API-Function: " + reason });
}); });
}; };
this.getCookie = function (cname) { this.getCookie = function (cname) {
var name = cname + "="; let name = cname + "=";
var decodedCookie = decodeURIComponent(document.cookie); let decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(";"); let ca = decodedCookie.split(";");
for (var i = 0; i < ca.length; i++) { for (let i = 0; i < ca.length; i++) {
var c = ca[i]; let c = ca[i];
while (c.charAt(0) === ' ') { while (c.charAt(0) === ' ') {
c = c.substring(1); c = c.substring(1);
} }
@ -84,9 +96,9 @@ let Core = function () {
return null; return null;
}; };
this.setParameter = function (param, newvalue) { this.setParameter = function (param, newValue) {
newvalue = typeof newvalue !== 'undefined' ? newvalue : ''; newValue = typeof newValue !== 'undefined' ? newValue : '';
this.parameters[param] = newvalue; this.parameters[param] = newValue;
this.updateUrl(); this.updateUrl();
}; };
@ -95,15 +107,13 @@ let Core = function () {
if (this.url.indexOf('?') === -1) if (this.url.indexOf('?') === -1)
return; return;
var paramString = this.url.substring(this.url.indexOf('?') + 1); let paramString = this.url.substring(this.url.indexOf('?') + 1);
var split = paramString.split('&'); let split = paramString.split('&');
for (var i = 0; i < split.length; i++) { for (let i = 0; i < split.length; i++) {
var param = split[i]; let param = split[i];
var index = param.indexOf('='); let index = param.indexOf('=');
if (index !== -1) { if (index !== -1) {
var key = param.substring(0, index); this.parameters[param.substring(0, index)] = param.substring(index + 1);
var val = param.substring(index + 1);
this.parameters[key] = val;
} else } else
this.parameters[param] = ''; this.parameters[param] = '';
} }
@ -112,7 +122,7 @@ let Core = function () {
this.updateUrl = function () { this.updateUrl = function () {
this.clearUrl(); this.clearUrl();
let i = 0; let i = 0;
for (var parameter in this.parameters) { for (let parameter in this.parameters) {
this.url += (i === 0 ? "?" : "&") + parameter; this.url += (i === 0 ? "?" : "&") + parameter;
if (this.parameters.hasOwnProperty(parameter) && this.parameters[parameter].toString().length > 0) { if (this.parameters.hasOwnProperty(parameter) && this.parameters[parameter].toString().length > 0) {
this.url += "=" + this.parameters[parameter]; this.url += "=" + this.parameters[parameter];
@ -127,10 +137,12 @@ let Core = function () {
}; };
this.clearUrl = function () { this.clearUrl = function () {
if (this.url.indexOf('#') !== -1) if (this.url.indexOf('#') !== -1) {
this.url = this.url.substring(0, this.url.indexOf('#')); 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.url = this.url.substring(0, this.url.indexOf('?'));
}
}; };
this.getJsonDateTime = function (date) { this.getJsonDateTime = function (date) {

@ -43,7 +43,7 @@ export default function AdminDashboard(props) {
}, []); }, []);
useEffect(() => { useEffect(() => {
requestModules(api, ["general", "admin"], currentLocale).then(data => { requestModules(api, ["general", "admin", "account"], currentLocale).then(data => {
if (!data.success) { if (!data.success) {
alert(data.msg); alert(data.msg);
} }

@ -54,8 +54,7 @@ export default class Settings extends React.Component {
this.hiddenKeys = [ this.hiddenKeys = [
"recaptcha_private_key", "recaptcha_private_key",
"mail_password", "mail_password"
"jwt_secret"
]; ];
} }