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"?>
<project version="4">
<component name="PhpProjectSharedConfiguration" php_language_level="8.0">
<component name="PhpProjectSharedConfiguration" php_language_level="8.1">
<option name="suggestChangeDefaultLanguageLevel" value="false" />
</component>
</project>

@ -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)
]);

@ -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");
}
}
}

@ -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)

@ -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": {

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

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

@ -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

@ -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 {

@ -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
<?php
return [
"Welcome" => "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{

56
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;
}

@ -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

@ -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;

@ -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 && \

@ -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) {

@ -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);
}

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