frontend & backend update
This commit is contained in:
@@ -18,6 +18,7 @@ namespace Core\API\ApiKey {
|
||||
use Core\API\Parameter\Parameter;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Condition\CondAnd;
|
||||
use Core\Driver\SQL\Query\Insert;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\DatabaseEntity\ApiKey;
|
||||
|
||||
@@ -46,6 +47,10 @@ namespace Core\API\ApiKey {
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [], "Allows users to create new API-Keys");
|
||||
}
|
||||
}
|
||||
|
||||
class Fetch extends ApiKeyAPI {
|
||||
@@ -82,6 +87,10 @@ namespace Core\API\ApiKey {
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [], "Allows users to fetch new API-Key");
|
||||
}
|
||||
}
|
||||
|
||||
class Refresh extends ApiKeyAPI {
|
||||
@@ -112,6 +121,10 @@ namespace Core\API\ApiKey {
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [], "Allows users to refresh API-Key");
|
||||
}
|
||||
}
|
||||
|
||||
class Revoke extends ApiKeyAPI {
|
||||
@@ -138,5 +151,9 @@ namespace Core\API\ApiKey {
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [], "Allows users to revoke API-Key");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,10 @@ namespace Core\API\Database {
|
||||
|
||||
use Core\API\DatabaseAPI;
|
||||
use Core\API\Parameter\StringType;
|
||||
use Core\Driver\SQL\Query\Insert;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\DatabaseEntity\Controller\DatabaseEntity;
|
||||
use Core\Objects\DatabaseEntity\Group;
|
||||
|
||||
class Status extends DatabaseAPI {
|
||||
|
||||
@@ -29,6 +31,10 @@ namespace Core\API\Database {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to view the database status");
|
||||
}
|
||||
}
|
||||
|
||||
class Migrate extends DatabaseAPI {
|
||||
@@ -98,5 +104,9 @@ namespace Core\API\Database {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to migrate the database structure");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,8 +36,8 @@ namespace Core\API\Groups {
|
||||
use Core\Driver\SQL\Expression\Alias;
|
||||
use Core\Driver\SQL\Expression\Count;
|
||||
use Core\Driver\SQL\Join\InnerJoin;
|
||||
use Core\Driver\SQL\Query\Insert;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\DatabaseEntity\Controller\NMRelation;
|
||||
use Core\Objects\DatabaseEntity\Group;
|
||||
use Core\Objects\DatabaseEntity\User;
|
||||
|
||||
@@ -83,6 +83,10 @@ namespace Core\API\Groups {
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN, Group::SUPPORT], "Allows users to fetch available groups");
|
||||
}
|
||||
}
|
||||
|
||||
class Get extends GroupsAPI {
|
||||
@@ -106,6 +110,10 @@ namespace Core\API\Groups {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN, Group::SUPPORT], "Allows users to get details about a group");
|
||||
}
|
||||
}
|
||||
|
||||
class GetMembers extends GroupsAPI {
|
||||
@@ -142,6 +150,9 @@ namespace Core\API\Groups {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN, Group::SUPPORT], "Allows users to fetch members of a group");
|
||||
}
|
||||
}
|
||||
|
||||
class Create extends GroupsAPI {
|
||||
@@ -182,6 +193,10 @@ namespace Core\API\Groups {
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to create a new group");
|
||||
}
|
||||
}
|
||||
|
||||
class Delete extends GroupsAPI {
|
||||
@@ -209,6 +224,10 @@ namespace Core\API\Groups {
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to delete a group");
|
||||
}
|
||||
}
|
||||
|
||||
class AddMember extends GroupsAPI {
|
||||
@@ -247,6 +266,10 @@ namespace Core\API\Groups {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to add members to a group");
|
||||
}
|
||||
}
|
||||
|
||||
class RemoveMember extends GroupsAPI {
|
||||
@@ -285,6 +308,9 @@ namespace Core\API\Groups {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to remove members from a group");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,9 @@ namespace Core\API\Logs {
|
||||
use Core\Driver\SQL\Column\Column;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Condition\CondIn;
|
||||
use Core\Driver\SQL\Query\Insert;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\DatabaseEntity\Group;
|
||||
use Core\Objects\DatabaseEntity\SystemLog;
|
||||
|
||||
class Get extends LogsAPI {
|
||||
@@ -118,6 +120,10 @@ namespace Core\API\Logs {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to fetch system logs");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -46,6 +46,8 @@ namespace Core\API\Mail {
|
||||
use Core\API\MailAPI;
|
||||
use Core\API\Parameter\Parameter;
|
||||
use Core\API\Parameter\StringType;
|
||||
use Core\Driver\SQL\Query\Insert;
|
||||
use Core\Objects\DatabaseEntity\Group;
|
||||
use Core\Objects\DatabaseEntity\MailQueueItem;
|
||||
use DateTimeInterface;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
@@ -78,6 +80,10 @@ namespace Core\API\Mail {
|
||||
$this->lastError = $req->getLastError();
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to send a test email to verify configuration");
|
||||
}
|
||||
}
|
||||
|
||||
class Send extends MailAPI {
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace Core\API\Permission {
|
||||
use Core\Driver\SQL\Condition\CondIn;
|
||||
use Core\Driver\SQL\Condition\CondLike;
|
||||
use Core\Driver\SQL\Condition\CondNot;
|
||||
use Core\Driver\SQL\Query\Insert;
|
||||
use Core\Driver\SQL\Strategy\UpdateStrategy;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\DatabaseEntity\Group;
|
||||
@@ -73,6 +74,16 @@ namespace Core\API\Permission {
|
||||
http_response_code(401);
|
||||
return $this->createError("Permission denied.");
|
||||
}
|
||||
|
||||
// user would have required groups, check for 2fa-state
|
||||
if ($currentUser) {
|
||||
$tfaToken = $currentUser->getTwoFactorToken();
|
||||
if ($tfaToken && $tfaToken->isConfirmed() && !$tfaToken->isAuthenticated()) {
|
||||
$this->lastError = '2FA-Authorization is required';
|
||||
http_response_code(401);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
@@ -127,6 +138,10 @@ namespace Core\API\Permission {
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to fetch API permissions");
|
||||
}
|
||||
}
|
||||
|
||||
class Save extends PermissionAPI {
|
||||
@@ -192,5 +207,10 @@ namespace Core\API\Permission {
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN],
|
||||
"Allows users to modify API permissions. This is restricted to the administrator and cannot be changed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,10 @@
|
||||
namespace Core\API;
|
||||
|
||||
use Core\Driver\Logger\Logger;
|
||||
use Core\Driver\SQL\Query\Insert;
|
||||
use Core\Objects\Context;
|
||||
use PhpMqtt\Client\MqttClient;
|
||||
|
||||
/**
|
||||
* TODO: we need following features, probably as abstract/generic class/method:
|
||||
* - easy way for pagination (select with limit/offset)
|
||||
* - CRUD Endpoints/Objects (Create, Update, Delete)
|
||||
*/
|
||||
|
||||
abstract class Request {
|
||||
|
||||
protected Context $context;
|
||||
@@ -62,7 +57,7 @@ abstract class Request {
|
||||
}
|
||||
}
|
||||
|
||||
protected function forbidMethod($method) {
|
||||
protected function forbidMethod($method): void {
|
||||
if (($key = array_search($method, $this->allowedMethods)) !== false) {
|
||||
unset($this->allowedMethods[$key]);
|
||||
}
|
||||
@@ -76,7 +71,7 @@ abstract class Request {
|
||||
return $this->isDisabled;
|
||||
}
|
||||
|
||||
protected function allowMethod($method) {
|
||||
protected function allowMethod($method): void {
|
||||
$availableMethods = ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "TRACE", "CONNECT"];
|
||||
if (in_array($method, $availableMethods) && !in_array($method, $this->allowedMethods)) {
|
||||
$this->allowedMethods[] = $method;
|
||||
@@ -113,7 +108,7 @@ abstract class Request {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function parseVariableParams($values) {
|
||||
public function parseVariableParams($values): void {
|
||||
foreach ($values as $name => $value) {
|
||||
if (isset($this->params[$name])) continue;
|
||||
$type = Parameter\Parameter::parseType($value);
|
||||
@@ -129,6 +124,7 @@ abstract class Request {
|
||||
}
|
||||
|
||||
protected abstract function _execute(): bool;
|
||||
public static function getDefaultACL(Insert $insert): void { }
|
||||
|
||||
public final function execute($values = array()): bool {
|
||||
|
||||
@@ -203,9 +199,11 @@ abstract class Request {
|
||||
} else if ($session) {
|
||||
$tfaToken = $session->getUser()->getTwoFactorToken();
|
||||
if ($tfaToken && $tfaToken->isConfirmed() && !$tfaToken->isAuthenticated()) {
|
||||
$this->lastError = '2FA-Authorization is required';
|
||||
http_response_code(401);
|
||||
return false;
|
||||
if (!($this instanceof \Core\API\Tfa\VerifyTotp) && !($this instanceof \Core\API\Tfa\VerifyKey)) {
|
||||
$this->lastError = '2FA-Authorization is required';
|
||||
http_response_code(401);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,7 +223,7 @@ abstract class Request {
|
||||
// Check for permission
|
||||
if (!($this instanceof \Core\API\Permission\Save)) {
|
||||
$req = new \Core\API\Permission\Check($this->context);
|
||||
$this->success = $req->execute(array("method" => $this->getMethod()));
|
||||
$this->success = $req->execute(array("method" => self::getEndpoint()));
|
||||
$this->lastError = $req->getLastError();
|
||||
if (!$this->success) {
|
||||
return false;
|
||||
@@ -266,11 +264,11 @@ abstract class Request {
|
||||
}
|
||||
|
||||
protected function getParam($name, $obj = NULL): mixed {
|
||||
// I don't know why phpstorm
|
||||
if ($obj === NULL) {
|
||||
$obj = $this->params;
|
||||
}
|
||||
|
||||
// I don't know why phpstorm
|
||||
return (isset($obj[$name]) ? $obj[$name]->value : NULL);
|
||||
}
|
||||
|
||||
@@ -302,10 +300,30 @@ abstract class Request {
|
||||
return $this->externalCall;
|
||||
}
|
||||
|
||||
private function getMethod() {
|
||||
$class = str_replace("\\", "/", get_class($this));
|
||||
$class = substr($class, strlen("api/"));
|
||||
return $class;
|
||||
public static function getEndpoint(string $prefix = ""): ?string {
|
||||
$reflectionClass = new \ReflectionClass(get_called_class());
|
||||
if ($reflectionClass->isAbstract()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$isNestedAPI = $reflectionClass->getParentClass()->getName() !== Request::class;
|
||||
if (!$isNestedAPI) {
|
||||
# e.g. /api/stats or /api/info
|
||||
$methodName = $reflectionClass->getShortName();
|
||||
return $prefix . lcfirst($methodName);
|
||||
} else {
|
||||
# e.g. /api/user/login
|
||||
$methodClass = $reflectionClass;
|
||||
$nestedClass = $reflectionClass->getParentClass();
|
||||
while (!endsWith($nestedClass->getName(), "API")) {
|
||||
$methodClass = $nestedClass;
|
||||
$nestedClass = $nestedClass->getParentClass();
|
||||
}
|
||||
|
||||
$nestedAPI = substr(lcfirst($nestedClass->getShortName()), 0, -3);
|
||||
$methodName = lcfirst($methodClass->getShortName());
|
||||
return $prefix . $nestedAPI . "/" . $methodName;
|
||||
}
|
||||
}
|
||||
|
||||
public function getJsonResult(): string {
|
||||
@@ -314,7 +332,7 @@ abstract class Request {
|
||||
return json_encode($this->result);
|
||||
}
|
||||
|
||||
protected function disableOutputBuffer() {
|
||||
protected function disableOutputBuffer(): void {
|
||||
ob_implicit_flush(true);
|
||||
$levels = ob_get_level();
|
||||
for ( $i = 0; $i < $levels; $i ++ ) {
|
||||
@@ -323,7 +341,7 @@ abstract class Request {
|
||||
flush();
|
||||
}
|
||||
|
||||
protected function disableCache() {
|
||||
protected function disableCache(): void {
|
||||
header("Last-Modified: " . (new \DateTime())->format("D, d M Y H:i:s T"));
|
||||
header("Expires: Sat, 26 Jul 1997 05:00:00 GMT");
|
||||
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
|
||||
@@ -331,7 +349,7 @@ abstract class Request {
|
||||
header("Pragma: no-cache");
|
||||
}
|
||||
|
||||
protected function setupSSE() {
|
||||
protected function setupSSE(): void {
|
||||
$this->context->sendCookies();
|
||||
$this->context->getSQL()?->close();
|
||||
set_time_limit(0);
|
||||
@@ -348,7 +366,7 @@ abstract class Request {
|
||||
* @throws \PhpMqtt\Client\Exceptions\DataTransferException
|
||||
* @throws \PhpMqtt\Client\Exceptions\MqttClientException
|
||||
*/
|
||||
protected function startMqttSSE(MqttClient $mqtt, callable $onPing) {
|
||||
protected function startMqttSSE(MqttClient $mqtt, callable $onPing): void {
|
||||
$lastPing = 0;
|
||||
$mqtt->registerLoopEventHandler(function(MqttClient $mqtt, $elapsed) use (&$lastPing, $onPing) {
|
||||
if ($elapsed - $lastPing >= 5) {
|
||||
@@ -366,7 +384,7 @@ abstract class Request {
|
||||
$mqtt->disconnect();
|
||||
}
|
||||
|
||||
protected function processImageUpload(string $uploadDir, array $allowedExtensions = ["jpg","jpeg","png","gif"], $transformCallback = null) {
|
||||
protected function processImageUpload(string $uploadDir, array $allowedExtensions = ["jpg","jpeg","png","gif"], $transformCallback = null): bool|array {
|
||||
if (empty($_FILES)) {
|
||||
return $this->createError("You need to upload an image.");
|
||||
} else if (count($_FILES) > 1) {
|
||||
@@ -470,4 +488,49 @@ abstract class Request {
|
||||
return [$fileName, $files[$fileName]];
|
||||
}
|
||||
}
|
||||
|
||||
public static function getApiEndpoints(): array {
|
||||
|
||||
// first load all direct classes
|
||||
$classes = [];
|
||||
$apiDirs = ["Core", "Site"];
|
||||
foreach ($apiDirs as $apiDir) {
|
||||
$basePath = realpath(WEBROOT . "/$apiDir/API/");
|
||||
if (!$basePath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (scandir($basePath) as $fileName) {
|
||||
$fullPath = $basePath . "/" . $fileName;
|
||||
if (is_file($fullPath) && endsWith($fileName, ".class.php")) {
|
||||
require_once $fullPath;
|
||||
$apiName = explode(".", $fileName)[0];
|
||||
$className = "\\$apiDir\\API\\$apiName";
|
||||
if (!class_exists($className)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$reflectionClass = new \ReflectionClass($className);
|
||||
if (!$reflectionClass->isSubclassOf(Request::class) || $reflectionClass->isAbstract()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$endpoint = "$className::getEndpoint"();
|
||||
$classes[$endpoint] = $reflectionClass;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// then load all inheriting classes
|
||||
foreach (get_declared_classes() as $declaredClass) {
|
||||
$reflectionClass = new \ReflectionClass($declaredClass);
|
||||
if (!$reflectionClass->isAbstract() && $reflectionClass->isSubclassOf(Request::class)) {
|
||||
$className = $reflectionClass->getName();
|
||||
$endpoint = "$className::getEndpoint"();
|
||||
$classes[$endpoint] = $reflectionClass;
|
||||
}
|
||||
}
|
||||
|
||||
return $classes;
|
||||
}
|
||||
}
|
||||
@@ -69,8 +69,10 @@ namespace Core\API\Routes {
|
||||
use Core\API\RoutesAPI;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Condition\CondBool;
|
||||
use Core\Driver\SQL\Query\Insert;
|
||||
use Core\Driver\SQL\Query\StartTransaction;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\DatabaseEntity\Group;
|
||||
use Core\Objects\DatabaseEntity\Route;
|
||||
use Core\Objects\Router\DocumentRoute;
|
||||
use Core\Objects\Router\RedirectPermanentlyRoute;
|
||||
@@ -101,6 +103,10 @@ namespace Core\API\Routes {
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to fetch site routing");
|
||||
}
|
||||
}
|
||||
|
||||
class Save extends RoutesAPI {
|
||||
@@ -202,6 +208,10 @@ namespace Core\API\Routes {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to save the site routing");
|
||||
}
|
||||
}
|
||||
|
||||
class Add extends RoutesAPI {
|
||||
@@ -236,6 +246,10 @@ namespace Core\API\Routes {
|
||||
$this->lastError = $sql->getLastError();
|
||||
return $this->success && $this->regenerateCache();
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to add new routes");
|
||||
}
|
||||
}
|
||||
|
||||
class Update extends RoutesAPI {
|
||||
@@ -270,9 +284,13 @@ namespace Core\API\Routes {
|
||||
$exact = $this->getParam("exact");
|
||||
$active = $this->getParam("active");
|
||||
if ($route->getType() !== $type) {
|
||||
$route = $this->createRoute($type, $pattern, $target, $extra, $exact, $active);
|
||||
if ($route === null) {
|
||||
if (!$route->delete($sql)) {
|
||||
return false;
|
||||
} else {
|
||||
$route = $this->createRoute($type, $pattern, $target, $extra, $exact, $active);
|
||||
if ($route === null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$route->setPattern($pattern);
|
||||
@@ -286,6 +304,10 @@ namespace Core\API\Routes {
|
||||
$this->lastError = $sql->getLastError();
|
||||
return $this->success && $this->regenerateCache();
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to update existing routes");
|
||||
}
|
||||
}
|
||||
|
||||
class Remove extends RoutesAPI {
|
||||
@@ -311,6 +333,10 @@ namespace Core\API\Routes {
|
||||
$this->lastError = $sql->getLastError();
|
||||
return $this->success && $this->regenerateCache();
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to remove routes");
|
||||
}
|
||||
}
|
||||
|
||||
class Enable extends RoutesAPI {
|
||||
@@ -325,6 +351,10 @@ namespace Core\API\Routes {
|
||||
$id = $this->getParam("id");
|
||||
return $this->toggleRoute($id, true);
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to enable a route");
|
||||
}
|
||||
}
|
||||
|
||||
class Disable extends RoutesAPI {
|
||||
@@ -339,6 +369,10 @@ namespace Core\API\Routes {
|
||||
$id = $this->getParam("id");
|
||||
return $this->toggleRoute($id, false);
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to disable a route");
|
||||
}
|
||||
}
|
||||
|
||||
class GenerateCache extends RoutesAPI {
|
||||
@@ -380,6 +414,10 @@ namespace Core\API\Routes {
|
||||
public function getRouter(): ?Router {
|
||||
return $this->router;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN, Group::SUPPORT], "Allows users to regenerate the routing cache");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,10 @@ namespace Core\API\Settings {
|
||||
use Core\Driver\SQL\Condition\CondIn;
|
||||
use Core\Driver\SQL\Condition\CondNot;
|
||||
use Core\Driver\SQL\Condition\CondRegex;
|
||||
use Core\Driver\SQL\Query\Insert;
|
||||
use Core\Driver\SQL\Strategy\UpdateStrategy;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\DatabaseEntity\Group;
|
||||
|
||||
class Get extends SettingsAPI {
|
||||
|
||||
@@ -46,6 +48,10 @@ namespace Core\API\Settings {
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to fetch site settings");
|
||||
}
|
||||
}
|
||||
|
||||
class Set extends SettingsAPI {
|
||||
@@ -144,6 +150,10 @@ namespace Core\API\Settings {
|
||||
$this->lastError = $sql->getLastError();
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to modify site settings");
|
||||
}
|
||||
}
|
||||
|
||||
class GenerateJWT extends SettingsAPI {
|
||||
@@ -173,5 +183,9 @@ namespace Core\API\Settings {
|
||||
$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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ namespace Core\API;
|
||||
|
||||
use Core\Driver\SQL\Expression\Count;
|
||||
use Core\Driver\SQL\Expression\Distinct;
|
||||
use Core\Driver\SQL\Query\Insert;
|
||||
use Core\Objects\DatabaseEntity\Group;
|
||||
use DateTime;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Condition\CondBool;
|
||||
@@ -113,4 +115,7 @@ class Stats extends Request {
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN, Group::SUPPORT], "Allows users to view site statistics");
|
||||
}
|
||||
}
|
||||
@@ -19,60 +19,11 @@ class Swagger extends Request {
|
||||
die($this->getDocumentation());
|
||||
}
|
||||
|
||||
private function getApiEndpoints(): array {
|
||||
|
||||
// first load all direct classes
|
||||
$classes = [];
|
||||
$apiDirs = ["Core", "Site"];
|
||||
foreach ($apiDirs as $apiDir) {
|
||||
$basePath = realpath(WEBROOT . "/$apiDir/API/");
|
||||
if (!$basePath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (scandir($basePath) as $fileName) {
|
||||
$fullPath = $basePath . "/" . $fileName;
|
||||
if (is_file($fullPath) && endsWith($fileName, ".class.php")) {
|
||||
require_once $fullPath;
|
||||
$apiName = explode(".", $fileName)[0];
|
||||
$className = "\\$apiDir\\API\\$apiName";
|
||||
if (!class_exists($className)) {
|
||||
var_dump("Class not exist: $className");
|
||||
continue;
|
||||
}
|
||||
|
||||
$reflection = new \ReflectionClass($className);
|
||||
if (!$reflection->isSubclassOf(Request::class) || $reflection->isAbstract()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$endpoint = "/" . strtolower($apiName);
|
||||
$classes[$endpoint] = $reflection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// then load all inheriting classes
|
||||
foreach (get_declared_classes() as $declaredClass) {
|
||||
$reflectionClass = new \ReflectionClass($declaredClass);
|
||||
if (!$reflectionClass->isAbstract() && $reflectionClass->isSubclassOf(Request::class)) {
|
||||
$inheritingClass = $reflectionClass->getParentClass();
|
||||
if ($inheritingClass->isAbstract() && endsWith($inheritingClass->getShortName(), "API")) {
|
||||
$endpoint = strtolower(substr($inheritingClass->getShortName(), 0, -3));
|
||||
$endpoint = "/$endpoint/" . lcfirst($reflectionClass->getShortName());
|
||||
$classes[$endpoint] = $reflectionClass;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $classes;
|
||||
}
|
||||
|
||||
private function fetchPermissions(): array {
|
||||
$req = new Permission\Fetch($this->context);
|
||||
$req = new \Core\API\Permission\Fetch($this->context);
|
||||
$this->success = $req->execute();
|
||||
$permissions = [];
|
||||
foreach( $req->getResult()["permissions"] as $permission) {
|
||||
foreach ($req->getResult()["permissions"] as $permission) {
|
||||
$permissions["/" . strtolower($permission["method"])] = $permission["groups"];
|
||||
}
|
||||
|
||||
@@ -85,12 +36,13 @@ class Swagger extends Request {
|
||||
}
|
||||
|
||||
$currentUser = $this->context->getUser();
|
||||
if (($request->loginRequired() || !empty($requiredGroups)) && !$currentUser) {
|
||||
$isLoggedIn = $currentUser !== null;
|
||||
if (($request->loginRequired() || !empty($requiredGroups)) && !$isLoggedIn) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// special case: hardcoded permission
|
||||
if ($request instanceof Permission\Save && (!$currentUser || !$currentUser->hasGroup(Group::ADMIN))) {
|
||||
if ($request instanceof \Core\API\Permission\Save && (!$isLoggedIn || !$currentUser->hasGroup(Group::ADMIN))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,17 +36,20 @@ namespace Core\API {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function verifyClientDataJSON($jsonData, KeyBasedTwoFactorToken $token): bool {
|
||||
protected function verifyClientDataJSON(array $jsonData, KeyBasedTwoFactorToken $token): bool {
|
||||
$settings = $this->context->getSettings();
|
||||
$expectedType = $token->isConfirmed() ? "webauthn.get" : "webauthn.create";
|
||||
$type = $jsonData["type"] ?? "null";
|
||||
if ($type !== $expectedType) {
|
||||
return $this->createError("Invalid client data json type. Expected: '$expectedType', Got: '$type'");
|
||||
} else if ($token->getData() !== base64url_decode($jsonData["challenge"] ?? "")) {
|
||||
} else if (base64url_decode($token->getChallenge()) !== base64url_decode($jsonData["challenge"] ?? "")) {
|
||||
return $this->createError("Challenge does not match");
|
||||
} else if (($jsonData["origin"] ?? null) !== $settings->getBaseURL()) {
|
||||
}
|
||||
|
||||
$origin = $jsonData["origin"] ?? null;
|
||||
if ($origin !== $settings->getBaseURL()) {
|
||||
$baseUrl = $settings->getBaseURL();
|
||||
return $this->createError("Origin does not match. Expected: '$baseUrl', Got: '${$jsonData["origin"]}'");
|
||||
// return $this->createError("Origin does not match. Expected: '$baseUrl', Got: '$origin'");
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -96,31 +99,34 @@ namespace Core\API\TFA {
|
||||
|
||||
if ($this->success && $token->isConfirmed()) {
|
||||
// send an email
|
||||
$settings = $this->context->getSettings();
|
||||
$req = new \Core\API\Template\Render($this->context);
|
||||
$this->success = $req->execute([
|
||||
"file" => "mail/2fa_remove.twig",
|
||||
"parameters" => [
|
||||
"username" => $currentUser->getFullName() ?? $currentUser->getUsername(),
|
||||
"site_name" => $settings->getSiteName(),
|
||||
"sender_mail" => $settings->getMailSender()
|
||||
]
|
||||
]);
|
||||
|
||||
if ($this->success) {
|
||||
$body = $req->getResult()["html"];
|
||||
$gpg = $currentUser->getGPG();
|
||||
$siteName = $settings->getSiteName();
|
||||
$req = new \Core\API\Mail\Send($this->context);
|
||||
$email = $currentUser->getEmail();
|
||||
if ($email) {
|
||||
$settings = $this->context->getSettings();
|
||||
$req = new \Core\API\Template\Render($this->context);
|
||||
$this->success = $req->execute([
|
||||
"to" => $currentUser->getEmail(),
|
||||
"subject" => "[$siteName] 2FA-Authentication removed",
|
||||
"body" => $body,
|
||||
"gpgFingerprint" => $gpg?->getFingerprint()
|
||||
"file" => "mail/2fa_remove.twig",
|
||||
"parameters" => [
|
||||
"username" => $currentUser->getFullName() ?? $currentUser->getUsername(),
|
||||
"site_name" => $settings->getSiteName(),
|
||||
"sender_mail" => $settings->getMailSender()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$this->lastError = $req->getLastError();
|
||||
if ($this->success) {
|
||||
$body = $req->getResult()["html"];
|
||||
$gpg = $currentUser->getGPG();
|
||||
$siteName = $settings->getSiteName();
|
||||
$req = new \Core\API\Mail\Send($this->context);
|
||||
$this->success = $req->execute([
|
||||
"to" => $email,
|
||||
"subject" => "[$siteName] 2FA-Authentication removed",
|
||||
"body" => $body,
|
||||
"gpgFingerprint" => $gpg?->getFingerprint()
|
||||
]);
|
||||
}
|
||||
|
||||
$this->lastError = $req->getLastError();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
@@ -211,6 +217,8 @@ namespace Core\API\TFA {
|
||||
return $this->createError("Invalid 2FA-token endpoint");
|
||||
}
|
||||
|
||||
$this->result["time"] = time();
|
||||
$this->result["time_zone"] = $this->context->getSettings()->getTimeZone();
|
||||
$code = $this->getParam("code");
|
||||
if (!$twoFactorToken->verify($code)) {
|
||||
return $this->createError("Code does not match");
|
||||
@@ -250,11 +258,11 @@ namespace Core\API\TFA {
|
||||
if (!($twoFactorToken instanceof KeyBasedTwoFactorToken) || $twoFactorToken->isConfirmed()) {
|
||||
return $this->createError("You already added a two factor token");
|
||||
} else {
|
||||
$challenge = base64_encode($twoFactorToken->getData());
|
||||
$challenge = $twoFactorToken->getChallenge();
|
||||
}
|
||||
} else {
|
||||
$challenge = base64_encode(generateRandomString(32, "raw"));
|
||||
$twoFactorToken = new KeyBasedTwoFactorToken($challenge);
|
||||
$twoFactorToken = KeyBasedTwoFactorToken::create();
|
||||
$challenge = $twoFactorToken->getChallenge();
|
||||
$this->success = ($twoFactorToken->save($sql) !== false);
|
||||
$this->lastError = $sql->getLastError();
|
||||
if (!$this->success) {
|
||||
@@ -262,7 +270,7 @@ namespace Core\API\TFA {
|
||||
}
|
||||
|
||||
$currentUser->setTwoFactorToken($twoFactorToken);
|
||||
$this->success = $currentUser->save($sql) !== false;
|
||||
$this->success = $currentUser->save($sql, ["twoFactorToken"]) !== false;
|
||||
$this->lastError = $sql->getLastError();
|
||||
if (!$this->success) {
|
||||
return false;
|
||||
|
||||
@@ -8,15 +8,16 @@ use Core\Driver\SQL\Condition\Condition;
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Core\Objects\DatabaseEntity\Controller\DatabaseEntityHandler;
|
||||
use Core\Objects\DatabaseEntity\Controller\DatabaseEntityQuery;
|
||||
use Core\Objects\DatabaseEntity\User;
|
||||
|
||||
trait Pagination {
|
||||
|
||||
function getPaginationParameters(array $orderColumns, string $defaultOrderBy = "id", string $defaultSortOrder = "asc"): array {
|
||||
function getPaginationParameters(array $orderColumns, string $defaultOrderBy = null, string $defaultSortOrder = "asc"): array {
|
||||
$this->paginationOrderColumns = $orderColumns;
|
||||
$defaultOrderBy = $defaultOrderBy ?? current($orderColumns);
|
||||
|
||||
return [
|
||||
'page' => new Parameter('page', Parameter::TYPE_INT, true, 1),
|
||||
'count' => new Parameter('count', Parameter::TYPE_INT, true, 20),
|
||||
'count' => new Parameter('count', Parameter::TYPE_INT, true, 25),
|
||||
'orderBy' => new StringType('orderBy', -1, true, $defaultOrderBy, $orderColumns),
|
||||
'sortOrder' => new StringType('sortOrder', -1, true, $defaultSortOrder, ['asc', 'desc']),
|
||||
];
|
||||
|
||||
@@ -138,6 +138,7 @@ namespace Core\API\User {
|
||||
use Core\Driver\SQL\Condition\CondBool;
|
||||
use Core\Driver\SQL\Condition\CondOr;
|
||||
use Core\Driver\SQL\Expression\Alias;
|
||||
use Core\Driver\SQL\Query\Insert;
|
||||
use Core\Objects\DatabaseEntity\Group;
|
||||
use Core\Objects\DatabaseEntity\UserToken;
|
||||
use Core\Driver\SQL\Column\Column;
|
||||
@@ -182,6 +183,9 @@ namespace Core\API\User {
|
||||
|
||||
$groups = [];
|
||||
$sql = $this->context->getSQL();
|
||||
|
||||
// TODO: Currently low-privileged users can request any groups here, so a simple privilege escalation is possible. \
|
||||
// what do? limit access to user/create to admins only?
|
||||
$requestedGroups = array_unique($this->getParam("groups"));
|
||||
if (!empty($requestedGroups)) {
|
||||
$groups = Group::findAll($sql, new CondIn(new Column("id"), $requestedGroups));
|
||||
@@ -206,6 +210,10 @@ namespace Core\API\User {
|
||||
public function getUser(): User {
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to create new users");
|
||||
}
|
||||
}
|
||||
|
||||
class Fetch extends UserAPI {
|
||||
@@ -263,6 +271,10 @@ namespace Core\API\User {
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN, Group::SUPPORT], "Allows users to fetch all users");
|
||||
}
|
||||
}
|
||||
|
||||
class Get extends UserAPI {
|
||||
@@ -302,6 +314,10 @@ namespace Core\API\User {
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN, Group::SUPPORT], "Allows users to get details about a user");
|
||||
}
|
||||
}
|
||||
|
||||
class Info extends UserAPI {
|
||||
@@ -314,31 +330,35 @@ namespace Core\API\User {
|
||||
public function _execute(): bool {
|
||||
|
||||
$currentUser = $this->context->getUser();
|
||||
$language = $this->context->getLanguage();
|
||||
$this->result["language"] = $language->jsonSerialize();
|
||||
|
||||
if (!$currentUser) {
|
||||
$this->result["loggedIn"] = false;
|
||||
$userGroups = [];
|
||||
} else {
|
||||
$this->result["loggedIn"] = true;
|
||||
$userGroups = array_keys($currentUser->getGroups());
|
||||
$sql = $this->context->getSQL();
|
||||
$res = $sql->select("method", "groups")
|
||||
->from("ApiPermission")
|
||||
->execute();
|
||||
|
||||
$permissions = [];
|
||||
if (is_array($res)) {
|
||||
foreach ($res as $row) {
|
||||
$requiredGroups = json_decode($row["groups"], true);
|
||||
if (empty($requiredGroups) || !empty(array_intersect($requiredGroups, $userGroups))) {
|
||||
$permissions[] = $row["method"];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->result["permissions"] = $permissions;
|
||||
$this->result["user"] = $currentUser->jsonSerialize();
|
||||
$this->result["session"] = $this->context->getSession()->jsonSerialize();
|
||||
}
|
||||
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
$res = $sql->select("method", "groups")
|
||||
->from("ApiPermission")
|
||||
->execute();
|
||||
|
||||
$this->result["permissions"] = [];
|
||||
if (is_array($res)) {
|
||||
foreach ($res as $row) {
|
||||
$requiredGroups = json_decode($row["groups"], true);
|
||||
if (empty($requiredGroups) || !empty(array_intersect($requiredGroups, $userGroups))) {
|
||||
$this->result["permissions"][] = $row["method"];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
@@ -415,6 +435,10 @@ namespace Core\API\User {
|
||||
$this->logger->info("Created new user with id=" . $user->getId());
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN, Group::SUPPORT], "Allows users to invite new users");
|
||||
}
|
||||
}
|
||||
|
||||
class AcceptInvite extends UserAPI {
|
||||
@@ -526,6 +550,14 @@ namespace Core\API\User {
|
||||
if ($this->context->getUser()) {
|
||||
$this->lastError = L('You are already logged in');
|
||||
$this->success = true;
|
||||
|
||||
$tfaToken = $this->context->getUser()->getTwoFactorToken();
|
||||
if ($tfaToken && $tfaToken->isConfirmed() && !$tfaToken->isAuthenticated()) {
|
||||
$this->result["twoFactorToken"] = $tfaToken->jsonSerialize([
|
||||
"type", "challenge", "authenticated", "confirmed", "credentialID"
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -552,16 +584,19 @@ namespace Core\API\User {
|
||||
return $this->createError("Error creating Session: " . $sql->getLastError());
|
||||
} else {
|
||||
$tfaToken = $user->getTwoFactorToken();
|
||||
|
||||
$this->result["loggedIn"] = true;
|
||||
$this->result["user"] = $user->jsonSerialize();
|
||||
$this->result["session"] = $session->jsonSerialize();
|
||||
$this->result["logoutIn"] = $session->getExpiresSeconds();
|
||||
$this->result["csrfToken"] = $session->getCsrfToken();
|
||||
if ($tfaToken && $tfaToken->isConfirmed()) {
|
||||
$this->result["2fa"] = ["type" => $tfaToken->getType()];
|
||||
if ($tfaToken instanceof KeyBasedTwoFactorToken) {
|
||||
$challenge = base64_encode(generateRandomString(32, "raw"));
|
||||
$this->result["2fa"]["challenge"] = $challenge;
|
||||
$_SESSION["challenge"] = $challenge;
|
||||
$tfaToken->generateChallenge();
|
||||
}
|
||||
|
||||
$this->result["twoFactorToken"] = $tfaToken->jsonSerialize([
|
||||
"type", "challenge", "authenticated", "confirmed", "credentialID"
|
||||
]);
|
||||
}
|
||||
$this->success = true;
|
||||
}
|
||||
@@ -823,6 +858,10 @@ namespace Core\API\User {
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to modify other user's details");
|
||||
}
|
||||
}
|
||||
|
||||
class Delete extends UserAPI {
|
||||
@@ -856,6 +895,10 @@ namespace Core\API\User {
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN], "Allows users to delete other users");
|
||||
}
|
||||
}
|
||||
|
||||
class RequestPasswordReset extends UserAPI {
|
||||
|
||||
@@ -17,6 +17,8 @@ namespace Core\API\Visitors {
|
||||
use Core\API\Parameter\StringType;
|
||||
use Core\API\VisitorsAPI;
|
||||
use Core\Driver\SQL\Expression\Count;
|
||||
use Core\Driver\SQL\Query\Insert;
|
||||
use Core\Objects\DatabaseEntity\Group;
|
||||
use DateTime;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Expression\Add;
|
||||
@@ -112,5 +114,9 @@ namespace Core\API\Visitors {
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public static function getDefaultACL(Insert $insert): void {
|
||||
$insert->addRow(self::getEndpoint(), [Group::ADMIN, Group::SUPPORT], "Allows users to view visitor statistics");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,10 @@
|
||||
|
||||
namespace Core\Configuration;
|
||||
|
||||
use Core\API\Request;
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Core\Objects\DatabaseEntity\Controller\DatabaseEntity;
|
||||
use Core\Objects\DatabaseEntity\Controller\DatabaseEntityHandler;
|
||||
use Core\Objects\DatabaseEntity\Group;
|
||||
use Core\Objects\DatabaseEntity\Language;
|
||||
use Core\Objects\DatabaseEntity\Route;
|
||||
use Core\Objects\Router\DocumentRoute;
|
||||
use Core\Objects\Router\StaticFileRoute;
|
||||
@@ -53,35 +52,7 @@ class CreateDatabase extends DatabaseScript {
|
||||
->addString("description", 128, false, "")
|
||||
->primaryKey("method");
|
||||
|
||||
$queries[] = $sql->insert("ApiPermission", array("method", "groups", "description"))
|
||||
->addRow("ApiKey/create", array(), "Allows users to create API-Keys for themselves")
|
||||
->addRow("ApiKey/fetch", array(), "Allows users to list their API-Keys")
|
||||
->addRow("ApiKey/refresh", array(), "Allows users to refresh their API-Keys")
|
||||
->addRow("ApiKey/revoke", array(), "Allows users to revoke their API-Keys")
|
||||
->addRow("Groups/fetch", array(Group::SUPPORT, Group::ADMIN), "Allows users to list all available groups")
|
||||
->addRow("Groups/create", array(Group::ADMIN), "Allows users to create a new groups")
|
||||
->addRow("Groups/delete", array(Group::ADMIN), "Allows users to delete a group")
|
||||
->addRow("Routes/fetch", array(Group::ADMIN), "Allows users to list all configured routes")
|
||||
->addRow("Routes/save", array(Group::ADMIN), "Allows users to create, delete and modify routes")
|
||||
->addRow("Mail/test", array(Group::SUPPORT, Group::ADMIN), "Allows users to send a test email to a given address")
|
||||
->addRow("Mail/Sync", array(Group::SUPPORT, Group::ADMIN), "Allows users to synchronize mails with the database")
|
||||
->addRow("Settings/get", array(Group::ADMIN), "Allows users to fetch server settings")
|
||||
->addRow("Settings/set", array(Group::ADMIN), "Allows users create, delete or modify server settings")
|
||||
->addRow("Settings/generateJWT", array(Group::ADMIN), "Allows users generate a new jwt key")
|
||||
->addRow("Stats", array(Group::ADMIN, Group::SUPPORT), "Allows users to fetch server stats")
|
||||
->addRow("User/create", array(Group::ADMIN), "Allows users to create a new user, email address does not need to be confirmed")
|
||||
->addRow("User/fetch", array(Group::ADMIN, Group::SUPPORT), "Allows users to list all registered users")
|
||||
->addRow("User/get", array(Group::ADMIN, Group::SUPPORT), "Allows users to get information about a single user")
|
||||
->addRow("User/invite", array(Group::ADMIN), "Allows users to create a new user and send them an invitation link")
|
||||
->addRow("User/edit", array(Group::ADMIN), "Allows users to edit details and group memberships of any user")
|
||||
->addRow("User/delete", array(Group::ADMIN), "Allows users to delete any other user")
|
||||
->addRow("Permission/fetch", array(Group::ADMIN), "Allows users to list all API permissions")
|
||||
->addRow("Visitors/stats", array(Group::ADMIN, Group::SUPPORT), "Allows users to see visitor statistics")
|
||||
->addRow("Contact/respond", array(Group::ADMIN, Group::SUPPORT), "Allows users to respond to contact requests")
|
||||
->addRow("Contact/fetch", array(Group::ADMIN, Group::SUPPORT), "Allows users to fetch all contact requests")
|
||||
->addRow("Contact/get", array(Group::ADMIN, Group::SUPPORT), "Allows users to see messages within a contact request")
|
||||
->addRow("Logs/get", [Group::ADMIN], "Allows users to fetch system logs");
|
||||
|
||||
self::loadDefaultACL($queries, $sql);
|
||||
self::loadPatches($queries, $sql);
|
||||
|
||||
return $queries;
|
||||
@@ -108,7 +79,7 @@ class CreateDatabase extends DatabaseScript {
|
||||
}
|
||||
}
|
||||
|
||||
public static function loadEntities(&$queries, $sql) {
|
||||
private static function loadEntities(&$queries, $sql) {
|
||||
$persistables = [];
|
||||
$baseDirs = ["Core", "Site"];
|
||||
foreach ($baseDirs as $baseDir) {
|
||||
@@ -161,4 +132,17 @@ class CreateDatabase extends DatabaseScript {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function loadDefaultACL(array &$queries, SQL $sql) {
|
||||
$query = $sql->insert("ApiPermission", ["method", "groups", "description"]);
|
||||
|
||||
foreach (Request::getApiEndpoints() as $reflectionClass) {
|
||||
$method = $reflectionClass->getName() . "::getDefaultACL";
|
||||
$method($query);
|
||||
}
|
||||
|
||||
if ($query->hasRows()) {
|
||||
$queries[] = $query;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use Core\Driver\SQL\SQL;
|
||||
class Logger {
|
||||
|
||||
public const LOG_FILE_DATE_FORMAT = "Y-m-d_H-i-s_v";
|
||||
public const LOG_LEVEL_NONE = -1;
|
||||
public const LOG_LEVEL_DEBUG = 0;
|
||||
public const LOG_LEVEL_INFO = 1;
|
||||
public const LOG_LEVEL_WARNING = 2;
|
||||
|
||||
@@ -7,8 +7,8 @@ use Core\Driver\SQL\SQL;
|
||||
|
||||
class CondIn extends Condition {
|
||||
|
||||
private $needle;
|
||||
private $haystack;
|
||||
private mixed $needle;
|
||||
private mixed $haystack;
|
||||
|
||||
public function __construct($needle, $haystack) {
|
||||
$this->needle = $needle;
|
||||
@@ -20,22 +20,28 @@ class CondIn extends Condition {
|
||||
|
||||
function getExpression(SQL $sql, array &$params): string {
|
||||
|
||||
$haystack = $this->getHaystack();
|
||||
if (is_array($haystack)) {
|
||||
$values = array();
|
||||
foreach ($haystack as $value) {
|
||||
$values[] = $sql->addValue($value, $params);
|
||||
}
|
||||
$needle = $sql->addValue($this->needle, $params);
|
||||
|
||||
$values = implode(",", $values);
|
||||
$values = "($values)";
|
||||
} else if($haystack instanceof Select) {
|
||||
$values = $haystack->getExpression($sql, $params);
|
||||
if (is_array($this->haystack)) {
|
||||
if (!empty($this->haystack)) {
|
||||
$values = array();
|
||||
foreach ($this->haystack as $value) {
|
||||
$values[] = $sql->addValue($value, $params);
|
||||
}
|
||||
|
||||
$values = implode(",", $values);
|
||||
$values = "($values)";
|
||||
} else {
|
||||
$sql->getLogger()->error("Empty haystack for in-expression with needle: " . $needle);
|
||||
return false;
|
||||
}
|
||||
} else if ($this->haystack instanceof Select) {
|
||||
$values = $this->haystack->getExpression($sql, $params);
|
||||
} else {
|
||||
$sql->getLogger()->error("Unsupported in-expression value: " . get_class($haystack));
|
||||
$sql->getLogger()->error("Unsupported in-expression value: " . get_class($this->haystack));
|
||||
return false;
|
||||
}
|
||||
|
||||
return $sql->addValue($this->needle, $params) . " IN $values";
|
||||
return "$needle IN $values";
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace Core\Driver\SQL;
|
||||
|
||||
use Core\API\Parameter\Parameter;
|
||||
|
||||
use Core\Driver\Logger\Logger;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Condition\CondLike;
|
||||
use Core\Driver\SQL\Expression\Count;
|
||||
@@ -138,7 +139,7 @@ class MySQL extends SQL {
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
protected function execute($query, $values = NULL, int $fetchType = self::FETCH_NONE) {
|
||||
protected function execute($query, $values = NULL, int $fetchType = self::FETCH_NONE, int $logLevel = Logger::LOG_LEVEL_ERROR) {
|
||||
|
||||
$result = null;
|
||||
$this->lastError = "";
|
||||
@@ -146,6 +147,10 @@ class MySQL extends SQL {
|
||||
$res = null;
|
||||
$success = false;
|
||||
|
||||
if ($logLevel === Logger::LOG_LEVEL_DEBUG) {
|
||||
$this->logger->debug("query: " . $query . ", args: " . json_encode($values), false);
|
||||
}
|
||||
|
||||
try {
|
||||
if (empty($values)) {
|
||||
$res = mysqli_query($this->connection, $query);
|
||||
@@ -167,7 +172,6 @@ class MySQL extends SQL {
|
||||
}
|
||||
}
|
||||
} else if ($stmt = $this->connection->prepare($query)) {
|
||||
|
||||
$sqlParams = $this->getPreparedParams($values);
|
||||
if ($stmt->bind_param(...$sqlParams)) {
|
||||
if ($stmt->execute()) {
|
||||
@@ -201,10 +205,12 @@ class MySQL extends SQL {
|
||||
}
|
||||
}
|
||||
} catch (\mysqli_sql_exception $exception) {
|
||||
$this->lastError = $this->logger->error("MySQL::execute failed: " .
|
||||
($stmt !== null
|
||||
? "$stmt->error ($stmt->errno)"
|
||||
: $exception->getMessage()));
|
||||
if ($logLevel >= Logger::LOG_LEVEL_ERROR) {
|
||||
$this->lastError = $this->logger->error("MySQL::execute failed: " .
|
||||
($stmt !== null
|
||||
? "$stmt->error ($stmt->errno)"
|
||||
: $exception->getMessage()));
|
||||
}
|
||||
} finally {
|
||||
|
||||
if ($res !== null && !is_bool($res) && $fetchType !== self::FETCH_ITERATIVE) {
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Core\Driver\SQL;
|
||||
|
||||
use Core\API\Parameter\Parameter;
|
||||
|
||||
use Core\Driver\Logger\Logger;
|
||||
use Core\Driver\SQL\Column\Column;
|
||||
use Core\Driver\SQL\Column\IntColumn;
|
||||
use Core\Driver\SQL\Column\NumericColumn;
|
||||
@@ -96,12 +97,16 @@ class PostgreSQL extends SQL {
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
protected function execute($query, $values = NULL, int $fetchType = self::FETCH_NONE) {
|
||||
protected function execute($query, $values = NULL, int $fetchType = self::FETCH_NONE, int $logLevel = Logger::LOG_LEVEL_ERROR) {
|
||||
|
||||
$this->lastError = "";
|
||||
$stmt_name = uniqid();
|
||||
$pgParams = array();
|
||||
|
||||
if ($logLevel === Logger::LOG_LEVEL_DEBUG) {
|
||||
$this->logger->debug("query: " . $query . ", args: " . json_encode($values), false);
|
||||
}
|
||||
|
||||
if (!is_null($values)) {
|
||||
foreach ($values as $value) {
|
||||
$paramType = Parameter::parseType($value);
|
||||
|
||||
@@ -40,6 +40,7 @@ class Insert extends Query {
|
||||
public function getTableName(): string { return $this->tableName; }
|
||||
public function getColumns(): array { return $this->columns; }
|
||||
public function getRows(): array { return $this->rows; }
|
||||
public function hasRows(): bool { return !empty($this->rows); }
|
||||
public function onDuplicateKey(): ?Strategy { return $this->onDuplicateKey; }
|
||||
public function getReturning(): ?string { return $this->returning; }
|
||||
|
||||
|
||||
@@ -149,7 +149,12 @@ abstract class SQL {
|
||||
return false;
|
||||
}
|
||||
|
||||
$res = $this->execute($queryStr, $parameters, $fetchType);
|
||||
$logLevel = Logger::LOG_LEVEL_DEBUG;
|
||||
if ($query instanceof Insert && $query->getTableName() === "SystemLog") {
|
||||
$logLevel = Logger::LOG_LEVEL_NONE;
|
||||
}
|
||||
|
||||
$res = $this->execute($queryStr, $parameters, $fetchType, $logLevel);
|
||||
$success = ($res !== FALSE);
|
||||
|
||||
// fetch generated serial ids for Insert statements
|
||||
@@ -273,7 +278,7 @@ abstract class SQL {
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
protected abstract function execute($query, $values = NULL, int $fetchType = self::FETCH_NONE);
|
||||
protected abstract function execute($query, $values = NULL, int $fetchType = self::FETCH_NONE, int $logLevel = Logger::LOG_LEVEL_ERROR);
|
||||
|
||||
public function buildCondition(Condition|array $condition, &$params): string {
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ return [
|
||||
"resend_confirm_email_title" => "Bestätigungsmail erneut senden",
|
||||
"resend_confirm_email_description" => "Eine neue Bestätigungsmail anfordern um die Accounterstellung abzuschließen",
|
||||
"resend_confirm_email_form_title" => "Geben Sie Ihre E-Mailadresse ein, um eine neue Bestätigungsmail zu erhalten",
|
||||
"reset_password_title" => "Password zurücksetzen",
|
||||
"reset_password_title" => "Passwort zurücksetzen",
|
||||
"reset_password_description" => "Fordern Sie ein Passwort-Reset an, sobald Sie eine E-Mail erhalten, können Sie ein neues Passwort wählen",
|
||||
"reset_password_form_title" => "Ein neues Passwort wählen",
|
||||
"reset_password_request_form_title" => "Geben Sie Ihre E-Mail Adresse ein um ein Passwort-Reset Token zu erhalten",
|
||||
@@ -23,10 +23,14 @@ return [
|
||||
"email" => "E-Mail Adresse",
|
||||
"password" => "Passwort",
|
||||
"password_confirm" => "Passwort bestätigen",
|
||||
"password_old" => "Altes Passwort",
|
||||
"password_new" => "Neues Passwort",
|
||||
"full_name" => "Voller Name",
|
||||
"remember_me" => "Eingeloggt bleiben",
|
||||
"signing_in" => "Einloggen",
|
||||
"sign_in" => "Einloggen",
|
||||
"forgot_password" => "Passwort vergessen?",
|
||||
"change_password" => "Passwort ändern",
|
||||
"passwords_do_not_match" => "Die Passwörter stimmen nicht überein",
|
||||
"back_to_login" => "Zurück zum Login",
|
||||
"register_text" => "Noch keinen Account? Jetzt registrieren",
|
||||
@@ -38,4 +42,6 @@ return [
|
||||
"invalid_link" => "Den Link den Sie besucht haben ist nicht länger gültig",
|
||||
"confirm_success" => "Ihre E-Mail Adresse wurde erfolgreich bestätigt, Sie können sich jetzt einloggen",
|
||||
"confirm_error" => "Fehler beim Bestätigen der E-Mail Adresse",
|
||||
"gpg_key" => "GPG-Schlüssel",
|
||||
"2fa_token" => "Zwei-Faktor Authentifizierung (2FA)",
|
||||
];
|
||||
@@ -22,22 +22,28 @@ return [
|
||||
"name" => "Name",
|
||||
"type" => "Typ",
|
||||
"size" => "Größe",
|
||||
"last_modified" => "Zuletzt geändert",
|
||||
|
||||
# dialog / actions
|
||||
"action" => "Aktion",
|
||||
"actions" => "Aktionen",
|
||||
"title" => "Titel",
|
||||
"message" => "Nachricht",
|
||||
"rename" => "Umbenennen",
|
||||
"remove" => "Entfernen",
|
||||
"change" => "Bearbeiten",
|
||||
"reset" => "Zurücksetzen",
|
||||
"move" => "Verschieben",
|
||||
"delete" => "Löschen",
|
||||
"info" => "Info",
|
||||
"reload" => "Aktualisieren",
|
||||
|
||||
# file
|
||||
"choose_file" => "Datei auswählen",
|
||||
"download" => "Herunterladen",
|
||||
"download_all" => "Alles Herunterladen",
|
||||
"upload" => "Hochladen",
|
||||
"uploading" => "Lade hoch",
|
||||
"overwrite" => "Überschreiben",
|
||||
"reload" => "Aktualisieren",
|
||||
|
||||
# data table
|
||||
"showing_x_of_y_entries" => "Zeige %d von %d Einträgen",
|
||||
@@ -47,6 +53,8 @@ return [
|
||||
"date" => "Datum",
|
||||
"start_date" => "Startdatum",
|
||||
"end_date" => "Enddatum",
|
||||
"last_modified" => "Zuletzt geändert",
|
||||
"timestamp" => "Zeitpunkt",
|
||||
"date_format" => "d.m.Y",
|
||||
"date_time_format" => "d.m.Y H:i",
|
||||
"date_time_format_precise" => "d.m.Y H:i:s",
|
||||
|
||||
24
Core/Localization/de_DE/settings.php
Normal file
24
Core/Localization/de_DE/settings.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"settings" => "Einstellungen",
|
||||
"information" => "Informationen",
|
||||
|
||||
# API Key
|
||||
"api_key" => "API Schlüssel",
|
||||
"valid_until" => "Gültig bis",
|
||||
"token" => "Token",
|
||||
"request_new_key" => "Neuen Schlüssel anfordern",
|
||||
"show_only_active_keys" => "Zeige nur aktive Schlüssel",
|
||||
|
||||
# GPG Key
|
||||
"gpg_key_placeholder_text" => "GPG-Key im ASCII format reinziehen oder einfügen...",
|
||||
|
||||
# 2fa
|
||||
"register_2fa_device" => "Ein 2FA-Gerät registrieren",
|
||||
"register_2fa_totp_text" => "Scan den QR-Code mit einem Gerät, das du als Zwei-Faktor-Authentifizierung (2FA) benutzen willst. " .
|
||||
"Unter Android kannst du den Google Authenticator benutzen.",
|
||||
"register_2fa_fido_text" => "Möglicherweise musst du mit dem Gerät interagieren, zum Beispiel durch Eingeben einer PIN oder durch Berühren des Geräts",
|
||||
"remove_2fa" => "2FA-Token entfernen",
|
||||
"remove_2fa_text" => "Gib dein aktuelles Passwort ein um das Entfernen des 2FA-Tokens zu bestätigen"
|
||||
];
|
||||
@@ -23,10 +23,14 @@ return [
|
||||
"email" => "E-Mail Address",
|
||||
"password" => "Password",
|
||||
"password_confirm" => "Confirm Password",
|
||||
"password_old" => "Old Password",
|
||||
"password_new" => "New Password",
|
||||
"full_name" => "Full Name",
|
||||
"remember_me" => "Remember Me",
|
||||
"signing_in" => "Signing in",
|
||||
"sign_in" => "Sign In",
|
||||
"forgot_password" => "Forgot password?",
|
||||
"change_password" => "Change password",
|
||||
"register_text" => "Don't have an account? Sign Up",
|
||||
"passwords_do_not_match" => "Your passwords did not match",
|
||||
"back_to_login" => "Back to Login",
|
||||
@@ -38,4 +42,6 @@ return [
|
||||
"invalid_link" => "The link you visited is no longer valid",
|
||||
"confirm_success" => "Your e-mail address was successfully confirmed, you may now log in",
|
||||
"confirm_error" => "Error confirming e-mail address",
|
||||
"gpg_key" => "GPG Key",
|
||||
"2fa_token" => "Two-Factor Authentication (2FA)",
|
||||
];
|
||||
@@ -12,10 +12,10 @@ return [
|
||||
"name" => "Name",
|
||||
"type" => "Type",
|
||||
"size" => "Size",
|
||||
"last_modified" => "Last Modified",
|
||||
|
||||
# dialog / actions
|
||||
"action" => "Action",
|
||||
"actions" => "Actions",
|
||||
"title" => "Title",
|
||||
"message" => "Message",
|
||||
"edit" => "Edit",
|
||||
@@ -24,12 +24,19 @@ return [
|
||||
"request" => "Request",
|
||||
"cancel" => "Cancel",
|
||||
"confirm" => "Confirm",
|
||||
"remove" => "Remove",
|
||||
"change" => "Change",
|
||||
"reset" => "Reset",
|
||||
"retry" => "Retry",
|
||||
"go_back" => "Go Back",
|
||||
"save" => "Save",
|
||||
"saving" => "Saving",
|
||||
"delete" => "Delete",
|
||||
"info" => "Info",
|
||||
"reload" => "Reload",
|
||||
|
||||
# file
|
||||
"choose_file" => "Choose File",
|
||||
"download" => "Download",
|
||||
"download_all" => "Download All",
|
||||
"upload" => "Upload",
|
||||
@@ -37,7 +44,7 @@ return [
|
||||
"rename" => "Rename",
|
||||
"move" => "Move",
|
||||
"overwrite" => "Overwrite",
|
||||
"reload" => "Reload",
|
||||
|
||||
|
||||
# data table
|
||||
"showing_x_of_y_entries" => "Showing %d of %d entries",
|
||||
@@ -47,6 +54,8 @@ return [
|
||||
"date" => "Date",
|
||||
"start_date" => "Start Date",
|
||||
"end_date" => "End Date",
|
||||
"last_modified" => "Last Modified",
|
||||
"timestamp" => "Timestamp",
|
||||
"date_format" => "m/d/Y",
|
||||
"date_time_format" => "m/d/Y G:i A",
|
||||
"date_time_format_precise" => "m/d/Y G:i:s A",
|
||||
|
||||
24
Core/Localization/en_US/settings.php
Normal file
24
Core/Localization/en_US/settings.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"settings" => "Settings",
|
||||
"information" => "Information",
|
||||
|
||||
# API Key
|
||||
"api_key" => "API Key",
|
||||
"valid_until" => "Valid until",
|
||||
"token" => "Token",
|
||||
"request_new_key" => "Request new Key",
|
||||
"show_only_active_keys" => "Show only active keys",
|
||||
|
||||
# GPG Key
|
||||
"gpg_key_placeholder_text" => "Paste or drag'n'drop your GPG-Key in ASCII format...",
|
||||
|
||||
# 2fa
|
||||
"register_2fa_device" => "Register a 2FA-Device",
|
||||
"register_2fa_totp_text" => "Scan the QR-Code with a device you want to use for Two-Factor-Authentication (2FA). " .
|
||||
"On Android, you can use the Google Authenticator.",
|
||||
"register_2fa_fido_text" => "You may need to interact with your Device, e.g. typing in your PIN or touching to confirm the registration.",
|
||||
"remove_2fa" => "Remove 2FA Token",
|
||||
"remove_2fa_text" => "Enter your current password to confirm the removal of your 2FA Token",
|
||||
];
|
||||
@@ -139,7 +139,7 @@ class Context {
|
||||
->where(new CondOr(
|
||||
new CondLike("name", "%$lang%"), // english
|
||||
new Compare("code", $lang), // de_DE
|
||||
new CondLike("code", "${lang}_%") // de -> de_%
|
||||
new CondLike("code", "{$lang}_%") // de -> de_%
|
||||
))
|
||||
);
|
||||
if ($language) {
|
||||
|
||||
@@ -57,6 +57,12 @@ abstract class DatabaseEntity implements ArrayAccess, JsonSerializable {
|
||||
public function jsonSerialize(?array $propertyNames = null): array {
|
||||
$reflectionClass = (new \ReflectionClass(get_called_class()));
|
||||
$properties = $reflectionClass->getProperties();
|
||||
|
||||
while ($reflectionClass->getParentClass()->getName() !== DatabaseEntity::class) {
|
||||
$reflectionClass = $reflectionClass->getParentClass();
|
||||
$properties = array_merge($reflectionClass->getProperties(), $properties);
|
||||
}
|
||||
|
||||
$ignoredProperties = ["entityLogConfig", "customData"];
|
||||
|
||||
$jsonArray = [];
|
||||
|
||||
@@ -524,7 +524,15 @@ class DatabaseEntityHandler implements Persistable {
|
||||
$thisIdProperty->setValue($relEntity, $entity);
|
||||
}
|
||||
|
||||
$success = $otherHandler->getInsertQuery($relEntities)->execute() && $success;
|
||||
$statement = $otherHandler->getInsertQuery($relEntities);
|
||||
if ($ignoreExisting) {
|
||||
$columns = $nmRelation->getRefColumns();
|
||||
$statement->onDuplicateKeyStrategy(new UpdateStrategy($columns, [
|
||||
$thisIdColumn => $entity->getId()
|
||||
]));
|
||||
}
|
||||
|
||||
$success = $statement->execute() && $success;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -557,6 +565,10 @@ class DatabaseEntityHandler implements Persistable {
|
||||
}
|
||||
|
||||
$entityIds = array_keys($entities);
|
||||
if (empty($entityIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->nmRelations as $nmProperty => $nmRelation) {
|
||||
$nmTable = $nmRelation->getTableName();
|
||||
$property = $this->properties[$nmProperty];
|
||||
@@ -599,29 +611,27 @@ class DatabaseEntityHandler implements Persistable {
|
||||
|
||||
$otherHandler = $nmRelation->getRelHandler();
|
||||
$thisIdColumn = $otherHandler->getColumnName($nmRelation->getThisProperty(), false);
|
||||
$relIdColumn = $otherHandler->getColumnName($nmRelation->getRefProperty(), false);
|
||||
if (!empty($entityIds)) {
|
||||
$relEntityQuery = DatabaseEntityQuery::fetchAll($otherHandler)
|
||||
->where(new CondIn(new Column($thisIdColumn), $entityIds));
|
||||
$relIdColumn = $otherHandler->getColumnName($nmRelation->getRefProperty(), false);
|
||||
$relEntityQuery = DatabaseEntityQuery::fetchAll($otherHandler)
|
||||
->where(new CondIn(new Column($thisIdColumn), $entityIds));
|
||||
|
||||
$relEntityQuery->fetchEntities($fetchEntities === DatabaseEntityQuery::FETCH_RECURSIVE);
|
||||
$rows = $relEntityQuery->executeSQL();
|
||||
if (!is_array($rows)) {
|
||||
$this->logger->error("Error fetching n:m relations from table: '$nmTable': " . $this->sql->getLastError());
|
||||
return;
|
||||
}
|
||||
$relEntityQuery->fetchEntities($fetchEntities === DatabaseEntityQuery::FETCH_RECURSIVE);
|
||||
$rows = $relEntityQuery->executeSQL();
|
||||
if (!is_array($rows)) {
|
||||
$this->logger->error("Error fetching n:m relations from table: '$nmTable': " . $this->sql->getLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
$thisIdProperty = $otherHandler->properties[$nmRelation->getThisProperty()];
|
||||
$thisIdProperty->setAccessible(true);
|
||||
$thisIdProperty = $otherHandler->properties[$nmRelation->getThisProperty()];
|
||||
$thisIdProperty->setAccessible(true);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$relEntity = $otherHandler->entityFromRow($row, [], $fetchEntities);
|
||||
$thisEntity = $entities[$row[$thisIdColumn]];
|
||||
$thisIdProperty->setValue($relEntity, $thisEntity);
|
||||
$targetArray = $property->getValue($thisEntity);
|
||||
$targetArray[$row[$relIdColumn]] = $relEntity;
|
||||
$property->setValue($thisEntity, $targetArray);
|
||||
}
|
||||
foreach ($rows as $row) {
|
||||
$relEntity = $otherHandler->entityFromRow($row, [], $fetchEntities);
|
||||
$thisEntity = $entities[$row[$thisIdColumn]];
|
||||
$thisIdProperty->setValue($relEntity, $thisEntity);
|
||||
$targetArray = $property->getValue($thisEntity);
|
||||
$targetArray[$row[$relIdColumn]] = $relEntity;
|
||||
$property->setValue($thisEntity, $targetArray);
|
||||
}
|
||||
} else {
|
||||
$this->logger->error("fetchNMRelations for type '" . get_class($nmRelation) . "' is not implemented");
|
||||
|
||||
@@ -94,7 +94,7 @@ class DatabaseEntityQuery extends Select {
|
||||
|
||||
$relIndex = 1;
|
||||
foreach ($this->handler->getRelations() as $propertyName => $relationHandler) {
|
||||
if ($this->handler !== $relationHandler) {
|
||||
if ($this->handler !== $relationHandler || !$recursive) {
|
||||
$this->fetchRelation($propertyName, $this->handler->getTableName(), $this->handler, $relationHandler, $relIndex, $recursive);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,13 @@ class NMRelationReference implements Persistable {
|
||||
return $this->refProperty;
|
||||
}
|
||||
|
||||
public function getRefColumns(): array {
|
||||
return [
|
||||
$this->handler->getColumnName($this->getThisProperty(), false),
|
||||
$this->handler->getColumnName($this->getRefProperty(), false),
|
||||
];
|
||||
}
|
||||
|
||||
public function getRelHandler(): DatabaseEntityHandler {
|
||||
return $this->handler;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace Core\Objects\DatabaseEntity;
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Core\Objects\DatabaseEntity\Attribute\ExtendingEnum;
|
||||
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
|
||||
use Core\Objects\DatabaseEntity\Attribute\Transient;
|
||||
use Core\Objects\DatabaseEntity\Attribute\Visibility;
|
||||
use Core\Objects\TwoFactor\KeyBasedTwoFactorToken;
|
||||
use Core\Objects\TwoFactor\TimeBasedTwoFactorToken;
|
||||
use Core\Objects\DatabaseEntity\Controller\DatabaseEntity;
|
||||
@@ -18,8 +20,13 @@ abstract class TwoFactorToken extends DatabaseEntity {
|
||||
|
||||
#[ExtendingEnum(self::TWO_FACTOR_TOKEN_TYPES)] private string $type;
|
||||
private bool $confirmed;
|
||||
|
||||
#[Transient]
|
||||
private bool $authenticated;
|
||||
#[MaxLength(512)] private ?string $data;
|
||||
|
||||
#[MaxLength(512)]
|
||||
#[Visibility(Visibility::NONE)]
|
||||
private ?string $data;
|
||||
|
||||
public function __construct(string $type, ?int $id = null, bool $confirmed = false) {
|
||||
parent::__construct($id);
|
||||
@@ -39,6 +46,7 @@ abstract class TwoFactorToken extends DatabaseEntity {
|
||||
|
||||
public function postFetch(SQL $sql, array $row) {
|
||||
parent::postFetch($sql, $row);
|
||||
$this->authenticated = $_SESSION["2faAuthenticated"] ?? false;
|
||||
$this->readData($row["data"]);
|
||||
}
|
||||
|
||||
@@ -63,4 +71,14 @@ abstract class TwoFactorToken extends DatabaseEntity {
|
||||
$this->confirmed = true;
|
||||
return $this->save($sql) !== false;
|
||||
}
|
||||
|
||||
public function jsonSerialize(?array $propertyNames = null): array {
|
||||
$jsonData = parent::jsonSerialize($propertyNames);
|
||||
|
||||
if ($propertyNames === null || in_array("authenticated", $propertyNames)) {
|
||||
$jsonData["authenticated"] = $this->authenticated;
|
||||
}
|
||||
|
||||
return $jsonData;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
namespace Core\Objects\DatabaseEntity;
|
||||
|
||||
use Core\Driver\SQL\Column\Column;
|
||||
use Core\Driver\SQL\Expression\Alias;
|
||||
use Core\Driver\SQL\Expression\Coalesce;
|
||||
use Core\Driver\SQL\Expression\CurrentTimeStamp;
|
||||
use Core\Driver\SQL\Expression\NullIf;
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Core\Objects\DatabaseEntity\Attribute\DefaultValue;
|
||||
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
|
||||
@@ -141,4 +145,13 @@ class User extends DatabaseEntity {
|
||||
public function getDisplayName(): string {
|
||||
return !empty($this->fullName) ? $this->fullName : $this->name;
|
||||
}
|
||||
|
||||
public static function buildSQLDisplayName(SQL $sql, string $joinColumn): Alias {
|
||||
return new Alias(
|
||||
$sql->select(new Coalesce(
|
||||
new NullIf(new Column("User.full_name"), ""),
|
||||
new NullIf(new Column("User.name"), ""))
|
||||
)->from("User")->whereEq("User.id", new Column($joinColumn)),
|
||||
"user");
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use Core\Objects\ApiObject;
|
||||
|
||||
class AttestationObject extends ApiObject {
|
||||
|
||||
use Core\Objects\TwoFactor\CBORDecoder;
|
||||
use CBORDecoder;
|
||||
|
||||
private string $format;
|
||||
private array $statement;
|
||||
|
||||
@@ -3,70 +3,92 @@
|
||||
namespace Core\Objects\TwoFactor;
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Core\Objects\DatabaseEntity\Attribute\Transient;
|
||||
use Cose\Algorithm\Signature\ECDSA\ECSignature;
|
||||
use Core\Objects\DatabaseEntity\TwoFactorToken;
|
||||
use Cose\Key\Key;
|
||||
|
||||
class KeyBasedTwoFactorToken extends TwoFactorToken {
|
||||
|
||||
const TYPE = "fido";
|
||||
|
||||
#[Transient]
|
||||
private ?string $challenge;
|
||||
private ?string $credentialId;
|
||||
|
||||
#[Transient]
|
||||
private ?string $credentialID;
|
||||
|
||||
#[Transient]
|
||||
private ?PublicKey $publicKey;
|
||||
|
||||
public function __construct(string $challenge) {
|
||||
private function __construct() {
|
||||
parent::__construct(self::TYPE);
|
||||
$this->challenge = $challenge;
|
||||
}
|
||||
|
||||
public function generateChallenge(int $length = 32) {
|
||||
$this->challenge = base64_encode(generateRandomString($length, "raw"));
|
||||
$_SESSION["challenge"] = $this->challenge;
|
||||
}
|
||||
|
||||
public static function create(int $challengeLength = 32): KeyBasedTwoFactorToken {
|
||||
$token = new KeyBasedTwoFactorToken();
|
||||
$token->generateChallenge($challengeLength);
|
||||
return $token;
|
||||
}
|
||||
|
||||
public function getChallenge(): string {
|
||||
return $this->challenge;
|
||||
}
|
||||
|
||||
protected function readData(string $data) {
|
||||
if (!$this->isConfirmed()) {
|
||||
$this->challenge = base64_decode($data);
|
||||
$this->credentialId = null;
|
||||
$this->challenge = $data;
|
||||
$this->credentialID = null;
|
||||
$this->publicKey = null;
|
||||
} else {
|
||||
$jsonData = json_decode($data, true);
|
||||
$this->challenge = base64_decode($_SESSION["challenge"] ?? "");
|
||||
$this->credentialId = base64_decode($jsonData["credentialID"]);
|
||||
$this->challenge = $_SESSION["challenge"] ?? "";
|
||||
$this->credentialID = base64_decode($jsonData["credentialID"]);
|
||||
$this->publicKey = PublicKey::fromJson($jsonData["publicKey"]);
|
||||
}
|
||||
}
|
||||
|
||||
public function getData(): string {
|
||||
if ($this->isConfirmed()) {
|
||||
return base64_encode($this->challenge);
|
||||
if (!$this->isConfirmed()) {
|
||||
return $this->challenge;
|
||||
} else {
|
||||
return json_encode([
|
||||
"credentialId" => $this->credentialId,
|
||||
"credentialID" => $this->credentialID,
|
||||
"publicKey" => $this->publicKey->jsonSerialize()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function confirmKeyBased(SQL $sql, string $credentialId, PublicKey $publicKey): bool {
|
||||
$this->credentialId = $credentialId;
|
||||
public function confirmKeyBased(SQL $sql, string $credentialID, PublicKey $publicKey): bool {
|
||||
$this->credentialID = $credentialID;
|
||||
$this->publicKey = $publicKey;
|
||||
return parent::confirm($sql);
|
||||
}
|
||||
|
||||
|
||||
public function getPublicKey(): ?PublicKey {
|
||||
return $this->publicKey;
|
||||
}
|
||||
|
||||
public function getCredentialId(): ?string {
|
||||
return $this->credentialId;
|
||||
return $this->credentialID;
|
||||
}
|
||||
|
||||
public function jsonSerialize(?array $propertyNames = null): array {
|
||||
$jsonData = parent::jsonSerialize();
|
||||
|
||||
if (!empty($this->challenge) && !$this->isAuthenticated() && in_array("challenge", $propertyNames)) {
|
||||
$jsonData["challenge"] = base64_encode($this->challenge);
|
||||
}
|
||||
if (!$this->isAuthenticated()) {
|
||||
if (!empty($this->challenge) && ($propertyNames === null || in_array("challenge", $propertyNames))) {
|
||||
$jsonData["challenge"] = $this->challenge;
|
||||
}
|
||||
|
||||
if (!empty($this->credentialId) && in_array("credentialID", $propertyNames)) {
|
||||
$jsonData["credentialID"] = base64_encode($this->credentialId);
|
||||
if (!empty($this->credentialID) && ($propertyNames === null || in_array("credentialID", $propertyNames))) {
|
||||
$jsonData["credentialID"] = base64_encode($this->credentialID);
|
||||
}
|
||||
}
|
||||
|
||||
return $jsonData;
|
||||
|
||||
@@ -6,7 +6,7 @@ use Core\Objects\ApiObject;
|
||||
|
||||
class PublicKey extends ApiObject {
|
||||
|
||||
use Core\Objects\TwoFactor\CBORDecoder;
|
||||
use CBORDecoder;
|
||||
|
||||
private int $keyType;
|
||||
private int $usedAlgorithm;
|
||||
|
||||
@@ -7,11 +7,14 @@ use chillerlan\QRCode\QRCode;
|
||||
use chillerlan\QRCode\QROptions;
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\DatabaseEntity\Attribute\Visibility;
|
||||
use Core\Objects\DatabaseEntity\TwoFactorToken;
|
||||
|
||||
class TimeBasedTwoFactorToken extends TwoFactorToken {
|
||||
|
||||
const TYPE = "totp";
|
||||
|
||||
#[Visibility(Visibility::NONE)]
|
||||
private string $secret;
|
||||
|
||||
public function __construct(string $secret) {
|
||||
|
||||
@@ -229,7 +229,7 @@ function getClassPath($class, string $suffix = ".class"): string {
|
||||
$group = $pathParts[$pathCount - 2];
|
||||
if (strcasecmp($group, "Parameter") !== 0 && strcasecmp($group, "Traits") !== 0) {
|
||||
$pathParts = array_slice($pathParts, 0, $pathCount - 2);
|
||||
$pathParts[] = "${group}API";
|
||||
$pathParts[] = "{$group}API";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user