Namespace and ClassPath rewrites
This commit is contained in:
182
Core/Objects/AesStream.class.php
Normal file
182
Core/Objects/AesStream.class.php
Normal file
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects;
|
||||
|
||||
class AesStream {
|
||||
|
||||
private string $key;
|
||||
private string $iv;
|
||||
private $callback;
|
||||
private ?string $outputFile;
|
||||
private ?string $inputFile;
|
||||
private int $offset;
|
||||
private ?int $length;
|
||||
|
||||
//
|
||||
private ?string $md5SumIn;
|
||||
private ?string $sha1SumIn;
|
||||
private ?string $md5SumOut;
|
||||
private ?string $sha1SumOut;
|
||||
|
||||
public function __construct(string $key, string $iv) {
|
||||
$this->key = $key;
|
||||
$this->iv = $iv;
|
||||
$this->inputFile = null;
|
||||
$this->outputFile = null;
|
||||
$this->callback = null;
|
||||
$this->offset = 0;
|
||||
$this->length = null;
|
||||
$this->md5SumIn = null;
|
||||
$this->sha1SumIn = null;
|
||||
$this->md5SumOut = null;
|
||||
$this->sha1SumOut = null;
|
||||
|
||||
if (!in_array(strlen($key), [16, 24, 32])) {
|
||||
throw new \Exception("Invalid Key Size");
|
||||
} else if (strlen($iv) !== 16) {
|
||||
throw new \Exception("Invalid IV Size");
|
||||
}
|
||||
}
|
||||
|
||||
public function setInputFile(string $file) {
|
||||
$this->inputFile = $file;
|
||||
}
|
||||
|
||||
public function setOutput(callable $callback) {
|
||||
$this->callback = $callback;
|
||||
}
|
||||
|
||||
public function setOutputFile(string $file) {
|
||||
$this->outputFile = $file;
|
||||
}
|
||||
|
||||
public function start(): bool {
|
||||
if (!$this->inputFile) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$blockSize = 16;
|
||||
$aesMode = $this->getCipherMode();
|
||||
|
||||
$outputHandle = null;
|
||||
$inputHandle = fopen($this->inputFile, "rb");
|
||||
if (!$inputHandle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->outputFile !== null) {
|
||||
$outputHandle = fopen($this->outputFile, "wb");
|
||||
if (!$outputHandle) {
|
||||
fclose($inputHandle);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
set_time_limit(0);
|
||||
$md5ContextIn = hash_init("md5");
|
||||
$sha1ContextIn = hash_init("sha1");
|
||||
$md5ContextOut = hash_init("md5");
|
||||
$sha1ContextOut = hash_init("sha1");
|
||||
|
||||
$ivCounter = $this->iv;
|
||||
$modulo = \gmp_init("0x1" . str_repeat("00", $blockSize), 16);
|
||||
$written = 0;
|
||||
|
||||
while (!feof($inputHandle)) {
|
||||
$chunk = fread($inputHandle, 65536);
|
||||
$chunkSize = strlen($chunk);
|
||||
if ($chunkSize > 0) {
|
||||
|
||||
hash_update($md5ContextIn, $chunk);
|
||||
hash_update($sha1ContextIn, $chunk);
|
||||
|
||||
$blockCount = intval(ceil($chunkSize / $blockSize));
|
||||
$encrypted = openssl_encrypt($chunk, $aesMode, $this->key, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING, $ivCounter);
|
||||
|
||||
$ivNumber = \gmp_init(bin2hex($ivCounter), 16);
|
||||
$ivNumber = \gmp_add($ivNumber, $blockCount);
|
||||
$ivNumber = \gmp_mod($ivNumber, $modulo);
|
||||
$ivNumber = str_pad(\gmp_strval($ivNumber, 16), $blockSize * 2, "0", STR_PAD_LEFT);
|
||||
$ivCounter = hex2bin($ivNumber);
|
||||
|
||||
// partial content
|
||||
$skip = false;
|
||||
if ($this->offset > 0 && $written < $this->offset) {
|
||||
if ($written + $chunkSize >= $this->offset) {
|
||||
$encrypted = substr($encrypted, $this->offset - $written);
|
||||
} else {
|
||||
$skip = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->length !== null) {
|
||||
$notSkipped = max($written - $this->offset, 0);
|
||||
if ($notSkipped + $chunkSize >= $this->length) {
|
||||
$encrypted = substr($encrypted, 0, $this->length - $notSkipped);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$skip) {
|
||||
if ($this->callback !== null) {
|
||||
call_user_func($this->callback, $encrypted);
|
||||
}
|
||||
|
||||
if ($outputHandle !== null) {
|
||||
fwrite($outputHandle, $encrypted);
|
||||
}
|
||||
|
||||
hash_update($md5ContextOut, $encrypted);
|
||||
hash_update($sha1ContextOut, $encrypted);
|
||||
}
|
||||
|
||||
$written += $chunkSize;
|
||||
if ($this->length !== null && $written - $this->offset >= $this->length) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fclose($inputHandle);
|
||||
if ($outputHandle) {
|
||||
fclose($outputHandle);
|
||||
}
|
||||
|
||||
$this->md5SumIn = hash_final($md5ContextIn, false);
|
||||
$this->sha1SumIn = hash_final($sha1ContextIn, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getCipherMode(): string {
|
||||
$bitStrength = strlen($this->key) * 8;
|
||||
return "aes-$bitStrength-ctr";
|
||||
}
|
||||
|
||||
public function getKey(): string {
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
public function getIV(): string {
|
||||
return $this->iv;
|
||||
}
|
||||
|
||||
public function setRange(int $offset, int $length) {
|
||||
$this->offset = $offset;
|
||||
$this->length = $length;
|
||||
}
|
||||
|
||||
public function getMD5SumIn(): ?string {
|
||||
return $this->md5SumIn;
|
||||
}
|
||||
|
||||
public function getSHA1SumIn(): ?string {
|
||||
return $this->sha1SumIn;
|
||||
}
|
||||
|
||||
public function getMD5SumOut(): ?string {
|
||||
return $this->md5SumOut;
|
||||
}
|
||||
|
||||
public function getSHA1SumOut(): ?string {
|
||||
return $this->sha1SumOut;
|
||||
}
|
||||
}
|
||||
11
Core/Objects/ApiObject.class.php
Normal file
11
Core/Objects/ApiObject.class.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects;
|
||||
|
||||
abstract class ApiObject implements \JsonSerializable {
|
||||
|
||||
public abstract function jsonSerialize(): array;
|
||||
|
||||
public function __toString() { return json_encode($this->jsonSerialize()); }
|
||||
|
||||
}
|
||||
42
Core/Objects/ConnectionData.class.php
Normal file
42
Core/Objects/ConnectionData.class.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects;
|
||||
|
||||
class ConnectionData {
|
||||
|
||||
private string $host;
|
||||
private int $port;
|
||||
private string $login;
|
||||
private string $password;
|
||||
private array $properties;
|
||||
|
||||
public function __construct($host, $port, $login, $password) {
|
||||
$this->host = $host;
|
||||
$this->port = $port;
|
||||
$this->login = $login;
|
||||
$this->password = $password;
|
||||
$this->properties = array();
|
||||
}
|
||||
|
||||
public function getProperties(): array {
|
||||
return $this->properties;
|
||||
}
|
||||
|
||||
public function getProperty($key, $defaultValue='') {
|
||||
return $this->properties[$key] ?? $defaultValue;
|
||||
}
|
||||
|
||||
public function setProperty($key, $val): bool {
|
||||
if (!is_scalar($val)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->properties[$key] = $val;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getHost(): string { return $this->host; }
|
||||
public function getPort(): int { return $this->port; }
|
||||
public function getLogin(): string { return $this->login; }
|
||||
public function getPassword(): string { return $this->password; }
|
||||
}
|
||||
205
Core/Objects/Context.class.php
Normal file
205
Core/Objects/Context.class.php
Normal file
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects;
|
||||
|
||||
use Core\Configuration\Configuration;
|
||||
use Core\Configuration\Settings;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Condition\CondLike;
|
||||
use Core\Driver\SQL\Condition\CondOr;
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Firebase\JWT\JWT;
|
||||
use Core\Objects\DatabaseEntity\Language;
|
||||
use Core\Objects\DatabaseEntity\Session;
|
||||
use Core\Objects\DatabaseEntity\User;
|
||||
use Core\Objects\Router\Router;
|
||||
|
||||
class Context {
|
||||
|
||||
private ?SQL $sql;
|
||||
private ?Session $session;
|
||||
private ?User $user;
|
||||
private Configuration $configuration;
|
||||
private Language $language;
|
||||
public ?Router $router;
|
||||
|
||||
public function __construct() {
|
||||
|
||||
$this->sql = null;
|
||||
$this->session = null;
|
||||
$this->user = null;
|
||||
$this->router = null;
|
||||
$this->configuration = new Configuration();
|
||||
$this->setLanguage(Language::DEFAULT_LANGUAGE());
|
||||
|
||||
if (!$this->isCLI()) {
|
||||
@session_start();
|
||||
}
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
if ($this->sql && $this->sql->isConnected()) {
|
||||
$this->sql->close();
|
||||
$this->sql = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function setLanguage(Language $language) {
|
||||
$this->language = $language;
|
||||
$this->language->activate();
|
||||
|
||||
if ($this->user && $this->user->language->getId() !== $language->getId()) {
|
||||
$this->user->language = $language;
|
||||
}
|
||||
}
|
||||
|
||||
public function initSQL(): ?SQL {
|
||||
$databaseConf = $this->configuration->getDatabase();
|
||||
if ($databaseConf) {
|
||||
$this->sql = SQL::createConnection($databaseConf);
|
||||
if ($this->sql->isConnected()) {
|
||||
$settings = $this->configuration->getSettings();
|
||||
$settings->loadFromDatabase($this);
|
||||
return $this->sql;
|
||||
}
|
||||
} else {
|
||||
$this->sql = null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getSQL(): ?SQL {
|
||||
return $this->sql;
|
||||
}
|
||||
|
||||
public function getSettings(): Settings {
|
||||
return $this->configuration->getSettings();
|
||||
}
|
||||
|
||||
public function getUser(): ?User {
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function sendCookies() {
|
||||
$domain = $this->getSettings()->getDomain();
|
||||
$this->language->sendCookie($domain);
|
||||
$this->session?->sendCookie($domain);
|
||||
$this->session?->update();
|
||||
session_write_close();
|
||||
}
|
||||
|
||||
private function loadSession(int $userId, int $sessionId) {
|
||||
$this->session = Session::init($this, $userId, $sessionId);
|
||||
$this->user = $this->session?->getUser();
|
||||
}
|
||||
|
||||
public function parseCookies() {
|
||||
if (isset($_COOKIE['session']) && is_string($_COOKIE['session']) && !empty($_COOKIE['session'])) {
|
||||
try {
|
||||
$token = $_COOKIE['session'];
|
||||
$settings = $this->configuration->getSettings();
|
||||
$decoded = (array)JWT::decode($token, $settings->getJwtSecretKey());
|
||||
if (!is_null($decoded)) {
|
||||
$userId = ($decoded['userId'] ?? NULL);
|
||||
$sessionId = ($decoded['sessionId'] ?? NULL);
|
||||
if (!is_null($userId) && !is_null($sessionId)) {
|
||||
$this->loadSession($userId, $sessionId);
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
// set language by priority: 1. GET parameter, 2. cookie, 3. user's settings
|
||||
if (isset($_GET['lang']) && is_string($_GET["lang"]) && !empty($_GET["lang"])) {
|
||||
$this->updateLanguage($_GET['lang']);
|
||||
} else if (isset($_COOKIE['lang']) && is_string($_COOKIE["lang"]) && !empty($_COOKIE["lang"])) {
|
||||
$this->updateLanguage($_COOKIE['lang']);
|
||||
} else if ($this->user) {
|
||||
$this->setLanguage($this->user->language);
|
||||
}
|
||||
}
|
||||
|
||||
public function updateLanguage(string $lang): bool {
|
||||
if ($this->sql) {
|
||||
$language = Language::findBuilder($this->sql)
|
||||
->where(new CondOr(
|
||||
new CondLike("name", "%$lang%"), // english
|
||||
new Compare("code", $lang), // de_DE
|
||||
new CondLike("code", $lang . "_%"))) // de -> de_%
|
||||
->execute();
|
||||
if ($language) {
|
||||
$this->setLanguage($language);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function processVisit() {
|
||||
if (isset($_COOKIE["PHPSESSID"]) && !empty($_COOKIE["PHPSESSID"])) {
|
||||
if ($this->isBot()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cookie = $_COOKIE["PHPSESSID"];
|
||||
$req = new \Core\API\Visitors\ProcessVisit($this);
|
||||
$req->execute(["cookie" => $cookie]);
|
||||
}
|
||||
}
|
||||
|
||||
private function isBot(): bool {
|
||||
if (empty($_SERVER["HTTP_USER_AGENT"])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return preg_match('/robot|spider|crawler|curl|^$/i', $_SERVER['HTTP_USER_AGENT']) === 1;
|
||||
}
|
||||
|
||||
public function isCLI(): bool {
|
||||
return php_sapi_name() === "cli";
|
||||
}
|
||||
|
||||
public function getConfig(): Configuration {
|
||||
return $this->configuration;
|
||||
}
|
||||
|
||||
public function getSession(): ?Session {
|
||||
return $this->session;
|
||||
}
|
||||
|
||||
public function loadApiKey(string $apiKey): bool {
|
||||
$this->user = User::findBuilder($this->sql)
|
||||
->addJoin(new \Driver\SQL\Join("INNER","ApiKey", "ApiKey.user_id", "User.id"))
|
||||
->where(new Compare("ApiKey.api_key", $apiKey))
|
||||
->where(new Compare("valid_until", $this->sql->currentTimestamp(), ">"))
|
||||
->where(new Compare("ApiKey.active", true))
|
||||
->where(new Compare("User.confirmed", true))
|
||||
->fetchEntities()
|
||||
->execute();
|
||||
|
||||
return $this->user !== null;
|
||||
}
|
||||
|
||||
public function createSession(int $userId, bool $stayLoggedIn): ?Session {
|
||||
$this->user = User::find($this->sql, $userId);
|
||||
if ($this->user) {
|
||||
$this->session = new Session($this, $this->user);
|
||||
$this->session->stayLoggedIn = $stayLoggedIn;
|
||||
if ($this->session->update()) {
|
||||
return $this->session;
|
||||
}
|
||||
}
|
||||
|
||||
$this->user = null;
|
||||
$this->session = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getLanguage(): Language {
|
||||
return $this->language;
|
||||
}
|
||||
}
|
||||
18
Core/Objects/CustomTwigFunctions.class.php
Normal file
18
Core/Objects/CustomTwigFunctions.class.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects;
|
||||
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class CustomTwigFunctions extends AbstractExtension {
|
||||
public function getFunctions(): array {
|
||||
return [
|
||||
new TwigFunction('L', array($this, 'translate')),
|
||||
];
|
||||
}
|
||||
|
||||
public function translate(string $key): string {
|
||||
return L($key);
|
||||
}
|
||||
}
|
||||
27
Core/Objects/DatabaseEntity/ApiKey.class.php
Normal file
27
Core/Objects/DatabaseEntity/ApiKey.class.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\DatabaseEntity;
|
||||
|
||||
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
|
||||
|
||||
class ApiKey extends DatabaseEntity {
|
||||
|
||||
private bool $active;
|
||||
#[MaxLength(64)] public String $apiKey;
|
||||
public \DateTime $validUntil;
|
||||
public User $user;
|
||||
|
||||
public function __construct(?int $id = null) {
|
||||
parent::__construct($id);
|
||||
$this->active = true;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return [
|
||||
"id" => $this->getId(),
|
||||
"active" => $this->active,
|
||||
"apiKey" => $this->apiKey,
|
||||
"validUntil" => $this->validUntil->getTimestamp()
|
||||
];
|
||||
}
|
||||
}
|
||||
21
Core/Objects/DatabaseEntity/Attribute/DefaultValue.class.php
Normal file
21
Core/Objects/DatabaseEntity/Attribute/DefaultValue.class.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\DatabaseEntity\Attribute;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_PROPERTY)] class DefaultValue {
|
||||
|
||||
private mixed $value;
|
||||
|
||||
public function __construct(mixed $value) {
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public function getValue() {
|
||||
if (is_string($this->value) && isClass($this->value)) {
|
||||
return new $this->value();
|
||||
}
|
||||
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
}
|
||||
17
Core/Objects/DatabaseEntity/Attribute/Enum.class.php
Normal file
17
Core/Objects/DatabaseEntity/Attribute/Enum.class.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\DatabaseEntity\Attribute;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_PROPERTY)] class Enum {
|
||||
|
||||
private array $values;
|
||||
|
||||
public function __construct(string ...$values) {
|
||||
$this->values = $values;
|
||||
}
|
||||
|
||||
public function getValues(): array {
|
||||
return $this->values;
|
||||
}
|
||||
|
||||
}
|
||||
7
Core/Objects/DatabaseEntity/Attribute/Json.class.php
Normal file
7
Core/Objects/DatabaseEntity/Attribute/Json.class.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\DatabaseEntity\Attribute;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_PROPERTY)] class Json {
|
||||
|
||||
}
|
||||
16
Core/Objects/DatabaseEntity/Attribute/Many.class.php
Normal file
16
Core/Objects/DatabaseEntity/Attribute/Many.class.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\DatabaseEntity\Attribute;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_PROPERTY)] class Many {
|
||||
|
||||
private string $type;
|
||||
|
||||
public function __construct(string $type) {
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
public function getValue(): string {
|
||||
return $this->type;
|
||||
}
|
||||
}
|
||||
15
Core/Objects/DatabaseEntity/Attribute/MaxLength.class.php
Normal file
15
Core/Objects/DatabaseEntity/Attribute/MaxLength.class.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\DatabaseEntity\Attribute;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_PROPERTY)] class MaxLength {
|
||||
private int $maxLength;
|
||||
|
||||
function __construct(int $maxLength) {
|
||||
$this->maxLength = $maxLength;
|
||||
}
|
||||
|
||||
public function getValue(): int {
|
||||
return $this->maxLength;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\DatabaseEntity\Attribute;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_PROPERTY)] class Transient {
|
||||
|
||||
}
|
||||
7
Core/Objects/DatabaseEntity/Attribute/Unique.class.php
Normal file
7
Core/Objects/DatabaseEntity/Attribute/Unique.class.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\DatabaseEntity\Attribute;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_PROPERTY)] class Unique {
|
||||
|
||||
}
|
||||
130
Core/Objects/DatabaseEntity/DatabaseEntity.class.php
Normal file
130
Core/Objects/DatabaseEntity/DatabaseEntity.class.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\DatabaseEntity;
|
||||
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Condition\Condition;
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
abstract class DatabaseEntity {
|
||||
|
||||
private static array $handlers = [];
|
||||
protected ?int $id;
|
||||
|
||||
public function __construct(?int $id = null) {
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public abstract function jsonSerialize(): array;
|
||||
|
||||
public function preInsert(array &$row) { }
|
||||
public function postFetch(SQL $sql, array $row) { }
|
||||
|
||||
public static function fromRow(SQL $sql, array $row): static {
|
||||
$handler = self::getHandler($sql);
|
||||
return $handler->entityFromRow($row);
|
||||
}
|
||||
|
||||
public static function newInstance(\ReflectionClass $reflectionClass, array $row) {
|
||||
return $reflectionClass->newInstanceWithoutConstructor();
|
||||
}
|
||||
|
||||
public static function find(SQL $sql, int $id, bool $fetchEntities = false, bool $fetchRecursive = false): static|bool|null {
|
||||
$handler = self::getHandler($sql);
|
||||
if ($fetchEntities) {
|
||||
return DatabaseEntityQuery::fetchOne(self::getHandler($sql))
|
||||
->where(new Compare($handler->getTableName() . ".id", $id))
|
||||
->fetchEntities($fetchRecursive)
|
||||
->execute();
|
||||
} else {
|
||||
return $handler->fetchOne($id);
|
||||
}
|
||||
}
|
||||
|
||||
public static function exists(SQL $sql, int $id): bool {
|
||||
$handler = self::getHandler($sql);
|
||||
$res = $sql->select($sql->count())
|
||||
->from($handler->getTableName())
|
||||
->where(new Compare($handler->getTableName() . ".id", $id))
|
||||
->execute();
|
||||
|
||||
return $res !== false && $res[0]["count"] !== 0;
|
||||
}
|
||||
|
||||
public static function findBuilder(SQL $sql): DatabaseEntityQuery {
|
||||
return DatabaseEntityQuery::fetchOne(self::getHandler($sql));
|
||||
}
|
||||
|
||||
public static function findAll(SQL $sql, ?Condition $condition = null): ?array {
|
||||
$handler = self::getHandler($sql);
|
||||
return $handler->fetchMultiple($condition);
|
||||
}
|
||||
|
||||
public static function findAllBuilder(SQL $sql): DatabaseEntityQuery {
|
||||
return DatabaseEntityQuery::fetchAll(self::getHandler($sql));
|
||||
}
|
||||
|
||||
public function save(SQL $sql, ?array $columns = null): bool {
|
||||
$handler = self::getHandler($sql);
|
||||
$res = $handler->insertOrUpdate($this, $columns);
|
||||
if ($res === false) {
|
||||
return false;
|
||||
} else if ($this->id === null) {
|
||||
$this->id = $res;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function insert(SQL $sql): bool {
|
||||
$handler = self::getHandler($sql);
|
||||
$res = $handler->insert($this);
|
||||
if ($res === false) {
|
||||
return false;
|
||||
} else if ($this->id === null) {
|
||||
$this->id = $res;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function delete(SQL $sql): bool {
|
||||
$handler = self::getHandler($sql);
|
||||
if ($this->id === null) {
|
||||
$handler->getLogger()->error("Cannot delete entity without id");
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($handler->delete($this->id)) {
|
||||
$this->id = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function getHandler(SQL $sql, $obj_or_class = null): DatabaseEntityHandler {
|
||||
|
||||
if (!$obj_or_class) {
|
||||
$obj_or_class = get_called_class();
|
||||
}
|
||||
|
||||
if (!($obj_or_class instanceof \ReflectionClass)) {
|
||||
$class = new \ReflectionClass($obj_or_class);
|
||||
} else {
|
||||
$class = $obj_or_class;
|
||||
}
|
||||
|
||||
$handler = self::$handlers[$class->getShortName()] ?? null;
|
||||
if (!$handler) {
|
||||
$handler = new DatabaseEntityHandler($sql, $class);
|
||||
self::$handlers[$class->getShortName()] = $handler;
|
||||
}
|
||||
|
||||
return $handler;
|
||||
}
|
||||
|
||||
public function getId(): ?int {
|
||||
return $this->id;
|
||||
}
|
||||
}
|
||||
414
Core/Objects/DatabaseEntity/DatabaseEntityHandler.php
Normal file
414
Core/Objects/DatabaseEntity/DatabaseEntityHandler.php
Normal file
@@ -0,0 +1,414 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\DatabaseEntity;
|
||||
|
||||
use Core\Driver\Logger\Logger;
|
||||
use Core\Driver\SQL\Column\BoolColumn;
|
||||
use Core\Driver\SQL\Column\DateTimeColumn;
|
||||
use Core\Driver\SQL\Column\EnumColumn;
|
||||
use Core\Driver\SQL\Column\IntColumn;
|
||||
use Core\Driver\SQL\Column\JsonColumn;
|
||||
use Core\Driver\SQL\Column\StringColumn;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Condition\Condition;
|
||||
use Core\Driver\SQL\Column\DoubleColumn;
|
||||
use Core\Driver\SQL\Column\FloatColumn;
|
||||
use Core\Driver\SQL\Constraint\ForeignKey;
|
||||
use Core\Driver\SQL\Query\CreateTable;
|
||||
use Core\Driver\SQL\Query\Select;
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Core\Driver\SQL\Strategy\CascadeStrategy;
|
||||
use Core\Driver\SQL\Strategy\SetNullStrategy;
|
||||
use Core\Objects\DatabaseEntity\Attribute\Enum;
|
||||
use Core\Objects\DatabaseEntity\Attribute\DefaultValue;
|
||||
use Core\Objects\DatabaseEntity\Attribute\Json;
|
||||
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
|
||||
use Core\Objects\DatabaseEntity\Attribute\Transient;
|
||||
use Core\Objects\DatabaseEntity\Attribute\Unique;
|
||||
use PHPUnit\Util\Exception;
|
||||
|
||||
class DatabaseEntityHandler {
|
||||
|
||||
private \ReflectionClass $entityClass;
|
||||
private string $tableName;
|
||||
private array $columns;
|
||||
private array $properties;
|
||||
private array $relations;
|
||||
private array $constraints;
|
||||
private SQL $sql;
|
||||
private Logger $logger;
|
||||
|
||||
public function __construct(SQL $sql, \ReflectionClass $entityClass) {
|
||||
$this->sql = $sql;
|
||||
$className = $entityClass->getName();
|
||||
$this->logger = new Logger($entityClass->getShortName(), $sql);
|
||||
$this->entityClass = $entityClass;
|
||||
if (!$this->entityClass->isSubclassOf(DatabaseEntity::class)) {
|
||||
$this->raiseError("Cannot persist class '$className': Not an instance of DatabaseEntity or not instantiable.");
|
||||
}
|
||||
|
||||
$this->tableName = $this->entityClass->getShortName();
|
||||
$this->columns = []; // property name => database column name
|
||||
$this->properties = []; // property name => \ReflectionProperty
|
||||
$this->relations = []; // property name => referenced table name
|
||||
$this->constraints = []; // \Driver\SQL\Constraint\Constraint
|
||||
|
||||
foreach ($this->entityClass->getProperties() as $property) {
|
||||
$propertyName = $property->getName();
|
||||
if ($propertyName === "id") {
|
||||
$this->properties[$propertyName] = $property;
|
||||
continue;
|
||||
}
|
||||
|
||||
$propertyType = $property->getType();
|
||||
$columnName = self::getColumnName($propertyName);
|
||||
if (!($propertyType instanceof \ReflectionNamedType)) {
|
||||
$this->raiseError("Cannot persist class '$className': Property '$propertyName' has no valid type");
|
||||
}
|
||||
|
||||
$nullable = $propertyType->allowsNull();
|
||||
$propertyTypeName = $propertyType->getName();
|
||||
if (!empty($property->getAttributes(Transient::class))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$defaultValue = (self::getAttribute($property, DefaultValue::class))?->getValue();
|
||||
$isUnique = !empty($property->getAttributes(Unique::class));
|
||||
|
||||
if ($propertyTypeName === 'string') {
|
||||
$enum = self::getAttribute($property, Enum::class);
|
||||
if ($enum) {
|
||||
$this->columns[$propertyName] = new EnumColumn($columnName, $enum->getValues(), $nullable, $defaultValue);
|
||||
} else {
|
||||
$maxLength = self::getAttribute($property, MaxLength::class);
|
||||
$this->columns[$propertyName] = new StringColumn($columnName, $maxLength?->getValue(), $nullable, $defaultValue);
|
||||
}
|
||||
} else if ($propertyTypeName === 'int') {
|
||||
$this->columns[$propertyName] = new IntColumn($columnName, $nullable, $defaultValue);
|
||||
} else if ($propertyTypeName === 'float') {
|
||||
$this->columns[$propertyName] = new FloatColumn($columnName, $nullable, $defaultValue);
|
||||
} else if ($propertyTypeName === 'double') {
|
||||
$this->columns[$propertyName] = new DoubleColumn($columnName, $nullable, $defaultValue);
|
||||
} else if ($propertyTypeName === 'bool') {
|
||||
$this->columns[$propertyName] = new BoolColumn($columnName, $defaultValue ?? false);
|
||||
} else if ($propertyTypeName === 'DateTime') {
|
||||
$this->columns[$propertyName] = new DateTimeColumn($columnName, $nullable, $defaultValue);
|
||||
/*} else if ($propertyName === 'array') {
|
||||
$many = self::getAttribute($property, Many::class);
|
||||
if ($many) {
|
||||
$requestedType = $many->getValue();
|
||||
if (isClass($requestedType)) {
|
||||
$requestedClass = new \ReflectionClass($requestedType);
|
||||
} else {
|
||||
$this->raiseError("Cannot persist class '$className': Property '$propertyName' has non persist-able type: $requestedType");
|
||||
}
|
||||
} else {
|
||||
$this->raiseError("Cannot persist class '$className': Property '$propertyName' has non persist-able type: $propertyTypeName");
|
||||
}*/
|
||||
} else if ($propertyTypeName !== "mixed") {
|
||||
try {
|
||||
$requestedClass = new \ReflectionClass($propertyTypeName);
|
||||
if ($requestedClass->isSubclassOf(DatabaseEntity::class)) {
|
||||
$columnName .= "_id";
|
||||
$requestedHandler = ($requestedClass->getName() === $this->entityClass->getName()) ?
|
||||
$this : DatabaseEntity::getHandler($this->sql, $requestedClass);
|
||||
$strategy = $nullable ? new SetNullStrategy() : new CascadeStrategy();
|
||||
$this->columns[$propertyName] = new IntColumn($columnName, $nullable, $defaultValue);
|
||||
$this->constraints[] = new ForeignKey($columnName, $requestedHandler->tableName, "id", $strategy);
|
||||
$this->relations[$propertyName] = $requestedHandler;
|
||||
} else {
|
||||
$this->raiseError("Cannot persist class '$className': Property '$propertyName' has non persist-able type: $propertyTypeName");
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
$this->raiseError("Cannot persist class '$className' property '$propertyTypeName': " . $ex->getMessage());
|
||||
}
|
||||
} else {
|
||||
if (!empty($property->getAttributes(Json::class))) {
|
||||
$this->columns[$propertyName] = new JsonColumn($columnName, $nullable, $defaultValue);
|
||||
} else {
|
||||
$this->raiseError("Cannot persist class '$className': Property '$propertyName' has non persist-able type: $propertyTypeName");
|
||||
}
|
||||
}
|
||||
|
||||
$this->properties[$propertyName] = $property;
|
||||
|
||||
if ($isUnique) {
|
||||
$this->constraints[] = new \Core\Driver\SQL\Constraint\Unique($columnName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function getAttribute(\ReflectionProperty $property, string $attributeClass): ?object {
|
||||
$attributes = $property->getAttributes($attributeClass);
|
||||
$attribute = array_shift($attributes);
|
||||
return $attribute?->newInstance();
|
||||
}
|
||||
|
||||
public static function getColumnName(string $propertyName): string {
|
||||
// abcTestLOL => abc_test_lol
|
||||
return strtolower(preg_replace_callback("/([a-z])([A-Z]+)/", function ($m) {
|
||||
return $m[1] . "_" . strtolower($m[2]);
|
||||
}, $propertyName));
|
||||
}
|
||||
|
||||
public function getReflection(): \ReflectionClass {
|
||||
return $this->entityClass;
|
||||
}
|
||||
|
||||
public function getLogger(): Logger {
|
||||
return $this->logger;
|
||||
}
|
||||
|
||||
public function getTableName(): string {
|
||||
return $this->tableName;
|
||||
}
|
||||
|
||||
public function getRelations(): array {
|
||||
return $this->relations;
|
||||
}
|
||||
|
||||
public function getColumnNames(): array {
|
||||
$columns = ["$this->tableName.id"];
|
||||
foreach ($this->columns as $column) {
|
||||
$columns[] = $this->tableName . "." . $column->getName();
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
public function getColumns(): array {
|
||||
return $this->columns;
|
||||
}
|
||||
|
||||
public function dependsOn(): array {
|
||||
$foreignTables = array_map(function (DatabaseEntityHandler $relationHandler) {
|
||||
return $relationHandler->getTableName();
|
||||
}, $this->relations);
|
||||
return array_unique($foreignTables);
|
||||
}
|
||||
|
||||
public static function getPrefixedRow(array $row, string $prefix): array {
|
||||
$rel_row = [];
|
||||
foreach ($row as $relKey => $relValue) {
|
||||
if (startsWith($relKey, $prefix)) {
|
||||
$rel_row[substr($relKey, strlen($prefix))] = $relValue;
|
||||
}
|
||||
}
|
||||
return $rel_row;
|
||||
}
|
||||
|
||||
public function entityFromRow(array $row): ?DatabaseEntity {
|
||||
try {
|
||||
|
||||
$entity = call_user_func($this->entityClass->getName() . "::newInstance", $this->entityClass, $row);
|
||||
if (!($entity instanceof DatabaseEntity)) {
|
||||
$this->logger->error("Created Object is not of type DatabaseEntity");
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($this->columns as $propertyName => $column) {
|
||||
$columnName = $column->getName();
|
||||
if (array_key_exists($columnName, $row)) {
|
||||
$value = $row[$columnName];
|
||||
$property = $this->properties[$propertyName];
|
||||
|
||||
if ($column instanceof DateTimeColumn) {
|
||||
$value = new \DateTime($value);
|
||||
} else if ($column instanceof JsonColumn) {
|
||||
$value = json_decode($value);
|
||||
} else if (isset($this->relations[$propertyName])) {
|
||||
$relColumnPrefix = self::getColumnName($propertyName) . "_";
|
||||
if (array_key_exists($relColumnPrefix . "id", $row)) {
|
||||
$relId = $row[$relColumnPrefix . "id"];
|
||||
if ($relId !== null) {
|
||||
$relationHandler = $this->relations[$propertyName];
|
||||
$value = $relationHandler->entityFromRow(self::getPrefixedRow($row, $relColumnPrefix));
|
||||
} else if (!$column->notNull()) {
|
||||
$value = null;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$property->setAccessible(true);
|
||||
$property->setValue($entity, $value);
|
||||
}
|
||||
}
|
||||
|
||||
$this->properties["id"]->setAccessible(true);
|
||||
$this->properties["id"]->setValue($entity, $row["id"]);
|
||||
$entity->postFetch($this->sql, $row);
|
||||
return $entity;
|
||||
} catch (\Exception $exception) {
|
||||
$this->logger->error("Error creating entity from database row: " . $exception->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function getSelectQuery(): Select {
|
||||
return $this->sql->select(...$this->getColumnNames())
|
||||
->from($this->tableName);
|
||||
}
|
||||
|
||||
public function fetchOne(int $id): DatabaseEntity|bool|null {
|
||||
$res = $this->getSelectQuery()
|
||||
->where(new Compare($this->tableName . ".id", $id))
|
||||
->first()
|
||||
->execute();
|
||||
|
||||
if ($res === false || $res === null) {
|
||||
return $res;
|
||||
} else {
|
||||
return $this->entityFromRow($res);
|
||||
}
|
||||
}
|
||||
|
||||
public function fetchMultiple(?Condition $condition = null): ?array {
|
||||
$query = $this->getSelectQuery();
|
||||
|
||||
if ($condition) {
|
||||
$query->where($condition);
|
||||
}
|
||||
|
||||
$res = $query->execute();
|
||||
if ($res === false) {
|
||||
return null;
|
||||
} else {
|
||||
$entities = [];
|
||||
foreach ($res as $row) {
|
||||
$entity = $this->entityFromRow($row);
|
||||
if ($entity) {
|
||||
$entities[$entity->getId()] = $entity;
|
||||
}
|
||||
}
|
||||
return $entities;
|
||||
}
|
||||
}
|
||||
|
||||
public function getTableQuery(): CreateTable {
|
||||
$query = $this->sql->createTable($this->tableName)
|
||||
->onlyIfNotExists()
|
||||
->addSerial("id")
|
||||
->primaryKey("id");
|
||||
|
||||
foreach ($this->columns as $column) {
|
||||
$query->addColumn($column);
|
||||
}
|
||||
|
||||
foreach ($this->constraints as $constraint) {
|
||||
$query->addConstraint($constraint);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function createTable(): bool {
|
||||
$query = $this->getTableQuery();
|
||||
return $query->execute();
|
||||
}
|
||||
|
||||
private function prepareRow(DatabaseEntity $entity, string $action, ?array $columns = null) {
|
||||
$row = [];
|
||||
foreach ($this->columns as $propertyName => $column) {
|
||||
if ($columns && !in_array($column->getName(), $columns)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$property = $this->properties[$propertyName];
|
||||
$property->setAccessible(true);
|
||||
if ($property->isInitialized($entity)) {
|
||||
$value = $property->getValue($entity);
|
||||
if (isset($this->relations[$propertyName])) {
|
||||
$value = $value->getId();
|
||||
}
|
||||
} else if (!$this->columns[$propertyName]->notNull()) {
|
||||
$value = null;
|
||||
} else {
|
||||
$defaultValue = self::getAttribute($property, DefaultValue::class);
|
||||
if ($defaultValue) {
|
||||
$value = $defaultValue->getValue();
|
||||
} else if ($action !== "update") {
|
||||
$this->logger->error("Cannot $action entity: property '$propertyName' was not initialized yet.");
|
||||
return false;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$row[$column->getName()] = $value;
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
public function update(DatabaseEntity $entity, ?array $columns = null) {
|
||||
$row = $this->prepareRow($entity, "update", $columns);
|
||||
if ($row === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$entity->preInsert($row);
|
||||
$query = $this->sql->update($this->tableName)
|
||||
->where(new Compare($this->tableName . ".id", $entity->getId()));
|
||||
|
||||
foreach ($row as $columnName => $value) {
|
||||
$query->set($columnName, $value);
|
||||
}
|
||||
|
||||
return $query->execute();
|
||||
}
|
||||
|
||||
public function insert(DatabaseEntity $entity) {
|
||||
$row = $this->prepareRow($entity, "insert");
|
||||
if ($row === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$entity->preInsert($row);
|
||||
|
||||
// insert with id?
|
||||
$entityId = $entity->getId();
|
||||
if ($entityId !== null) {
|
||||
$row["id"] = $entityId;
|
||||
}
|
||||
|
||||
$query = $this->sql->insert($this->tableName, array_keys($row))
|
||||
->addRow(...array_values($row));
|
||||
|
||||
// return id if its auto-generated
|
||||
if ($entityId === null) {
|
||||
$query->returning("id");
|
||||
}
|
||||
|
||||
$res = $query->execute();
|
||||
if ($res !== false) {
|
||||
return $this->sql->getLastInsertId();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function insertOrUpdate(DatabaseEntity $entity, ?array $columns = null) {
|
||||
$id = $entity->getId();
|
||||
if ($id === null) {
|
||||
return $this->insert($entity);
|
||||
} else {
|
||||
return $this->update($entity, $columns);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(int $id) {
|
||||
return $this->sql
|
||||
->delete($this->tableName)
|
||||
->where(new Compare($this->tableName . ".id", $id))
|
||||
->execute();
|
||||
}
|
||||
|
||||
private function raiseError(string $message) {
|
||||
$this->logger->error($message);
|
||||
throw new Exception($message);
|
||||
}
|
||||
}
|
||||
135
Core/Objects/DatabaseEntity/DatabaseEntityQuery.class.php
Normal file
135
Core/Objects/DatabaseEntity/DatabaseEntityQuery.class.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\DatabaseEntity;
|
||||
|
||||
use Core\Driver\SQL\Condition\Condition;
|
||||
use Core\Driver\SQL\Join;
|
||||
use Core\Driver\SQL\Query\Select;
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
/**
|
||||
* this class is similar to \Driver\SQL\Query\Select but with reduced functionality
|
||||
* and more adapted to entities.
|
||||
*/
|
||||
class DatabaseEntityQuery {
|
||||
|
||||
private DatabaseEntityHandler $handler;
|
||||
private Select $selectQuery;
|
||||
private int $resultType;
|
||||
|
||||
private function __construct(DatabaseEntityHandler $handler, int $resultType) {
|
||||
$this->handler = $handler;
|
||||
$this->selectQuery = $handler->getSelectQuery();
|
||||
$this->resultType = $resultType;
|
||||
|
||||
if ($this->resultType === SQL::FETCH_ONE) {
|
||||
$this->selectQuery->first();
|
||||
}
|
||||
}
|
||||
|
||||
public static function fetchAll(DatabaseEntityHandler $handler): DatabaseEntityQuery {
|
||||
return new DatabaseEntityQuery($handler, SQL::FETCH_ALL);
|
||||
}
|
||||
|
||||
public static function fetchOne(DatabaseEntityHandler $handler): DatabaseEntityQuery {
|
||||
return new DatabaseEntityQuery($handler, SQL::FETCH_ONE);
|
||||
}
|
||||
|
||||
public function limit(int $limit): DatabaseEntityQuery {
|
||||
$this->selectQuery->limit($limit);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function where(Condition ...$condition): DatabaseEntityQuery {
|
||||
$this->selectQuery->where(...$condition);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function orderBy(string ...$column): DatabaseEntityQuery {
|
||||
$this->selectQuery->orderBy(...$column);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function ascending(): DatabaseEntityQuery {
|
||||
$this->selectQuery->ascending();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function descending(): DatabaseEntityQuery {
|
||||
$this->selectQuery->descending();
|
||||
return $this;
|
||||
}
|
||||
|
||||
// TODO: clean this up
|
||||
public function fetchEntities(bool $recursive = false): DatabaseEntityQuery {
|
||||
|
||||
// $this->selectQuery->dump();
|
||||
|
||||
$relIndex = 1;
|
||||
foreach ($this->handler->getRelations() as $propertyName => $relationHandler) {
|
||||
$this->fetchRelation($propertyName, $this->handler->getTableName(), $this->handler, $relationHandler, $relIndex, $recursive);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function fetchRelation(string $propertyName, string $tableName, DatabaseEntityHandler $src, DatabaseEntityHandler $relationHandler,
|
||||
int &$relIndex = 1, bool $recursive = false, string $relationColumnPrefix = "") {
|
||||
|
||||
$columns = $src->getColumns();
|
||||
|
||||
$foreignColumn = $columns[$propertyName];
|
||||
$foreignColumnName = $foreignColumn->getName();
|
||||
$referencedTable = $relationHandler->getTableName();
|
||||
$isNullable = !$foreignColumn->notNull();
|
||||
$alias = "t$relIndex"; // t1, t2, t3, ...
|
||||
$relIndex++;
|
||||
|
||||
|
||||
if ($isNullable) {
|
||||
$this->selectQuery->leftJoin($referencedTable, "$tableName.$foreignColumnName", "$alias.id", $alias);
|
||||
} else {
|
||||
$this->selectQuery->innerJoin($referencedTable, "$tableName.$foreignColumnName", "$alias.id", $alias);
|
||||
}
|
||||
|
||||
$relationColumnPrefix .= DatabaseEntityHandler::getColumnName($propertyName) . "_";
|
||||
$recursiveRelations = $relationHandler->getRelations();
|
||||
foreach ($relationHandler->getColumns() as $relPropertyName => $relColumn) {
|
||||
$relColumnName = $relColumn->getName();
|
||||
if (!isset($recursiveRelations[$relPropertyName]) || $recursive) {
|
||||
$this->selectQuery->addValue("$alias.$relColumnName as $relationColumnPrefix$relColumnName");
|
||||
if (isset($recursiveRelations[$relPropertyName]) && $recursive) {
|
||||
$this->fetchRelation($relPropertyName, $alias, $relationHandler, $recursiveRelations[$relPropertyName], $relIndex, $recursive, $relationColumnPrefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function execute(): DatabaseEntity|array|null {
|
||||
$res = $this->selectQuery->execute();
|
||||
if ($res === null || $res === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->resultType === SQL::FETCH_ALL) {
|
||||
$entities = [];
|
||||
foreach ($res as $row) {
|
||||
$entity = $this->handler->entityFromRow($row);
|
||||
if ($entity) {
|
||||
$entities[$entity->getId()] = $entity;
|
||||
}
|
||||
}
|
||||
return $entities;
|
||||
} else if ($this->resultType === SQL::FETCH_ONE) {
|
||||
return $this->handler->entityFromRow($res);
|
||||
} else {
|
||||
$this->handler->getLogger()->error("Invalid result type for query builder, must be FETCH_ALL or FETCH_ONE");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function addJoin(Join $join): DatabaseEntityQuery {
|
||||
$this->selectQuery->addJoin($join);
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
133
Core/Objects/DatabaseEntity/GpgKey.class.php
Normal file
133
Core/Objects/DatabaseEntity/GpgKey.class.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\DatabaseEntity;
|
||||
|
||||
use Core\Driver\SQL\Expression\CurrentTimeStamp;
|
||||
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
|
||||
use Core\Objects\DatabaseEntity\Attribute\DefaultValue;
|
||||
|
||||
class GpgKey extends DatabaseEntity {
|
||||
|
||||
const GPG2 = "/usr/bin/gpg2";
|
||||
|
||||
private bool $confirmed;
|
||||
#[MaxLength(64)] private string $fingerprint;
|
||||
#[MaxLength(64)] private string $algorithm;
|
||||
private \DateTime $expires;
|
||||
#[DefaultValue(CurrentTimeStamp::class)] private \DateTime $added;
|
||||
|
||||
public function __construct(int $id, bool $confirmed, string $fingerprint, string $algorithm, string $expires) {
|
||||
parent::__construct($id);
|
||||
$this->confirmed = $confirmed;
|
||||
$this->fingerprint = $fingerprint;
|
||||
$this->algorithm = $algorithm;
|
||||
$this->expires = new \DateTime($expires);
|
||||
}
|
||||
|
||||
public static function encrypt(string $body, string $gpgFingerprint): array {
|
||||
$gpgFingerprint = escapeshellarg($gpgFingerprint);
|
||||
$cmd = self::GPG2 . " --encrypt --output - --recipient $gpgFingerprint --trust-model always --batch --armor";
|
||||
list($out, $err) = self::proc_exec($cmd, $body, true);
|
||||
if ($out === null) {
|
||||
return createError("Error while communicating with GPG agent");
|
||||
} else if ($err) {
|
||||
return createError($err);
|
||||
} else {
|
||||
return ["success" => true, "data" => $out];
|
||||
}
|
||||
}
|
||||
|
||||
private static function proc_exec(string $cmd, ?string $stdin = null, bool $raw = false): ?array {
|
||||
$descriptorSpec = array(0 => ["pipe", "r"], 1 => ["pipe", "w"], 2 => ["pipe", "w"]);
|
||||
$process = proc_open($cmd, $descriptorSpec, $pipes);
|
||||
if (!is_resource($process)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($stdin) {
|
||||
fwrite($pipes[0], $stdin);
|
||||
fclose($pipes[0]);
|
||||
}
|
||||
|
||||
$out = stream_get_contents($pipes[1]);
|
||||
$err = stream_get_contents($pipes[2]);
|
||||
fclose($pipes[1]);
|
||||
fclose($pipes[2]);
|
||||
proc_close($process);
|
||||
return [($raw ? $out : trim($out)), $err];
|
||||
}
|
||||
|
||||
public static function getKeyInfo(string $key): array {
|
||||
list($out, $err) = self::proc_exec(self::GPG2 . " --show-key", $key);
|
||||
if ($out === null) {
|
||||
return createError("Error while communicating with GPG agent");
|
||||
}
|
||||
|
||||
if ($err) {
|
||||
return createError($err);
|
||||
}
|
||||
|
||||
$lines = explode("\n", $out);
|
||||
if (count($lines) > 4) {
|
||||
return createError("It seems like you have uploaded more than one GPG-Key");
|
||||
} else if (count($lines) !== 4 || !preg_match("/(\S+)\s+(\w+)\s+.*\[expires: ([0-9-]+)]/", $lines[0], $matches)) {
|
||||
return createError("Error parsing GPG output");
|
||||
}
|
||||
|
||||
$keyType = $matches[1];
|
||||
$keyAlg = $matches[2];
|
||||
$expires = \DateTime::createFromFormat("Y-m-d", $matches[3]);
|
||||
$fingerprint = trim($lines[1]);
|
||||
$keyData = ["type" => $keyType, "algorithm" => $keyAlg, "expires" => $expires, "fingerprint" => $fingerprint];
|
||||
return ["success" => true, "data" => $keyData];
|
||||
}
|
||||
|
||||
public static function importKey(string $key): array {
|
||||
list($out, $err) = self::proc_exec(self::GPG2 . " --import", $key);
|
||||
if ($out === null) {
|
||||
return createError("Error while communicating with GPG agent");
|
||||
}
|
||||
|
||||
if (preg_match("/gpg:\s+Total number processed:\s+(\d+)/", $err, $matches) && intval($matches[1]) > 0) {
|
||||
if ((preg_match("/.*\s+unchanged:\s+(\d+)/", $err, $matches) && intval($matches[1]) > 0) ||
|
||||
(preg_match("/.*\s+imported:\s+(\d+)/", $err, $matches) && intval($matches[1]) > 0)) {
|
||||
return ["success" => true];
|
||||
}
|
||||
}
|
||||
|
||||
return createError($err);
|
||||
}
|
||||
|
||||
public static function export($gpgFingerprint, bool $armored): array {
|
||||
$cmd = self::GPG2 . " --export ";
|
||||
if ($armored) {
|
||||
$cmd .= "--armor ";
|
||||
}
|
||||
$cmd .= escapeshellarg($gpgFingerprint);
|
||||
list($out, $err) = self::proc_exec($cmd);
|
||||
if ($err) {
|
||||
return createError($err);
|
||||
}
|
||||
|
||||
return ["success" => true, "data" => $out];
|
||||
}
|
||||
|
||||
public function isConfirmed(): bool {
|
||||
return $this->confirmed;
|
||||
}
|
||||
|
||||
public function getFingerprint(): string {
|
||||
return $this->fingerprint;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return [
|
||||
"id" => $this->getId(),
|
||||
"fingerprint" => $this->fingerprint,
|
||||
"algorithm" => $this->algorithm,
|
||||
"expires" => $this->expires->getTimestamp(),
|
||||
"added" => $this->added->getTimestamp(),
|
||||
"confirmed" => $this->confirmed
|
||||
];
|
||||
}
|
||||
}
|
||||
23
Core/Objects/DatabaseEntity/Group.class.php
Normal file
23
Core/Objects/DatabaseEntity/Group.class.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\DatabaseEntity;
|
||||
|
||||
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
|
||||
|
||||
class Group extends DatabaseEntity {
|
||||
|
||||
#[MaxLength(32)] public string $name;
|
||||
#[MaxLength(10)] public string $color;
|
||||
|
||||
public function __construct(?int $id = null) {
|
||||
parent::__construct($id);
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return [
|
||||
"id" => $this->getId(),
|
||||
"name" => $this->name,
|
||||
"color" => $this->color
|
||||
];
|
||||
}
|
||||
}
|
||||
108
Core/Objects/DatabaseEntity/Language.class.php
Normal file
108
Core/Objects/DatabaseEntity/Language.class.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\DatabaseEntity {
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
|
||||
use Core\Objects\DatabaseEntity\Attribute\Transient;
|
||||
use Core\Objects\lang\LanguageModule;
|
||||
|
||||
// TODO: language from cookie?
|
||||
class Language extends DatabaseEntity {
|
||||
|
||||
const LANG_CODE_PATTERN = "/^[a-zA-Z]{2}_[a-zA-Z]{2}$/";
|
||||
|
||||
#[MaxLength(5)] private string $code;
|
||||
#[MaxLength(32)] private string $name;
|
||||
|
||||
#[Transient] private array $modules = [];
|
||||
#[Transient] protected array $entries = [];
|
||||
|
||||
public function __construct(int $id, string $code, string $name) {
|
||||
parent::__construct($id);
|
||||
$this->code = $code;
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function getCode(): string {
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
public function getShortCode(): string {
|
||||
return substr($this->code, 0, 2);
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function loadModule(LanguageModule|string $module) {
|
||||
if (!is_object($module)) {
|
||||
$module = new $module();
|
||||
}
|
||||
|
||||
if (!in_array($module, $this->modules)) {
|
||||
$moduleEntries = $module->getEntries($this->code);
|
||||
$this->entries = array_merge($this->entries, $moduleEntries);
|
||||
$this->modules[] = $module;
|
||||
}
|
||||
}
|
||||
|
||||
public function translate(string $key): string {
|
||||
return $this->entries[$key] ?? $key;
|
||||
}
|
||||
|
||||
public function sendCookie(string $domain) {
|
||||
setcookie('lang', $this->code, 0, "/", $domain, false, false);
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return array(
|
||||
'id' => $this->getId(),
|
||||
'code' => $this->code,
|
||||
'shortCode' => explode("_", $this->code)[0],
|
||||
'name' => $this->name,
|
||||
);
|
||||
}
|
||||
|
||||
public function activate() {
|
||||
global $LANGUAGE;
|
||||
$LANGUAGE = $this;
|
||||
}
|
||||
|
||||
public static function DEFAULT_LANGUAGE(bool $fromCookie = true): Language {
|
||||
if ($fromCookie && isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
|
||||
$acceptLanguage = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
|
||||
$acceptedLanguages = explode(',', $acceptLanguage);
|
||||
foreach ($acceptedLanguages as $code) {
|
||||
if (strlen($code) == 2) {
|
||||
$code = $code . '_' . strtoupper($code);
|
||||
}
|
||||
|
||||
$code = str_replace("-", "_", $code);
|
||||
if (!preg_match(self::LANG_CODE_PATTERN, $code)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return new Language(0, $code, "");
|
||||
}
|
||||
}
|
||||
|
||||
return new Language(1, "en_US", "American English");
|
||||
}
|
||||
|
||||
public function getEntries(): array {
|
||||
return $this->entries;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
function L($key) {
|
||||
if (!array_key_exists('LANGUAGE', $GLOBALS))
|
||||
return $key;
|
||||
|
||||
global $LANGUAGE;
|
||||
return $LANGUAGE->translate($key);
|
||||
}
|
||||
}
|
||||
30
Core/Objects/DatabaseEntity/News.class.php
Normal file
30
Core/Objects/DatabaseEntity/News.class.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\DatabaseEntity;
|
||||
|
||||
use Core\API\Parameter\Parameter;
|
||||
use Core\Driver\SQL\Expression\CurrentTimeStamp;
|
||||
use Core\Objects\DatabaseEntity\Attribute\DefaultValue;
|
||||
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
|
||||
|
||||
class News extends DatabaseEntity {
|
||||
|
||||
public User $publishedBy;
|
||||
#[DefaultValue(CurrentTimeStamp::class)] private \DateTime $publishedAt;
|
||||
#[MaxLength(128)] public string $title;
|
||||
#[MaxLength(1024)] public string $text;
|
||||
|
||||
public function __construct(?int $id = null) {
|
||||
parent::__construct($id);
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return [
|
||||
"id" => $this->getId(),
|
||||
"publishedBy" => $this->publishedBy->jsonSerialize(),
|
||||
"publishedAt" => $this->publishedAt->format(Parameter::DATE_TIME_FORMAT),
|
||||
"title" => $this->title,
|
||||
"text" => $this->text
|
||||
];
|
||||
}
|
||||
}
|
||||
30
Core/Objects/DatabaseEntity/Notification.class.php
Normal file
30
Core/Objects/DatabaseEntity/Notification.class.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\DatabaseEntity;
|
||||
|
||||
use Core\API\Parameter\Parameter;
|
||||
use Core\Driver\SQL\Expression\CurrentTimeStamp;
|
||||
use Core\Objects\DatabaseEntity\Attribute\DefaultValue;
|
||||
use Core\Objects\DatabaseEntity\Attribute\Enum;
|
||||
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
|
||||
|
||||
class Notification extends DatabaseEntity {
|
||||
|
||||
#[Enum('default', 'message', 'warning')] private string $type;
|
||||
#[DefaultValue(CurrentTimeStamp::class)] private \DateTime $createdAt;
|
||||
#[MaxLength(32)] public string $title;
|
||||
#[MaxLength(256)] public string $message;
|
||||
|
||||
public function __construct(?int $id = null) {
|
||||
parent::__construct($id);
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return [
|
||||
"id" => $this->getId(),
|
||||
"createdAt" => $this->createdAt->format(Parameter::DATE_TIME_FORMAT),
|
||||
"title" => $this->title,
|
||||
"message" => $this->message
|
||||
];
|
||||
}
|
||||
}
|
||||
128
Core/Objects/DatabaseEntity/Session.class.php
Normal file
128
Core/Objects/DatabaseEntity/Session.class.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
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;
|
||||
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
|
||||
use Core\Objects\DatabaseEntity\Attribute\Transient;
|
||||
|
||||
class Session extends DatabaseEntity {
|
||||
|
||||
# in minutes
|
||||
const DURATION = 60 * 60 * 24 * 14;
|
||||
#[Transient] private Context $context;
|
||||
|
||||
private User $user;
|
||||
private DateTime $expires;
|
||||
#[MaxLength(45)] private string $ipAddress;
|
||||
#[DefaultValue(true)] private bool $active;
|
||||
#[MaxLength(64)] private ?string $os;
|
||||
#[MaxLength(64)] private ?string $browser;
|
||||
#[DefaultValue(true)] public bool $stayLoggedIn;
|
||||
#[MaxLength(16)] private string $csrfToken;
|
||||
#[Json] private mixed $data;
|
||||
|
||||
public function __construct(Context $context, User $user, ?string $csrfToken = null) {
|
||||
parent::__construct();
|
||||
$this->context = $context;
|
||||
$this->user = $user;
|
||||
$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) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$session->context = $context;
|
||||
return $session;
|
||||
}
|
||||
|
||||
public function getUser(): User {
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
private function updateMetaData() {
|
||||
$this->expires = (new \DateTime())->modify(sprintf("+%d minutes", Session::DURATION));
|
||||
$this->ipAddress = $this->context->isCLI() ? "127.0.0.1" : $_SERVER['REMOTE_ADDR'];
|
||||
try {
|
||||
$userAgent = @get_browser($_SERVER['HTTP_USER_AGENT'], true);
|
||||
$this->os = $userAgent['platform'] ?? "Unknown";
|
||||
$this->browser = $userAgent['parent'] ?? "Unknown";
|
||||
} catch (Exception $ex) {
|
||||
$this->os = "Unknown";
|
||||
$this->browser = "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
public function setData(array $data) {
|
||||
foreach ($data as $key => $value) {
|
||||
$_SESSION[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
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 sendCookie(string $domain) {
|
||||
$sessionCookie = $this->getCookie();
|
||||
$secure = strcmp(getProtocol(), "https") === 0;
|
||||
setcookie('session', $sessionCookie, $this->getExpiresTime(), "/", $domain, $secure, true);
|
||||
}
|
||||
|
||||
public function getExpiresTime(): int {
|
||||
return ($this->stayLoggedIn ? $this->expires->getTimestamp() : 0);
|
||||
}
|
||||
|
||||
public function getExpiresSeconds(): int {
|
||||
return ($this->stayLoggedIn ? $this->expires->getTimestamp() - time() : -1);
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return array(
|
||||
'id' => $this->getId(),
|
||||
'active' => $this->active,
|
||||
'expires' => $this->expires,
|
||||
'ipAddress' => $this->ipAddress,
|
||||
'os' => $this->os,
|
||||
'browser' => $this->browser,
|
||||
'csrf_token' => $this->csrfToken,
|
||||
'data' => $this->data,
|
||||
);
|
||||
}
|
||||
|
||||
public function destroy(): bool {
|
||||
session_destroy();
|
||||
$this->active = false;
|
||||
return $this->save($this->context->getSQL());
|
||||
}
|
||||
|
||||
public function update(): bool {
|
||||
$this->updateMetaData();
|
||||
|
||||
$this->expires = (new DateTime())->modify(sprintf("+%d second", Session::DURATION));
|
||||
$this->data = json_encode($_SESSION ?? []);
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
return $this->user->update($sql) &&
|
||||
$this->save($sql);
|
||||
}
|
||||
|
||||
public function getCsrfToken(): string {
|
||||
return $this->csrfToken;
|
||||
}
|
||||
}
|
||||
31
Core/Objects/DatabaseEntity/SystemLog.class.php
Normal file
31
Core/Objects/DatabaseEntity/SystemLog.class.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\DatabaseEntity;
|
||||
|
||||
use Core\API\Parameter\Parameter;
|
||||
use Core\Driver\SQL\Expression\CurrentTimeStamp;
|
||||
use Core\Objects\DatabaseEntity\Attribute\DefaultValue;
|
||||
use Core\Objects\DatabaseEntity\Attribute\Enum;
|
||||
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
|
||||
|
||||
class SystemLog extends DatabaseEntity {
|
||||
|
||||
#[DefaultValue(CurrentTimeStamp::class)] private \DateTime $timestamp;
|
||||
private string $message;
|
||||
#[MaxLength(64)] #[DefaultValue('global')] private string $module;
|
||||
#[Enum('debug','info','warning','error','severe')] private string $severity;
|
||||
|
||||
public function __construct(?int $id = null) {
|
||||
parent::__construct($id);
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return [
|
||||
"id" => $this->getId(),
|
||||
"timestamp" => $this->timestamp->format(Parameter::DATE_TIME_FORMAT),
|
||||
"message" => $this->message,
|
||||
"module" => $this->module,
|
||||
"severity" => $this->severity
|
||||
];
|
||||
}
|
||||
}
|
||||
78
Core/Objects/DatabaseEntity/TwoFactorToken.class.php
Normal file
78
Core/Objects/DatabaseEntity/TwoFactorToken.class.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\DatabaseEntity;
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Core\Objects\DatabaseEntity\Attribute\Enum;
|
||||
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
|
||||
use Core\Objects\TwoFactor\KeyBasedTwoFactorToken;
|
||||
use Core\Objects\TwoFactor\TimeBasedTwoFactorToken;
|
||||
|
||||
abstract class TwoFactorToken extends DatabaseEntity {
|
||||
|
||||
#[Enum('totp','fido')] private string $type;
|
||||
private bool $confirmed;
|
||||
private bool $authenticated;
|
||||
#[MaxLength(512)] private string $data;
|
||||
|
||||
public function __construct(string $type, ?int $id = null, bool $confirmed = false) {
|
||||
parent::__construct($id);
|
||||
$this->id = $id;
|
||||
$this->type = $type;
|
||||
$this->confirmed = $confirmed;
|
||||
$this->authenticated = $_SESSION["2faAuthenticated"] ?? false;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return [
|
||||
"id" => $this->getId(),
|
||||
"type" => $this->type,
|
||||
"confirmed" => $this->confirmed,
|
||||
"authenticated" => $this->authenticated,
|
||||
];
|
||||
}
|
||||
|
||||
public abstract function getData(): string;
|
||||
protected abstract function readData(string $data);
|
||||
|
||||
public function preInsert(array &$row) {
|
||||
$row["data"] = $this->getData();
|
||||
}
|
||||
|
||||
public function postFetch(SQL $sql, array $row) {
|
||||
parent::postFetch($sql, $row);
|
||||
$this->readData($row["data"]);
|
||||
}
|
||||
|
||||
public function authenticate() {
|
||||
$this->authenticated = true;
|
||||
$_SESSION["2faAuthenticated"] = true;
|
||||
}
|
||||
|
||||
public function getType(): string {
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function isConfirmed(): bool {
|
||||
return $this->confirmed;
|
||||
}
|
||||
|
||||
public function getId(): int {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public static function newInstance(\ReflectionClass $reflectionClass, array $row) {
|
||||
if ($row["type"] === TimeBasedTwoFactorToken::TYPE) {
|
||||
return (new \ReflectionClass(TimeBasedTwoFactorToken::class))->newInstanceWithoutConstructor();
|
||||
} else if ($row["type"] === KeyBasedTwoFactorToken::TYPE) {
|
||||
return (new \ReflectionClass(KeyBasedTwoFactorToken::class))->newInstanceWithoutConstructor();
|
||||
} else {
|
||||
// TODO: error message
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function isAuthenticated(): bool {
|
||||
return $this->authenticated;
|
||||
}
|
||||
}
|
||||
109
Core/Objects/DatabaseEntity/User.class.php
Normal file
109
Core/Objects/DatabaseEntity/User.class.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\DatabaseEntity;
|
||||
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Expression\CurrentTimeStamp;
|
||||
use Core\Driver\SQL\Join;
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Core\Objects\DatabaseEntity\Attribute\DefaultValue;
|
||||
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
|
||||
use Core\Objects\DatabaseEntity\Attribute\Transient;
|
||||
use Core\Objects\DatabaseEntity\Attribute\Unique;
|
||||
|
||||
class User extends DatabaseEntity {
|
||||
|
||||
#[MaxLength(32)] #[Unique] public string $name;
|
||||
#[MaxLength(128)] public string $password;
|
||||
#[MaxLength(64)] public string $fullName;
|
||||
#[MaxLength(64)] #[Unique] public ?string $email;
|
||||
#[MaxLength(64)] private ?string $profilePicture;
|
||||
private ?\DateTime $lastOnline;
|
||||
#[DefaultValue(CurrentTimeStamp::class)] public \DateTime $registeredAt;
|
||||
public bool $confirmed;
|
||||
#[DefaultValue(1)] public Language $language;
|
||||
private ?GpgKey $gpgKey;
|
||||
private ?TwoFactorToken $twoFactorToken;
|
||||
|
||||
#[Transient] private array $groups;
|
||||
|
||||
public function __construct(?int $id = null) {
|
||||
parent::__construct($id);
|
||||
$this->groups = [];
|
||||
}
|
||||
|
||||
public function postFetch(SQL $sql, array $row) {
|
||||
parent::postFetch($sql, $row);
|
||||
$this->groups = [];
|
||||
|
||||
$groups = Group::findAllBuilder($sql)
|
||||
->fetchEntities()
|
||||
->addJoin(new Join("INNER", "UserGroup", "UserGroup.group_id", "Group.id"))
|
||||
->where(new Compare("UserGroup.user_id", $this->id))
|
||||
->execute();
|
||||
|
||||
if ($groups) {
|
||||
$this->groups = $groups;
|
||||
}
|
||||
}
|
||||
|
||||
public function getUsername(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getFullName(): string {
|
||||
return $this->fullName;
|
||||
}
|
||||
|
||||
public function getEmail(): ?string {
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function getGroups(): array {
|
||||
return $this->groups;
|
||||
}
|
||||
|
||||
public function hasGroup(int $group): bool {
|
||||
return isset($this->groups[$group]);
|
||||
}
|
||||
|
||||
public function getGPG(): ?GpgKey {
|
||||
return $this->gpgKey;
|
||||
}
|
||||
|
||||
public function getTwoFactorToken(): ?TwoFactorToken {
|
||||
return $this->twoFactorToken;
|
||||
}
|
||||
|
||||
public function getProfilePicture(): ?string {
|
||||
return $this->profilePicture;
|
||||
}
|
||||
|
||||
public function __debugInfo(): array {
|
||||
return [
|
||||
'id' => $this->getId(),
|
||||
'username' => $this->name,
|
||||
'language' => $this->language->getName(),
|
||||
];
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return [
|
||||
'id' => $this->getId(),
|
||||
'name' => $this->name,
|
||||
'fullName' => $this->fullName,
|
||||
'profilePicture' => $this->profilePicture,
|
||||
'email' => $this->email,
|
||||
'groups' => $this->groups ?? null,
|
||||
'language' => (isset($this->language) ? $this->language->jsonSerialize() : null),
|
||||
'session' => (isset($this->session) ? $this->session->jsonSerialize() : null),
|
||||
"gpg" => (isset($this->gpgKey) ? $this->gpgKey->jsonSerialize() : null),
|
||||
"2fa" => (isset($this->twoFactorToken) ? $this->twoFactorToken->jsonSerialize() : null),
|
||||
];
|
||||
}
|
||||
|
||||
public function update(SQL $sql): bool {
|
||||
$this->lastOnline = new \DateTime();
|
||||
return $this->save($sql, ["last_online", "language_id"]);
|
||||
}
|
||||
}
|
||||
157
Core/Objects/Router/AbstractRoute.class.php
Normal file
157
Core/Objects/Router/AbstractRoute.class.php
Normal file
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\Router;
|
||||
|
||||
use Core\API\Parameter\Parameter;
|
||||
|
||||
abstract class AbstractRoute {
|
||||
|
||||
const PARAMETER_PATTERN = "/^{([^:]+)(:(.*?)(\?)?)?}$/";
|
||||
|
||||
private string $pattern;
|
||||
private bool $exact;
|
||||
|
||||
public function __construct(string $pattern, bool $exact = true) {
|
||||
$this->pattern = $pattern;
|
||||
$this->exact = $exact;
|
||||
}
|
||||
|
||||
private static function parseParamType(?string $type): ?int {
|
||||
if ($type === null || trim($type) === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
$type = strtolower(trim($type));
|
||||
if (in_array($type, ["int", "integer"])) {
|
||||
return Parameter::TYPE_INT;
|
||||
} else if (in_array($type, ["float", "double"])) {
|
||||
return Parameter::TYPE_FLOAT;
|
||||
} else if (in_array($type, ["bool", "boolean"])) {
|
||||
return Parameter::TYPE_BOOLEAN;
|
||||
} else {
|
||||
return Parameter::TYPE_STRING;
|
||||
}
|
||||
}
|
||||
|
||||
public function getPattern(): string {
|
||||
return $this->pattern;
|
||||
}
|
||||
|
||||
public abstract function call(Router $router, array $params): string;
|
||||
|
||||
protected function getArgs(): array {
|
||||
return [$this->pattern, $this->exact];
|
||||
}
|
||||
|
||||
public function getClass(): \ReflectionClass {
|
||||
return new \ReflectionClass($this);
|
||||
}
|
||||
|
||||
public function generateCache(): string {
|
||||
$reflection = $this->getClass();
|
||||
$className = $reflection->getShortName();
|
||||
$args = implode(", ", array_map(function ($arg) {
|
||||
return var_export($arg, true);
|
||||
}, $this->getArgs()));
|
||||
return "new $className($args)";
|
||||
}
|
||||
|
||||
public function match(string $url) {
|
||||
|
||||
# /test/{abc}/{param:?}/{xyz:int}/{aaa:int?}
|
||||
$patternParts = explode("/", Router::cleanURL($this->pattern, false));
|
||||
$countPattern = count($patternParts);
|
||||
$patternOffset = 0;
|
||||
|
||||
# /test/param/optional/123
|
||||
$urlParts = explode("/", Router::cleanURL($url));
|
||||
$countUrl = count($urlParts);
|
||||
$urlOffset = 0;
|
||||
|
||||
$params = [];
|
||||
for (; $patternOffset < $countPattern; $patternOffset++) {
|
||||
if (!preg_match(self::PARAMETER_PATTERN, $patternParts[$patternOffset], $match)) {
|
||||
|
||||
// not a parameter? check if it matches
|
||||
if ($urlOffset >= $countUrl || $urlParts[$urlOffset] !== $patternParts[$patternOffset]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$urlOffset++;
|
||||
} else {
|
||||
|
||||
// we got a parameter here
|
||||
$paramName = $match[1];
|
||||
if (isset($match[2])) {
|
||||
$paramType = self::parseParamType($match[3]) ?? Parameter::TYPE_MIXED;
|
||||
$paramOptional = !empty($match[4] ?? null);
|
||||
} else {
|
||||
$paramType = Parameter::TYPE_MIXED;
|
||||
$paramOptional = false;
|
||||
}
|
||||
|
||||
$parameter = new Parameter($paramName, $paramType, $paramOptional);
|
||||
if ($urlOffset >= $countUrl || $urlParts[$urlOffset] === "") {
|
||||
if ($parameter->optional) {
|
||||
$value = $urlParts[$urlOffset] ?? null;
|
||||
if ($value === null || $value === "") {
|
||||
$params[$paramName] = null;
|
||||
} else {
|
||||
if (!$parameter->parseParam($value)) {
|
||||
return false;
|
||||
} else {
|
||||
$params[$paramName] = $parameter->value;
|
||||
}
|
||||
}
|
||||
|
||||
if ($urlOffset < $countUrl) {
|
||||
$urlOffset++;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$value = $urlParts[$urlOffset];
|
||||
if (!$parameter->parseParam($value)) {
|
||||
return false;
|
||||
} else {
|
||||
$params[$paramName] = $parameter->value;
|
||||
$urlOffset++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($urlOffset !== $countUrl && $this->exact) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function getUrl(array $parameters = []): string {
|
||||
$patternParts = explode("/", Router::cleanURL($this->pattern, false));
|
||||
|
||||
foreach ($patternParts as $i => $part) {
|
||||
if (preg_match(self::PARAMETER_PATTERN, $part, $match)) {
|
||||
$paramName = $match[1];
|
||||
$patternParts[$i] = $parameters[$paramName] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
return "/" . implode("/", array_filter($patternParts));
|
||||
}
|
||||
|
||||
public function getParameterNames(): array {
|
||||
$parameterNames = [];
|
||||
$patternParts = explode("/", Router::cleanURL($this->pattern, false));
|
||||
|
||||
foreach ($patternParts as $part) {
|
||||
if (preg_match(self::PARAMETER_PATTERN, $part, $match)) {
|
||||
$parameterNames[] = $match[1];
|
||||
}
|
||||
}
|
||||
|
||||
return $parameterNames;
|
||||
}
|
||||
}
|
||||
82
Core/Objects/Router/ApiRoute.class.php
Normal file
82
Core/Objects/Router/ApiRoute.class.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\Router;
|
||||
|
||||
use Core\API\Request;
|
||||
use Core\Elements\TemplateDocument;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
|
||||
class ApiRoute extends AbstractRoute {
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct("/api/{endpoint:?}/{method:?}", false);
|
||||
}
|
||||
|
||||
private static function checkClass(string $className): bool {
|
||||
$classPath = getClassPath($className);
|
||||
return file_exists($classPath) && class_exists($className);
|
||||
}
|
||||
|
||||
public function call(Router $router, array $params): string {
|
||||
if (empty($params["endpoint"])) {
|
||||
header("Content-Type: text/html");
|
||||
$document = new TemplateDocument($router, "swagger.twig");
|
||||
return $document->load();
|
||||
} else if (!preg_match("/[a-zA-Z]+/", $params["endpoint"])) {
|
||||
http_response_code(400);
|
||||
$response = createError("Invalid Method");
|
||||
} else {
|
||||
$apiEndpoint = ucfirst($params["endpoint"]);
|
||||
$isNestedAPI = !empty($params["method"]);
|
||||
if ($isNestedAPI) {
|
||||
$apiMethod = ucfirst($params["method"]);
|
||||
$parentClass = "\\API\\${apiEndpoint}API";
|
||||
$apiClass = "\\API\\${apiEndpoint}\\${apiMethod}";
|
||||
} else {
|
||||
$apiClass = "\\API\\${apiEndpoint}";
|
||||
$parentClass = $apiClass;
|
||||
}
|
||||
|
||||
try {
|
||||
$classFound = False;
|
||||
|
||||
// first: check if the parent class exists, for example:
|
||||
// /stats => Stats.class.php
|
||||
// /mail/send => MailAPI.class.php
|
||||
foreach (["Site", "Core"] as $module) {
|
||||
if ($this->checkClass("\\$module$parentClass")) {
|
||||
if (!$isNestedAPI || class_exists("\\$module$apiClass")) {
|
||||
$classFound = true;
|
||||
$apiClass = "\\$module$apiClass";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($classFound) {
|
||||
$apiClass = new ReflectionClass($apiClass);
|
||||
if (!$apiClass->isSubclassOf(Request::class) || !$apiClass->isInstantiable()) {
|
||||
http_response_code(400);
|
||||
$response = createError("Invalid Method");
|
||||
} else {
|
||||
$request = $apiClass->newInstanceArgs(array($router->getContext(), true));
|
||||
$success = $request->execute();
|
||||
$response = $request->getResult();
|
||||
$response["success"] = $success;
|
||||
$response["msg"] = $request->getLastError();
|
||||
}
|
||||
} else {
|
||||
http_response_code(404);
|
||||
$response = createError("Not found");
|
||||
}
|
||||
} catch (ReflectionException $e) {
|
||||
http_response_code(500);
|
||||
$response = createError("Error instantiating class: $e");
|
||||
}
|
||||
}
|
||||
|
||||
header("Content-Type: application/json");
|
||||
return json_encode($response);
|
||||
}
|
||||
}
|
||||
91
Core/Objects/Router/DocumentRoute.class.php
Normal file
91
Core/Objects/Router/DocumentRoute.class.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\Router;
|
||||
|
||||
use Core\Elements\Document;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\Search\Searchable;
|
||||
use Core\Objects\Search\SearchQuery;
|
||||
use ReflectionException;
|
||||
|
||||
class DocumentRoute extends AbstractRoute {
|
||||
|
||||
use Searchable;
|
||||
|
||||
private string $className;
|
||||
private array $args;
|
||||
private ?\ReflectionClass $reflectionClass;
|
||||
|
||||
public function __construct(string $pattern, bool $exact, string $className, ...$args) {
|
||||
parent::__construct($pattern, $exact);
|
||||
$this->className = $className;
|
||||
$this->args = $args;
|
||||
$this->reflectionClass = null;
|
||||
}
|
||||
|
||||
private function loadClass(): bool {
|
||||
|
||||
if ($this->reflectionClass === null) {
|
||||
try {
|
||||
$file = getClassPath($this->className);
|
||||
if (file_exists($file)) {
|
||||
$this->reflectionClass = new \ReflectionClass($this->className);
|
||||
if ($this->reflectionClass->isSubclassOf(Document::class)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (ReflectionException $exception) {
|
||||
$this->reflectionClass = null;
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
$this->reflectionClass = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function match(string $url) {
|
||||
$match = parent::match($url);
|
||||
if ($match === false || !$this->loadClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $match;
|
||||
}
|
||||
|
||||
protected function getArgs(): array {
|
||||
return array_merge(parent::getArgs(), [$this->className], $this->args);
|
||||
}
|
||||
|
||||
public function call(Router $router, array $params): string {
|
||||
try {
|
||||
if (!$this->loadClass()) {
|
||||
return $router->returnStatusCode(500, [ "message" => "Error loading class: $this->className"]);
|
||||
}
|
||||
|
||||
$args = array_merge([$router], $this->args, $params);
|
||||
$document = $this->reflectionClass->newInstanceArgs($args);
|
||||
return $document->load($params);
|
||||
} catch (\ReflectionException $e) {
|
||||
return $router->returnStatusCode(500, [ "message" => "Error loading class $this->className: " . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function doSearch(Context $context, SearchQuery $query): array {
|
||||
try {
|
||||
if ($this->loadClass()) {
|
||||
$args = array_merge([$context->router], $this->args);
|
||||
$document = $this->reflectionClass->newInstanceArgs($args);
|
||||
if ($document->isSearchable()) {
|
||||
return $document->doSearch($query, $this);
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (\ReflectionException) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
14
Core/Objects/Router/EmptyRoute.class.php
Normal file
14
Core/Objects/Router/EmptyRoute.class.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\Router;
|
||||
|
||||
class EmptyRoute extends AbstractRoute {
|
||||
|
||||
public function __construct(string $pattern, bool $exact = true) {
|
||||
parent::__construct($pattern, $exact);
|
||||
}
|
||||
|
||||
public function call(Router $router, array $params): string {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
25
Core/Objects/Router/RedirectRoute.class.php
Normal file
25
Core/Objects/Router/RedirectRoute.class.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\Router;
|
||||
|
||||
class RedirectRoute extends AbstractRoute {
|
||||
|
||||
private string $destination;
|
||||
private int $code;
|
||||
|
||||
public function __construct(string $pattern, bool $exact, string $destination, int $code = 307) {
|
||||
parent::__construct($pattern, $exact);
|
||||
$this->destination = $destination;
|
||||
$this->code = $code;
|
||||
}
|
||||
|
||||
public function call(Router $router, array $params): string {
|
||||
header("Location: $this->destination");
|
||||
http_response_code($this->code);
|
||||
return "";
|
||||
}
|
||||
|
||||
protected function getArgs(): array {
|
||||
return array_merge(parent::getArgs(), [$this->destination, $this->code]);
|
||||
}
|
||||
}
|
||||
169
Core/Objects/Router/Router.class.php
Normal file
169
Core/Objects/Router/Router.class.php
Normal file
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\Router;
|
||||
|
||||
use Core\Driver\Logger\Logger;
|
||||
use Core\Objects\Context;
|
||||
|
||||
class Router {
|
||||
|
||||
private Context $context;
|
||||
private Logger $logger;
|
||||
private ?AbstractRoute $activeRoute;
|
||||
private ?string $requestedUri;
|
||||
protected array $routes;
|
||||
protected array $statusCodeRoutes;
|
||||
|
||||
public function __construct(Context $context) {
|
||||
$this->context = $context;
|
||||
$this->routes = [];
|
||||
$this->statusCodeRoutes = [];
|
||||
$this->activeRoute = null;
|
||||
$this->requestedUri = null;
|
||||
$this->context->router = $this;
|
||||
|
||||
$sql = $context->getSQL();
|
||||
if ($sql) {
|
||||
$this->addRoute(new ApiRoute());
|
||||
$this->logger = new Logger("Router", $sql);
|
||||
} else {
|
||||
$this->logger = new Logger("Router");
|
||||
}
|
||||
}
|
||||
|
||||
public function getActiveRoute(): ?AbstractRoute {
|
||||
return $this->activeRoute;
|
||||
}
|
||||
|
||||
public function getRequestedUri(): ?string {
|
||||
return $this->requestedUri;
|
||||
}
|
||||
|
||||
public function run(string $url): string {
|
||||
|
||||
// TODO: do we want a global try cache and return status page 500 on any error?
|
||||
$this->requestedUri = $url;
|
||||
|
||||
$url = strtok($url, "?");
|
||||
foreach ($this->routes as $route) {
|
||||
$pathParams = $route->match($url);
|
||||
if ($pathParams !== false) {
|
||||
$this->activeRoute = $route;
|
||||
return $route->call($this, $pathParams);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->returnStatusCode(404);
|
||||
}
|
||||
|
||||
public function returnStatusCode(int $code, array $params = []): string {
|
||||
http_response_code($code);
|
||||
$params["status_code"] = $code;
|
||||
$params["status_description"] = HTTP_STATUS_DESCRIPTIONS[$code] ?? "Unknown Error";
|
||||
$route = $this->statusCodeRoutes[strval($code)] ?? null;
|
||||
if ($route) {
|
||||
return $route->call($this, $params);
|
||||
} else {
|
||||
$req = new \Core\API\Template\Render($this->context);
|
||||
$res = $req->execute(["file" => "error_document.twig", "parameters" => $params]);
|
||||
if ($res) {
|
||||
return $req->getResult()["html"];
|
||||
} else {
|
||||
$description = htmlspecialchars($params["status_description"]);
|
||||
return "<b>$code - $description</b>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function addRoute(AbstractRoute $route) {
|
||||
if (preg_match("/^\/(\d+)$/", $route->getPattern(), $re)) {
|
||||
$this->statusCodeRoutes[$re[1]] = $route;
|
||||
}
|
||||
|
||||
$this->routes[] = $route;
|
||||
}
|
||||
|
||||
public function writeCache(string $file): bool {
|
||||
|
||||
$routes = "";
|
||||
$uses = [
|
||||
"Core\Objects\Context",
|
||||
"Core\Objects\Router\Router",
|
||||
];
|
||||
|
||||
foreach ($this->routes as $route) {
|
||||
// do not generate cache for static api route
|
||||
if ($route instanceof ApiRoute) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$class = $route->getClass();
|
||||
$constructor = $route->generateCache();
|
||||
$uses[] = $class->getName();
|
||||
$routes .= "\n \$this->addRoute($constructor);";
|
||||
}
|
||||
|
||||
$uses = implode("\n", array_map(function ($use) { return "use $use;"; }, array_unique($uses)));
|
||||
$date = (new \DateTime())->format("Y/m/d H:i:s");
|
||||
$code = "<?php
|
||||
|
||||
/**
|
||||
* DO NOT EDIT!
|
||||
* This file is automatically generated by the RoutesAPI on $date.
|
||||
*/
|
||||
|
||||
namespace Core\Cache;
|
||||
$uses
|
||||
|
||||
class RouterCache extends Router {
|
||||
|
||||
public function __construct(Context \$context) {
|
||||
parent::__construct(\$context);$routes
|
||||
}
|
||||
}
|
||||
";
|
||||
|
||||
if (@file_put_contents($file, $code) === false) {
|
||||
$this->logger->severe("Could not write Router cache file: $file");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getContext(): Context {
|
||||
return $this->context;
|
||||
}
|
||||
|
||||
public function getLogger(): Logger {
|
||||
return $this->logger;
|
||||
}
|
||||
|
||||
public static function cleanURL(string $url, bool $cleanGET = true): string {
|
||||
// strip GET parameters
|
||||
if ($cleanGET) {
|
||||
if (($index = strpos($url, "?")) !== false) {
|
||||
$url = substr($url, 0, $index);
|
||||
}
|
||||
}
|
||||
|
||||
// strip document reference part
|
||||
if (($index = strpos($url, "#")) !== false) {
|
||||
$url = substr($url, 0, $index);
|
||||
}
|
||||
|
||||
// strip leading slash
|
||||
return preg_replace("/^\/+/", "", $url);
|
||||
}
|
||||
|
||||
public function getRoutes(bool $includeStatusRoutes = false): array {
|
||||
|
||||
if (!$includeStatusRoutes && !empty($this->statusCodeRoutes)) {
|
||||
return array_filter($this->routes, function ($route) {
|
||||
return !in_array($route, $this->statusCodeRoutes);
|
||||
});
|
||||
}
|
||||
|
||||
return $this->routes;
|
||||
}
|
||||
}
|
||||
114
Core/Objects/Router/StaticFileRoute.class.php
Normal file
114
Core/Objects/Router/StaticFileRoute.class.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\Router;
|
||||
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\Search\Searchable;
|
||||
use Core\Objects\Search\SearchQuery;
|
||||
use Core\Objects\Search\SearchResult;
|
||||
|
||||
class StaticFileRoute extends AbstractRoute {
|
||||
|
||||
use Searchable;
|
||||
|
||||
private string $path;
|
||||
private int $code;
|
||||
|
||||
public function __construct(string $pattern, bool $exact, string $path, int $code = 200) {
|
||||
parent::__construct($pattern, $exact);
|
||||
$this->path = $path;
|
||||
$this->code = $code;
|
||||
}
|
||||
|
||||
public function call(Router $router, array $params): string {
|
||||
http_response_code($this->code);
|
||||
$this->serveStatic($this->getAbsolutePath(), $router);
|
||||
return "";
|
||||
}
|
||||
|
||||
protected function getArgs(): array {
|
||||
return array_merge(parent::getArgs(), [$this->path, $this->code]);
|
||||
}
|
||||
|
||||
public function getAbsolutePath(): string {
|
||||
return WEBROOT . DIRECTORY_SEPARATOR . $this->path;
|
||||
}
|
||||
|
||||
public static function serveStatic(string $path, ?Router $router = null) {
|
||||
if (!startsWith($path, WEBROOT . DIRECTORY_SEPARATOR)) {
|
||||
http_response_code(406);
|
||||
echo "<b>Access restricted, requested file outside web root:</b> " . htmlspecialchars($path);
|
||||
}
|
||||
|
||||
if (!file_exists($path) || !is_file($path) || !is_readable($path)) {
|
||||
http_response_code(500);
|
||||
echo "<b>Unable to read file:</b> " . htmlspecialchars($path);
|
||||
}
|
||||
|
||||
$pathInfo = pathinfo($path);
|
||||
if ($router !== null) {
|
||||
$ext = $pathInfo["extension"] ?? "";
|
||||
if (!$router->getContext()->getSettings()->isExtensionAllowed($ext)) {
|
||||
http_response_code(406);
|
||||
echo "<b>Access restricted:</b> Extension '" . htmlspecialchars($ext) . "' not allowed to serve.";
|
||||
}
|
||||
}
|
||||
|
||||
$size = filesize($path);
|
||||
$mimeType = mime_content_type($path);
|
||||
header("Content-Type: $mimeType");
|
||||
header("Content-Length: $size");
|
||||
header('Accept-Ranges: bytes');
|
||||
|
||||
if (strcasecmp($_SERVER["REQUEST_METHOD"], "HEAD") !== 0) {
|
||||
$handle = fopen($path, "rb");
|
||||
if ($handle === false) {
|
||||
http_response_code(500);
|
||||
echo "<b>Unable to read file:</b> " . htmlspecialchars($path);
|
||||
}
|
||||
|
||||
$offset = 0;
|
||||
$length = $size;
|
||||
|
||||
if (isset($_SERVER['HTTP_RANGE'])) {
|
||||
preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches);
|
||||
$offset = intval($matches[1]);
|
||||
$length = intval($matches[2]) - $offset;
|
||||
http_response_code(206);
|
||||
header('Content-Range: bytes ' . $offset . '-' . ($offset + $length) . '/' . $size);
|
||||
}
|
||||
|
||||
downloadFile($handle, $offset, $length);
|
||||
}
|
||||
}
|
||||
|
||||
public function doSearch(Context $context, SearchQuery $query): array {
|
||||
|
||||
$results = [];
|
||||
$path = $this->getAbsolutePath();
|
||||
if (is_file($path) && is_readable($path)) {
|
||||
$pathInfo = pathinfo($path);
|
||||
$extension = $pathInfo["extension"] ?? "";
|
||||
$fileName = $pathInfo["filename"] ?? "";
|
||||
if ($context->getSettings()->isExtensionAllowed($extension)) {
|
||||
$mimeType = mime_content_type($path);
|
||||
if (startsWith($mimeType, "text/")) {
|
||||
$document = @file_get_contents($path);
|
||||
if ($document) {
|
||||
if ($mimeType === "text/html") {
|
||||
$results = Searchable::searchHtml($document, $query);
|
||||
} else {
|
||||
$results = Searchable::searchText($document, $query);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$results = array_map(function ($res) use ($fileName) {
|
||||
return new SearchResult($this->getPattern(), $fileName, $res["text"]);
|
||||
}, $results);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
24
Core/Objects/Router/StaticRoute.class.php
Normal file
24
Core/Objects/Router/StaticRoute.class.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\Router;
|
||||
|
||||
class StaticRoute extends AbstractRoute {
|
||||
|
||||
private string $data;
|
||||
private int $code;
|
||||
|
||||
public function __construct(string $pattern, bool $exact, string $data, int $code = 200) {
|
||||
parent::__construct($pattern, $exact);
|
||||
$this->data = $data;
|
||||
$this->code = $code;
|
||||
}
|
||||
|
||||
public function call(Router $router, array $params): string {
|
||||
http_response_code($this->code);
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
protected function getArgs(): array {
|
||||
return array_merge(parent::getArgs(), [$this->data, $this->code]);
|
||||
}
|
||||
}
|
||||
19
Core/Objects/Search/SearchQuery.class.php
Normal file
19
Core/Objects/Search/SearchQuery.class.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\Search;
|
||||
|
||||
class SearchQuery {
|
||||
|
||||
private string $query;
|
||||
private array $parts;
|
||||
|
||||
public function __construct(string $query) {
|
||||
$this->query = $query;
|
||||
$this->parts = array_unique(array_filter(explode(" ", strtolower($query))));
|
||||
}
|
||||
|
||||
public function getQuery(): string {
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
}
|
||||
30
Core/Objects/Search/SearchResult.class.php
Normal file
30
Core/Objects/Search/SearchResult.class.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\Search;
|
||||
|
||||
use Core\Objects\ApiObject;
|
||||
|
||||
class SearchResult extends ApiObject {
|
||||
|
||||
private string $url;
|
||||
private string $title;
|
||||
private string $text;
|
||||
|
||||
public function __construct(string $url, string $title, string $text) {
|
||||
$this->url = $url;
|
||||
$this->title = $title;
|
||||
$this->text = $text;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return [
|
||||
"url" => $this->url,
|
||||
"title" => $this->title,
|
||||
"text" => $this->text
|
||||
];
|
||||
}
|
||||
|
||||
public function setUrl(string $url) {
|
||||
$this->url = $url;
|
||||
}
|
||||
}
|
||||
91
Core/Objects/Search/Searchable.trait.php
Normal file
91
Core/Objects/Search/Searchable.trait.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\Search;
|
||||
|
||||
use Html2Text\Html2Text;
|
||||
use Core\Objects\Context;
|
||||
use function PHPUnit\Framework\stringContains;
|
||||
|
||||
trait Searchable {
|
||||
|
||||
public static function searchArray(array $arr, SearchQuery $query): array {
|
||||
$results = [];
|
||||
foreach ($arr as $key => $value) {
|
||||
$results = array_merge($results, self::searchHtml($key, $query));
|
||||
if (is_array($value)) {
|
||||
$results = array_merge($results, self::searchArray($value, $query));
|
||||
} else {
|
||||
$results = array_merge($results, self::searchHtml(strval($value), $query));
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public abstract function doSearch(Context $context, SearchQuery $query): array;
|
||||
|
||||
public static function searchHtml(string $document, SearchQuery $query): array {
|
||||
if (stringContains($document, "<")) {
|
||||
$converter = new Html2Text($document);
|
||||
$text = trim($converter->getText());
|
||||
} else {
|
||||
$text = $document;
|
||||
}
|
||||
|
||||
$text = trim(preg_replace('!\s+!', ' ', $text));
|
||||
return self::searchText($text, $query);
|
||||
}
|
||||
|
||||
public static function searchText(string $content, SearchQuery $query): array {
|
||||
$offset = 0;
|
||||
$searchTerm = $query->getQuery();
|
||||
$stringLength = strlen($searchTerm);
|
||||
$contentLength = strlen($content);
|
||||
$lastPos = 0;
|
||||
|
||||
$results = [];
|
||||
do {
|
||||
$pos = stripos($content, $searchTerm, $offset);
|
||||
if ($pos !== false) {
|
||||
if ($lastPos === 0 || $pos > $lastPos + 192 + $stringLength) {
|
||||
$extract = self::viewExtract($content, $pos, $searchTerm);
|
||||
$results[] = array(
|
||||
"text" => $extract,
|
||||
"pos" => $pos,
|
||||
"lastPos" => $lastPos
|
||||
);
|
||||
$lastPos = $pos;
|
||||
}
|
||||
|
||||
$offset = $pos + $stringLength;
|
||||
}
|
||||
} while ($pos !== false && $offset < $contentLength);
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
private static function viewExtract(string $content, int $pos, $string): array|string|null {
|
||||
$length = strlen($string);
|
||||
$start = max(0, $pos - 32);
|
||||
$end = min(strlen($content) - 1, $pos + $length + 192);
|
||||
|
||||
if ($start > 0) {
|
||||
if (!ctype_space($content[$start - 1]) &&
|
||||
!ctype_space($content[$start])) {
|
||||
$start = $start + strpos(substr($content, $start, $end), ' ');
|
||||
}
|
||||
}
|
||||
|
||||
if ($end < strlen($content) - 1) {
|
||||
if (!ctype_space($content[$end + 1]) &&
|
||||
!ctype_space($content[$end])) {
|
||||
$end = $start + strrpos(substr($content, $start, $end - $start), ' ');
|
||||
}
|
||||
}
|
||||
|
||||
$extract = trim(substr($content, $start, $end - $start + 1));
|
||||
if ($start > 0) $extract = ".. " . $extract;
|
||||
if ($end < strlen($content) - 1) $extract .= " ..";
|
||||
return preg_replace("/" . preg_quote($string) . "(?=[^>]*(<|$))/i", "<span class=\"highlight\">\$0</span>", $extract);
|
||||
}
|
||||
}
|
||||
37
Core/Objects/TwoFactor/AttestationObject.class.php
Normal file
37
Core/Objects/TwoFactor/AttestationObject.class.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\TwoFactor;
|
||||
|
||||
use Core\Objects\ApiObject;
|
||||
|
||||
class AttestationObject extends ApiObject {
|
||||
|
||||
use Core\Objects\TwoFactor\CBORDecoder;
|
||||
|
||||
private string $format;
|
||||
private array $statement;
|
||||
private AuthenticationData $authData;
|
||||
|
||||
public function __construct(string $buffer) {
|
||||
$data = $this->decode($buffer)->getNormalizedData();
|
||||
$this->format = $data["fmt"];
|
||||
$this->statement = $data["attStmt"];
|
||||
$this->authData = new AuthenticationData($data["authData"]);
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return [
|
||||
"format" => $this->format,
|
||||
"statement" => [
|
||||
"sig" => base64_encode($this->statement["sig"] ?? ""),
|
||||
"x5c" => base64_encode(($this->statement["x5c"] ?? [""])[0]),
|
||||
],
|
||||
"authData" => $this->authData->jsonSerialize()
|
||||
];
|
||||
}
|
||||
|
||||
public function getAuthData(): AuthenticationData {
|
||||
return $this->authData;
|
||||
}
|
||||
|
||||
}
|
||||
79
Core/Objects/TwoFactor/AuthenticationData.class.php
Normal file
79
Core/Objects/TwoFactor/AuthenticationData.class.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\TwoFactor;
|
||||
|
||||
use Core\Objects\ApiObject;
|
||||
|
||||
class AuthenticationData extends ApiObject {
|
||||
|
||||
private string $rpIDHash;
|
||||
private int $flags;
|
||||
private int $counter;
|
||||
private string $aaguid;
|
||||
private string $credentialID;
|
||||
private PublicKey $publicKey;
|
||||
|
||||
public function __construct(string $buffer) {
|
||||
|
||||
if (strlen($buffer) < 32 + 1 + 4) {
|
||||
throw new \Exception("Invalid authentication data buffer size");
|
||||
}
|
||||
|
||||
$offset = 0;
|
||||
$this->rpIDHash = substr($buffer, $offset, 32); $offset += 32;
|
||||
$this->flags = ord($buffer[$offset]); $offset += 1;
|
||||
$this->counter = unpack("N", $buffer, $offset)[1]; $offset += 4;
|
||||
|
||||
if (strlen($buffer) >= $offset + 4 + 2) {
|
||||
$this->aaguid = substr($buffer, $offset, 16); $offset += 16;
|
||||
$credentialIdLength = unpack("n", $buffer, $offset)[1]; $offset += 2;
|
||||
$this->credentialID = substr($buffer, $offset, $credentialIdLength); $offset += $credentialIdLength;
|
||||
|
||||
$credentialData = substr($buffer, $offset);
|
||||
$this->publicKey = new PublicKey($credentialData);
|
||||
}
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return [
|
||||
"rpIDHash" => base64_encode($this->rpIDHash),
|
||||
"flags" => $this->flags,
|
||||
"counter" => $this->counter,
|
||||
"aaguid" => base64_encode($this->aaguid),
|
||||
"credentialID" => base64_encode($this->credentialID),
|
||||
"publicKey" => $this->publicKey->jsonSerialize()
|
||||
];
|
||||
}
|
||||
|
||||
public function getHash(): string {
|
||||
return $this->rpIDHash;
|
||||
}
|
||||
|
||||
public function verifyIntegrity(string $rp): bool {
|
||||
return $this->rpIDHash === hash("sha256", $rp, true);
|
||||
}
|
||||
|
||||
public function isUserPresent(): bool {
|
||||
return boolval($this->flags & (1 << 0));
|
||||
}
|
||||
|
||||
public function isUserVerified(): bool {
|
||||
return boolval($this->flags & (1 << 2));
|
||||
}
|
||||
|
||||
public function attestedCredentialData(): bool {
|
||||
return boolval($this->flags & (1 << 6));
|
||||
}
|
||||
|
||||
public function hasExtensionData(): bool {
|
||||
return boolval($this->flags & (1 << 7));
|
||||
}
|
||||
|
||||
public function getPublicKey(): PublicKey {
|
||||
return $this->publicKey;
|
||||
}
|
||||
|
||||
public function getCredentialID() {
|
||||
return $this->credentialID;
|
||||
}
|
||||
}
|
||||
16
Core/Objects/TwoFactor/CBORDecoder.trait.php
Normal file
16
Core/Objects/TwoFactor/CBORDecoder.trait.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\TwoFactor;
|
||||
|
||||
use CBOR\StringStream;
|
||||
|
||||
trait CBORDecoder {
|
||||
|
||||
protected function decode(string $buffer): \CBOR\CBORObject {
|
||||
$objectManager = new \CBOR\OtherObject\OtherObjectManager();
|
||||
$tagManager = new \CBOR\Tag\TagObjectManager();
|
||||
$decoder = new \CBOR\Decoder($tagManager, $objectManager);
|
||||
return $decoder->decode(new StringStream($buffer));
|
||||
}
|
||||
|
||||
}
|
||||
75
Core/Objects/TwoFactor/KeyBasedTwoFactorToken.class.php
Normal file
75
Core/Objects/TwoFactor/KeyBasedTwoFactorToken.class.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\TwoFactor;
|
||||
|
||||
use Cose\Algorithm\Signature\ECDSA\ECSignature;
|
||||
use Core\Objects\DatabaseEntity\Attribute\Transient;
|
||||
use Core\Objects\DatabaseEntity\TwoFactorToken;
|
||||
|
||||
class KeyBasedTwoFactorToken extends TwoFactorToken {
|
||||
|
||||
const TYPE = "fido";
|
||||
|
||||
#[Transient] private ?string $challenge;
|
||||
#[Transient] private ?string $credentialId;
|
||||
#[Transient] private ?PublicKey $publicKey;
|
||||
|
||||
protected function readData(string $data) {
|
||||
if ($this->isConfirmed()) {
|
||||
$this->challenge = base64_decode($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->publicKey = PublicKey::fromJson($jsonData["publicKey"]);
|
||||
}
|
||||
}
|
||||
|
||||
public function getData(): string {
|
||||
return $this->challenge;
|
||||
}
|
||||
|
||||
public function getPublicKey(): ?PublicKey {
|
||||
return $this->publicKey;
|
||||
}
|
||||
|
||||
public function getCredentialId(): ?string {
|
||||
return $this->credentialId;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
$json = parent::jsonSerialize();
|
||||
|
||||
if (!empty($this->challenge) && !$this->isAuthenticated()) {
|
||||
$json["challenge"] = base64_encode($this->challenge);
|
||||
}
|
||||
|
||||
if (!empty($this->credentialId)) {
|
||||
$json["credentialID"] = base64_encode($this->credentialId);
|
||||
}
|
||||
|
||||
return $json;
|
||||
}
|
||||
|
||||
// TODO: algorithms, hardcoded values, ...
|
||||
public function verify(string $signature, string $data): bool {
|
||||
switch ($this->publicKey->getUsedAlgorithm()) {
|
||||
case -7: // EC2
|
||||
|
||||
if (strlen($signature) !== 64) {
|
||||
$signature = \Cose\Algorithm\Signature\ECDSA\ECSignature::fromAsn1($signature, 64);
|
||||
}
|
||||
|
||||
$coseKey = new \Cose\Key\Key($this->publicKey->getNormalizedData());
|
||||
$ec2key = new \Cose\Key\Ec2Key($coseKey->getData());
|
||||
$publicKey = $ec2key->toPublic();
|
||||
$signature = ECSignature::toAsn1($signature, 64);
|
||||
return openssl_verify($data, $signature, $publicKey->asPEM(), "sha256") === 1;
|
||||
default:
|
||||
// Not implemented :(
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
67
Core/Objects/TwoFactor/PublicKey.class.php
Normal file
67
Core/Objects/TwoFactor/PublicKey.class.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\TwoFactor;
|
||||
|
||||
use Core\Objects\ApiObject;
|
||||
|
||||
class PublicKey extends ApiObject {
|
||||
|
||||
use Core\Objects\TwoFactor\CBORDecoder;
|
||||
|
||||
private int $keyType;
|
||||
private int $usedAlgorithm;
|
||||
private int $curveType;
|
||||
private string $xCoordinate;
|
||||
private string $yCoordinate;
|
||||
|
||||
public function __construct(?string $cborData = null) {
|
||||
if ($cborData) {
|
||||
$data = $this->decode($cborData)->getNormalizedData();
|
||||
$this->keyType = $data["1"];
|
||||
$this->usedAlgorithm = $data["3"];
|
||||
$this->curveType = $data["-1"];
|
||||
$this->xCoordinate = $data["-2"];
|
||||
$this->yCoordinate = $data["-3"];
|
||||
}
|
||||
}
|
||||
|
||||
public static function fromJson($jsonData): PublicKey {
|
||||
$publicKey = new PublicKey(null);
|
||||
$publicKey->keyType = $jsonData["keyType"];
|
||||
$publicKey->usedAlgorithm = $jsonData["usedAlgorithm"];
|
||||
$publicKey->curveType = $jsonData["curveType"];
|
||||
$publicKey->xCoordinate = base64_decode($jsonData["coordinates"]["x"]);
|
||||
$publicKey->yCoordinate = base64_decode($jsonData["coordinates"]["y"]);
|
||||
return $publicKey;
|
||||
}
|
||||
|
||||
public function getUsedAlgorithm(): int {
|
||||
return $this->usedAlgorithm;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return [
|
||||
"keyType" => $this->keyType,
|
||||
"usedAlgorithm" => $this->usedAlgorithm,
|
||||
"curveType" => $this->curveType,
|
||||
"coordinates" => [
|
||||
"x" => base64_encode($this->xCoordinate),
|
||||
"y" => base64_encode($this->yCoordinate)
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getNormalizedData(): array {
|
||||
return [
|
||||
"1" => $this->keyType,
|
||||
"3" => $this->usedAlgorithm,
|
||||
"-1" => $this->curveType,
|
||||
"-2" => $this->xCoordinate,
|
||||
"-3" => $this->yCoordinate,
|
||||
];
|
||||
}
|
||||
|
||||
public function getU2F(): string {
|
||||
return bin2hex("\x04" . $this->xCoordinate . $this->yCoordinate);
|
||||
}
|
||||
}
|
||||
66
Core/Objects/TwoFactor/TimeBasedTwoFactorToken.class.php
Normal file
66
Core/Objects/TwoFactor/TimeBasedTwoFactorToken.class.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\TwoFactor;
|
||||
|
||||
use Base32\Base32;
|
||||
use chillerlan\QRCode\QRCode;
|
||||
use chillerlan\QRCode\QROptions;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\DatabaseEntity\Attribute\Transient;
|
||||
use Core\Objects\DatabaseEntity\TwoFactorToken;
|
||||
use Core\Objects\DatabaseEntity\User;
|
||||
|
||||
class TimeBasedTwoFactorToken extends TwoFactorToken {
|
||||
|
||||
const TYPE = "totp";
|
||||
#[Transient] private string $secret;
|
||||
|
||||
public function __construct(string $secret) {
|
||||
parent::__construct(self::TYPE);
|
||||
$this->secret = $secret;
|
||||
}
|
||||
|
||||
protected function readData(string $data) {
|
||||
$this->secret = $data;
|
||||
}
|
||||
|
||||
public function getUrl(Context $context): string {
|
||||
$otpType = self::TYPE;
|
||||
$name = rawurlencode($context->getUser()->getUsername());
|
||||
$settings = $context->getSettings();
|
||||
$urlArgs = [
|
||||
"secret" => $this->secret,
|
||||
"issuer" => $settings->getSiteName(),
|
||||
];
|
||||
|
||||
$urlArgs = http_build_query($urlArgs);
|
||||
return "otpauth://$otpType/$name?$urlArgs";
|
||||
}
|
||||
|
||||
public function generateQRCode(Context $context) {
|
||||
$options = new QROptions(['outputType' => QRCode::OUTPUT_IMAGE_PNG, "imageBase64" => false]);
|
||||
$qrcode = new QRCode($options);
|
||||
return $qrcode->render($this->getUrl($context));
|
||||
}
|
||||
|
||||
public function generate(?int $at = null, int $length = 6, int $period = 30): string {
|
||||
if ($at === null) {
|
||||
$at = time();
|
||||
}
|
||||
|
||||
$seed = intval($at / $period);
|
||||
$secret = Base32::decode($this->secret);
|
||||
$hmac = hash_hmac('sha1', pack("J", $seed), $secret, true);
|
||||
$offset = ord($hmac[-1]) & 0xF;
|
||||
$code = (unpack("N", substr($hmac, $offset, 4))[1] & 0x7fffffff) % intval(pow(10, $length));
|
||||
return substr(str_pad(strval($code), $length, "0", STR_PAD_LEFT), -1 * $length);
|
||||
}
|
||||
|
||||
public function verify(string $code): bool {
|
||||
return $this->generate() === $code;
|
||||
}
|
||||
|
||||
public function getData(): string {
|
||||
return $this->secret;
|
||||
}
|
||||
}
|
||||
8
Core/Objects/lang/LanguageModule.php
Normal file
8
Core/Objects/lang/LanguageModule.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\lang;
|
||||
|
||||
abstract class LanguageModule {
|
||||
|
||||
public abstract function getEntries(string $langCode);
|
||||
}
|
||||
Reference in New Issue
Block a user