v2.0-alpha

This commit is contained in:
Roman 2022-06-20 19:52:31 +02:00
parent b549af3166
commit ce647d4423
78 changed files with 2474 additions and 2083 deletions

@ -59,7 +59,7 @@ Each endpoint is represented by a class inheriting the [Request Class](/core/Api
```php ```php
namespace Api; namespace Api;
use Api\Parameter\Parameter; use Api\Parameter\Parameter;
use Objects\User; use Objects\DatabaseEntity\User;
class SingleEndpoint extends Request { class SingleEndpoint extends Request {
@ -108,7 +108,7 @@ Some endpoints are set to private, which means, they can be only accessed inside
can be used by creating the desired request object, and calling the execute function with our parameters like shown below: can be used by creating the desired request object, and calling the execute function with our parameters like shown below:
```php ```php
$req = new \Api\Mail\Send($user); $req = new \Api\Mail\Send($context);
$success = $req->execute(array( $success = $req->execute(array(
"to" => "mail@example.org", "to" => "mail@example.org",
"subject" => "Example Mail", "subject" => "Example Mail",

@ -92,7 +92,7 @@ export default class API {
} }
async deleteGroup(id) { async deleteGroup(id) {
return this.apiCall("groups/delete", { uid: id }); return this.apiCall("groups/delete", { id: id });
} }
async getSettings(key = "") { async getSettings(key = "") {

@ -25,10 +25,10 @@ export default function Header(props) {
let notificationItems = []; let notificationItems = [];
for (let i = 0; i < parent.notifications.length; i++) { for (let i = 0; i < parent.notifications.length; i++) {
const notification = parent.notifications[i]; const notification = parent.notifications[i];
const uid = notification.uid; const id = notification.id;
const createdAt = getPeriodString(notification["created_at"]); const createdAt = getPeriodString(notification["created_at"]);
notificationItems.push( notificationItems.push(
<Link to={"/admin/logs?notification=" + uid} className={"dropdown-item"} key={"notification-" + uid}> <Link to={"/admin/logs?notification=" + id} className={"dropdown-item"} key={"notification-" + id}>
{mailIcon} {mailIcon}
<span className={"ml-2"}>{notification.title}</span> <span className={"ml-2"}>{notification.title}</span>
<span className={"float-right text-muted text-sm"}>{createdAt}</span> <span className={"float-right text-muted text-sm"}>{createdAt}</span>

@ -83,7 +83,7 @@ export default class Logs extends React.Component {
for (let event of dates[date]) { for (let event of dates[date]) {
let timeString = moment(event.timestamp).fromNow(); let timeString = moment(event.timestamp).fromNow();
elements.push( elements.push(
<div key={"time-entry-" + event.uid}> <div key={"time-entry-" + event.id}>
<Icon icon={event.icon} className={"bg-" + color}/> <Icon icon={event.icon} className={"bg-" + color}/>
<div className="timeline-item"> <div className="timeline-item">
<span className="time"><Icon icon={"clock"}/> {timeString}</span> <span className="time"><Icon icon={"clock"}/> {timeString}</span>

@ -161,12 +161,12 @@ export default class UserOverview extends React.Component {
createUserCard() { createUserCard() {
let userRows = []; let userRows = [];
for (let uid in this.state.users.data) { for (let id in this.state.users.data) {
if (!this.state.users.data.hasOwnProperty(uid)) { if (!this.state.users.data.hasOwnProperty(id)) {
continue; continue;
} }
let user = this.state.users.data[uid]; let user = this.state.users.data[id];
let confirmedIcon = <Icon icon={user["confirmed"] ? "check" : "times"}/>; let confirmedIcon = <Icon icon={user["confirmed"] ? "check" : "times"}/>;
let groups = []; let groups = [];
@ -184,7 +184,7 @@ export default class UserOverview extends React.Component {
} }
userRows.push( userRows.push(
<tr key={"user-" + uid}> <tr key={"user-" + id}>
<td>{user.name}</td> <td>{user.name}</td>
<td>{user.email}</td> <td>{user.email}</td>
<td>{groups}</td> <td>{groups}</td>
@ -197,7 +197,7 @@ export default class UserOverview extends React.Component {
</td> </td>
<td className={"text-center"}>{confirmedIcon}</td> <td className={"text-center"}>{confirmedIcon}</td>
<td> <td>
<Link to={"/admin/user/edit/" + uid} className={"text-reset"}> <Link to={"/admin/user/edit/" + id} className={"text-reset"}>
<Icon icon={"pencil-alt"} data-effect={"solid"} <Icon icon={"pencil-alt"} data-effect={"solid"}
data-tip={"Modify user details & group membership"} data-tip={"Modify user details & group membership"}
data-type={"info"} data-place={"right"}/> data-type={"info"} data-place={"right"}/>
@ -291,15 +291,15 @@ export default class UserOverview extends React.Component {
createGroupCard() { createGroupCard() {
let groupRows = []; let groupRows = [];
for (let uid in this.state.groups.data) { for (let id in this.state.groups.data) {
if (!this.state.groups.data.hasOwnProperty(uid)) { if (!this.state.groups.data.hasOwnProperty(id)) {
continue; continue;
} }
let group = this.state.groups.data[uid]; let group = this.state.groups.data[id];
groupRows.push( groupRows.push(
<tr key={"group-" + uid}> <tr key={"group-" + id}>
<td>{group.name}</td> <td>{group.name}</td>
<td className={"text-center"}>{group["memberCount"]}</td> <td className={"text-center"}>{group["memberCount"]}</td>
<td> <td>
@ -309,7 +309,7 @@ export default class UserOverview extends React.Component {
</td> </td>
<td> <td>
<Icon icon={"trash"} style={{color: "red", cursor: "pointer"}} <Icon icon={"trash"} style={{color: "red", cursor: "pointer"}}
onClick={(e) => this.onDeleteGroup(e, uid)} data-effect={"solid"} onClick={(e) => this.onDeleteGroup(e, id)} data-effect={"solid"}
data-tip={"Delete"} data-type={"error"} data-tip={"Delete"} data-type={"error"}
data-place={"bottom"}/> data-place={"bottom"}/>
</td> </td>
@ -395,11 +395,11 @@ export default class UserOverview extends React.Component {
</div>; </div>;
} }
onDeleteGroup(e, uid) { onDeleteGroup(e, id) {
e.stopPropagation(); e.stopPropagation();
this.parent.showDialog("Are you really sure you want to delete this group?", "Delete Group?", ["Yes", "No"], (btn) => { this.parent.showDialog("Are you really sure you want to delete this group?", "Delete Group?", ["Yes", "No"], (btn) => {
if (btn === "Yes") { if (btn === "Yes") {
this.parent.api.deleteGroup(uid).then((res) => { this.parent.api.deleteGroup(id).then((res) => {
if (!res.success) { if (!res.success) {
let errors = this.state.errors.slice(); let errors = this.state.errors.slice();
errors.push({title: "Error deleting group", message: res.msg}); errors.push({title: "Error deleting group", message: res.msg});

103
cli.php

@ -6,14 +6,13 @@ include_once 'core/core.php';
require_once 'core/datetime.php'; require_once 'core/datetime.php';
include_once 'core/constants.php'; include_once 'core/constants.php';
use Configuration\Configuration;
use Configuration\DatabaseScript; use Configuration\DatabaseScript;
use Driver\SQL\Column\Column; use Driver\SQL\Column\Column;
use Driver\SQL\Condition\Compare; use Driver\SQL\Condition\Compare;
use Driver\SQL\Condition\CondIn; use Driver\SQL\Condition\CondIn;
use Driver\SQL\Expression\DateSub; use Driver\SQL\Expression\DateSub;
use Driver\SQL\SQL;
use Objects\ConnectionData; use Objects\ConnectionData;
use Objects\User;
function printLine(string $line = "") { function printLine(string $line = "") {
echo $line . PHP_EOL; echo $line . PHP_EOL;
@ -24,10 +23,6 @@ function _exit(string $line = "") {
die(); die();
} }
if (!is_cli()) {
_exit("Can only be executed via CLI");
}
function getDatabaseConfig(): ConnectionData { function getDatabaseConfig(): ConnectionData {
$configClass = "\\Configuration\\Database"; $configClass = "\\Configuration\\Database";
$file = getClassPath($configClass); $file = getClassPath($configClass);
@ -39,8 +34,12 @@ function getDatabaseConfig(): ConnectionData {
return new $configClass(); return new $configClass();
} }
$config = new Configuration(); $context = new \Objects\Context();
$database = $config->getDatabase(); if (!$context->isCLI()) {
_exit("Can only be executed via CLI");
}
$database = $context->getConfig()->getDatabase();
if ($database !== null && $database->getProperty("isDocker", false) && !is_file("/.dockerenv")) { if ($database !== null && $database->getProperty("isDocker", false) && !is_file("/.dockerenv")) {
if (count($argv) < 3 || $argv[1] !== "db" || !in_array($argv[2], ["shell", "import", "export"])) { if (count($argv) < 3 || $argv[1] !== "db" || !in_array($argv[2], ["shell", "import", "export"])) {
$command = array_merge(["docker", "exec", "-it", "php", "php"], $argv); $command = array_merge(["docker", "exec", "-it", "php", "php"], $argv);
@ -49,7 +48,7 @@ if ($database !== null && $database->getProperty("isDocker", false) && !is_file(
} }
} }
function getUser(): ?User { /*function getUser(): ?User {
global $config; global $config;
$user = new User($config); $user = new User($config);
if (!$user->getSQL() || !$user->getSQL()->isConnected()) { if (!$user->getSQL() || !$user->getSQL()->isConnected()) {
@ -58,6 +57,17 @@ function getUser(): ?User {
} }
return $user; return $user;
}*/
function connectSQL(): ?SQL {
global $context;
$sql = $context->initSQL();
if (!$sql || !$sql->isConnected()) {
printLine("Could not establish database connection");
return null;
}
return $sql;
} }
function printHelp() { function printHelp() {
@ -100,8 +110,7 @@ function handleDatabase(array $argv) {
_exit("Usage: cli.php db migrate <class name>"); _exit("Usage: cli.php db migrate <class name>");
} }
$user = getUser() or die(); $sql = connectSQL() or die();
$sql = $user->getSQL();
applyPatch($sql, $class); applyPatch($sql, $class);
} else if (in_array($action, ["export", "import", "shell"])) { } else if (in_array($action, ["export", "import", "shell"])) {
@ -193,9 +202,7 @@ function handleDatabase(array $argv) {
proc_close($process); proc_close($process);
} }
} else if ($action === "clean") { } else if ($action === "clean") {
$user = getUser() or die(); $sql = connectSQL() or die();
$sql = $user->getSQL();
printLine("Deleting user related data older than 90 days..."); printLine("Deleting user related data older than 90 days...");
// 1st: Select all related tables and entities // 1st: Select all related tables and entities
@ -221,9 +228,9 @@ function handleDatabase(array $argv) {
} }
// 2nd: delete! // 2nd: delete!
foreach ($tables as $table => $uids) { foreach ($tables as $table => $ids) {
$success = $sql->delete($table) $success = $sql->delete($table)
->where(new CondIn(new Column("uid"), $uids)) ->where(new CondIn(new Column("id"), $ids))
->execute(); ->execute();
if (!$success) { if (!$success) {
@ -336,9 +343,8 @@ function onMaintenance(array $argv) {
$newPatchFiles = array_diff($newPatchFiles, $oldPatchFiles); $newPatchFiles = array_diff($newPatchFiles, $oldPatchFiles);
if (count($newPatchFiles) > 0) { if (count($newPatchFiles) > 0) {
printLine("Applying new database patches"); printLine("Applying new database patches");
$user = getUser(); $sql = connectSQL();
if ($user) { if ($sql) {
$sql = $user->getSQL();
foreach ($newPatchFiles as $patchFile) { foreach ($newPatchFiles as $patchFile) {
if (preg_match("/core\/Configuration\/(Patch\/.*)\.class\.php/", $patchFile, $match)) { if (preg_match("/core\/Configuration\/(Patch\/.*)\.class\.php/", $patchFile, $match)) {
$patchName = $match[1]; $patchName = $match[1];
@ -408,12 +414,13 @@ function printTable(array $head, array $body) {
} }
function onSettings(array $argv) { function onSettings(array $argv) {
$user = getUser() or die(); global $context;
$sql = connectSQL() or die();
$action = $argv[2] ?? "list"; $action = $argv[2] ?? "list";
if ($action === "list" || $action === "get") { if ($action === "list" || $action === "get") {
$key = (($action === "list" || count($argv) < 4) ? null : $argv[3]); $key = (($action === "list" || count($argv) < 4) ? null : $argv[3]);
$req = new Api\Settings\Get($user); $req = new Api\Settings\Get($context);
$success = $req->execute(["key" => $key]); $success = $req->execute(["key" => $key]);
if (!$success) { if (!$success) {
_exit("Error listings settings: " . $req->getLastError()); _exit("Error listings settings: " . $req->getLastError());
@ -430,7 +437,7 @@ function onSettings(array $argv) {
} else { } else {
$key = $argv[3]; $key = $argv[3];
$value = $argv[4]; $value = $argv[4];
$req = new Api\Settings\Set($user); $req = new Api\Settings\Set($context);
$success = $req->execute(["settings" => [$key => $value]]); $success = $req->execute(["settings" => [$key => $value]]);
if (!$success) { if (!$success) {
_exit("Error updating settings: " . $req->getLastError()); _exit("Error updating settings: " . $req->getLastError());
@ -441,7 +448,7 @@ function onSettings(array $argv) {
_exit("Usage: $argv[0] settings $argv[2] <key>"); _exit("Usage: $argv[0] settings $argv[2] <key>");
} else { } else {
$key = $argv[3]; $key = $argv[3];
$req = new Api\Settings\Set($user); $req = new Api\Settings\Set($context);
$success = $req->execute(["settings" => [$key => null]]); $success = $req->execute(["settings" => [$key => null]]);
if (!$success) { if (!$success) {
_exit("Error updating settings: " . $req->getLastError()); _exit("Error updating settings: " . $req->getLastError());
@ -453,18 +460,18 @@ function onSettings(array $argv) {
} }
function onRoutes(array $argv) { function onRoutes(array $argv) {
global $context;
$user = getUser() or die(); $sql = connectSQL() or die();
$action = $argv[2] ?? "list"; $action = $argv[2] ?? "list";
if ($action === "list") { if ($action === "list") {
$req = new Api\Routes\Fetch($user); $req = new Api\Routes\Fetch($context);
$success = $req->execute(); $success = $req->execute();
if (!$success) { if (!$success) {
_exit("Error fetching routes: " . $req->getLastError()); _exit("Error fetching routes: " . $req->getLastError());
} else { } else {
$routes = $req->getResult()["routes"]; $routes = $req->getResult()["routes"];
$head = ["uid", "request", "action", "target", "extra", "active", "exact"]; $head = ["id", "request", "action", "target", "extra", "active", "exact"];
// strict boolean // strict boolean
foreach ($routes as &$route) { foreach ($routes as &$route) {
@ -486,7 +493,7 @@ function onRoutes(array $argv) {
"extra" => $argv[7] ?? "", "extra" => $argv[7] ?? "",
); );
$req = new Api\Routes\Add($user); $req = new Api\Routes\Add($context);
$success = $req->execute($params); $success = $req->execute($params);
if (!$success) { if (!$success) {
_exit($req->getLastError()); _exit($req->getLastError());
@ -497,13 +504,13 @@ function onRoutes(array $argv) {
$uid = $argv[3] ?? null; $uid = $argv[3] ?? null;
if ($uid === null || ($action === "modify" && count($argv) < 7)) { if ($uid === null || ($action === "modify" && count($argv) < 7)) {
if ($action === "modify") { if ($action === "modify") {
_exit("Usage: cli.php routes $action <uid> <request> <action> <target> [extra]"); _exit("Usage: cli.php routes $action <id> <request> <action> <target> [extra]");
} else { } else {
_exit("Usage: cli.php routes $action <uid>"); _exit("Usage: cli.php routes $action <id>");
} }
} }
$params = ["uid" => $uid]; $params = ["id" => $uid];
if ($action === "remove") { if ($action === "remove") {
$input = null; $input = null;
do { do {
@ -513,13 +520,13 @@ function onRoutes(array $argv) {
echo "Remove route #$uid? (y|n): "; echo "Remove route #$uid? (y|n): ";
} while(($input = trim(fgets(STDIN))) !== "y"); } while(($input = trim(fgets(STDIN))) !== "y");
$req = new Api\Routes\Remove($user); $req = new Api\Routes\Remove($context);
} else if ($action === "enable") { } else if ($action === "enable") {
$req = new Api\Routes\Enable($user); $req = new Api\Routes\Enable($context);
} else if ($action === "disable") { } else if ($action === "disable") {
$req = new Api\Routes\Disable($user); $req = new Api\Routes\Disable($context);
} else if ($action === "modify") { } else if ($action === "modify") {
$req = new Api\Routes\Update($user); $req = new Api\Routes\Update($context);
$params["request"] = $argv[4]; $params["request"] = $argv[4];
$params["action"] = $argv[5]; $params["action"] = $argv[5];
$params["target"] = $argv[6]; $params["target"] = $argv[6];
@ -597,14 +604,15 @@ function onTest($argv) {
} }
function onMail($argv) { function onMail($argv) {
global $context;
$action = $argv[2] ?? null; $action = $argv[2] ?? null;
if ($action === "sync") { if ($action === "sync") {
$user = getUser() or die(); $sql = connectSQL() or die();
if (!$user->getConfiguration()->getSettings()->isMailEnabled()) { if (!$context->getSettings()->isMailEnabled()) {
_exit("Mails are not configured yet."); _exit("Mails are not configured yet.");
} }
$req = new Api\Mail\Sync($user); $req = new Api\Mail\Sync($context);
printLine("Syncing emails…"); printLine("Syncing emails…");
if (!$req->execute()) { if (!$req->execute()) {
_exit("Error syncing mails: " . $req->getLastError()); _exit("Error syncing mails: " . $req->getLastError());
@ -612,8 +620,8 @@ function onMail($argv) {
_exit("Done."); _exit("Done.");
} else if ($action === "send_queue") { } else if ($action === "send_queue") {
$user = getUser() or die(); $sql = connectSQL() or die();
$req = new \Api\Mail\SendQueue($user); $req = new \Api\Mail\SendQueue($context);
$debug = in_array("debug", $argv); $debug = in_array("debug", $argv);
if (!$req->execute(["debug" => $debug])) { if (!$req->execute(["debug" => $debug])) {
_exit("Error processing mail queue: " . $req->getLastError()); _exit("Error processing mail queue: " . $req->getLastError());
@ -624,30 +632,31 @@ function onMail($argv) {
} }
function onImpersonate($argv) { function onImpersonate($argv) {
global $context;
if (count($argv) < 3) { if (count($argv) < 3) {
_exit("Usage: cli.php impersonate <user_id|user_name>"); _exit("Usage: cli.php impersonate <user_id|user_name>");
} }
$user = getUser() or exit; $sql = connectSQL() or die();
$userId = $argv[2]; $userId = $argv[2];
if (!is_numeric($userId)) { if (!is_numeric($userId)) {
$sql = $user->getSQL(); $res = $sql->select("id")
$res = $sql->select("uid")
->from("User") ->from("User")
->where(new Compare("name", $userId)) ->where(new Compare("name", $userId))
->execute(); ->execute();
if ($res === false) { if ($res === false) {
_exit("SQL error: " . $sql->getLastError()); _exit("SQL error: " . $sql->getLastError());
} else { } else {
$userId = $res[0]["uid"]; $userId = $res[0]["id"];
} }
} }
$user->createSession(intval($userId)); $user = new \Objects\DatabaseEntity\User($userId);
$session = $user->getSession(); $session = new \Objects\DatabaseEntity\Session($context, $user);
$session->setData(["2faAuthenticated" => true]); $session->setData(["2faAuthenticated" => true]);
$session->update(false); $session->update();
echo "session=" . $session->getCookie() . PHP_EOL; echo "session=" . $session->getCookie() . PHP_EOL;
} }

@ -3,15 +3,20 @@
namespace Api { namespace Api {
use Driver\SQL\Condition\Compare; use Driver\SQL\Condition\Compare;
use Objects\Context;
abstract class ApiKeyAPI extends Request { abstract class ApiKeyAPI extends Request {
protected function apiKeyExists($id): bool { public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
$sql = $this->user->getSQL(); parent::__construct($context, $externalCall, $params);
}
protected function apiKeyExists(int $id): bool {
$sql = $this->context->getSQL();
$res = $sql->select($sql->count()) $res = $sql->select($sql->count())
->from("ApiKey") ->from("ApiKey")
->where(new Compare("uid", $id)) ->where(new Compare("id", $id))
->where(new Compare("user_id", $this->user->getId())) ->where(new Compare("user_id", $this->context->getUser()->getId()))
->where(new Compare("valid_until", $sql->currentTimestamp(), ">")) ->where(new Compare("valid_until", $sql->currentTimestamp(), ">"))
->where(new Compare("active", 1)) ->where(new Compare("active", 1))
->execute(); ->execute();
@ -32,37 +37,32 @@ namespace Api\ApiKey {
use Api\ApiKeyAPI; use Api\ApiKeyAPI;
use Api\Parameter\Parameter; use Api\Parameter\Parameter;
use Api\Request;
use DateTime;
use Driver\SQL\Condition\Compare; use Driver\SQL\Condition\Compare;
use Exception; use Driver\SQL\Condition\CondAnd;
use Objects\Context;
use Objects\DatabaseEntity\ApiKey;
class Create extends ApiKeyAPI { class Create extends ApiKeyAPI {
public function __construct($user, $externalCall = false) { public function __construct(Context $context, $externalCall = false) {
parent::__construct($user, $externalCall, array()); parent::__construct($context, $externalCall, array());
$this->apiKeyAllowed = false; $this->apiKeyAllowed = false;
$this->loginRequired = true; $this->loginRequired = true;
} }
public function _execute(): bool { public function _execute(): bool {
$apiKey = generateRandomString(64); $sql = $this->context->getSQL();
$sql = $this->user->getSQL();
$validUntil = (new \DateTime())->modify("+30 DAY");
$this->success = $sql->insert("ApiKey", array("user_id", "api_key", "valid_until")) $apiKey = new ApiKey();
->addRow($this->user->getId(), $apiKey, $validUntil) $apiKey->apiKey = generateRandomString(64);
->returning("uid") $apiKey->validUntil = (new \DateTime())->modify("+30 DAY");
->execute(); $apiKey->user = $this->context->getUser();
$this->success = $apiKey->save($sql);
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
if ($this->success) { if ($this->success) {
$this->result["api_key"] = array( $this->result["api_key"] = $apiKey->jsonSerialize();
"api_key" => $apiKey,
"valid_until" => $validUntil->format("Y-m-d H:i:s"),
"uid" => $sql->getLastInsertId(),
);
} }
return $this->success; return $this->success;
@ -71,39 +71,33 @@ namespace Api\ApiKey {
class Fetch extends ApiKeyAPI { class Fetch extends ApiKeyAPI {
public function __construct($user, $externalCall = false) { public function __construct(Context $context, $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($context, $externalCall, array(
"showActiveOnly" => new Parameter("showActiveOnly", Parameter::TYPE_BOOLEAN, true, true) "showActiveOnly" => new Parameter("showActiveOnly", Parameter::TYPE_BOOLEAN, true, true)
)); ));
$this->loginRequired = true; $this->loginRequired = true;
} }
public function _execute(): bool { public function _execute(): bool {
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$query = $sql->select("uid", "api_key", "valid_until", "active")
->from("ApiKey")
->where(new Compare("user_id", $this->user->getId()));
$condition = new Compare("user_id", $this->context->getUser()->getId());
if ($this->getParam("showActiveOnly")) { if ($this->getParam("showActiveOnly")) {
$query->where(new Compare("valid_until", $sql->currentTimestamp(), ">")) $condition = new CondAnd(
->where(new Compare("active", true)); $condition,
new Compare("valid_until", $sql->currentTimestamp(), ">"),
new Compare("active", true)
);
} }
$res = $query->execute(); $apiKeys = ApiKey::findAll($sql, $condition);
$this->success = ($apiKeys !== FALSE);
$this->success = ($res !== FALSE);
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
if($this->success) { if ($this->success) {
$this->result["api_keys"] = array(); $this->result["api_keys"] = array();
foreach($res as $row) { foreach($apiKeys as $apiKey) {
$apiKeyId = intval($row["uid"]); $this->result["api_keys"][$apiKey->getId()] = $apiKey->jsonSerialize();
$this->result["api_keys"][$apiKeyId] = array(
"id" => $apiKeyId,
"api_key" => $row["api_key"],
"valid_until" => $row["valid_until"],
"revoked" => !$sql->parseBool($row["active"])
);
} }
} }
@ -113,8 +107,8 @@ namespace Api\ApiKey {
class Refresh extends ApiKeyAPI { class Refresh extends ApiKeyAPI {
public function __construct($user, $externalCall = false) { public function __construct(Context $context, $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($context, $externalCall, array(
"id" => new Parameter("id", Parameter::TYPE_INT), "id" => new Parameter("id", Parameter::TYPE_INT),
)); ));
$this->loginRequired = true; $this->loginRequired = true;
@ -122,15 +116,16 @@ namespace Api\ApiKey {
public function _execute(): bool { public function _execute(): bool {
$id = $this->getParam("id"); $id = $this->getParam("id");
if(!$this->apiKeyExists($id)) if (!$this->apiKeyExists($id)) {
return false; return false;
}
$validUntil = (new \DateTime)->modify("+30 DAY"); $validUntil = (new \DateTime)->modify("+30 DAY");
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$this->success = $sql->update("ApiKey") $this->success = $sql->update("ApiKey")
->set("valid_until", $validUntil) ->set("valid_until", $validUntil)
->where(new Compare("uid", $id)) ->where(new Compare("id", $id))
->where(new Compare("user_id", $this->user->getId())) ->where(new Compare("user_id", $this->context->getUser()->getId()))
->execute(); ->execute();
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
@ -153,20 +148,19 @@ namespace Api\ApiKey {
public function _execute(): bool { public function _execute(): bool {
$id = $this->getParam("id"); $id = $this->getParam("id");
if (!$this->apiKeyExists($id)) if (!$this->apiKeyExists($id)) {
return false; return false;
}
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$this->success = $sql->update("ApiKey") $this->success = $sql->update("ApiKey")
->set("active", false) ->set("active", false)
->where(new Compare("uid", $id)) ->where(new Compare("id", $id))
->where(new Compare("user_id", $this->user->getId())) ->where(new Compare("user_id", $this->context->getUser()->getId()))
->execute(); ->execute();
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
return $this->success; return $this->success;
} }
} }
} }

@ -2,21 +2,20 @@
namespace Api { namespace Api {
use Objects\User; use Objects\Context;
abstract class ContactAPI extends Request { abstract class ContactAPI extends Request {
protected ?string $messageId; protected ?string $messageId;
public function __construct(User $user, bool $externalCall, array $params) { public function __construct(Context $context, bool $externalCall, array $params) {
parent::__construct($user, $externalCall, $params); parent::__construct($context, $externalCall, $params);
$this->messageId = null; $this->messageId = null;
$this->csrfTokenRequired = false; $this->csrfTokenRequired = false;
} }
protected function sendMail(string $name, ?string $fromEmail, string $subject, string $message, ?string $to = null): bool { protected function sendMail(string $name, ?string $fromEmail, string $subject, string $message, ?string $to = null): bool {
$request = new \Api\Mail\Send($this->user); $request = new \Api\Mail\Send($this->context);
$this->success = $request->execute(array( $this->success = $request->execute(array(
"subject" => $subject, "subject" => $subject,
"body" => $message, "body" => $message,
@ -45,30 +44,30 @@ namespace Api\Contact {
use Driver\SQL\Condition\CondNot; use Driver\SQL\Condition\CondNot;
use Driver\SQL\Expression\CaseWhen; use Driver\SQL\Expression\CaseWhen;
use Driver\SQL\Expression\Sum; use Driver\SQL\Expression\Sum;
use Objects\User; use Objects\Context;
class Request extends ContactAPI { class Request extends ContactAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
$parameters = array( $parameters = array(
'fromName' => new StringType('fromName', 32), 'fromName' => new StringType('fromName', 32),
'fromEmail' => new Parameter('fromEmail', Parameter::TYPE_EMAIL), 'fromEmail' => new Parameter('fromEmail', Parameter::TYPE_EMAIL),
'message' => new StringType('message', 512), 'message' => new StringType('message', 512),
); );
$settings = $user->getConfiguration()->getSettings(); $settings = $context->getSettings();
if ($settings->isRecaptchaEnabled()) { if ($settings->isRecaptchaEnabled()) {
$parameters["captcha"] = new StringType("captcha"); $parameters["captcha"] = new StringType("captcha");
} }
parent::__construct($user, $externalCall, $parameters); parent::__construct($context, $externalCall, $parameters);
} }
public function _execute(): bool { public function _execute(): bool {
$settings = $this->user->getConfiguration()->getSettings(); $settings = $this->context->getSettings();
if ($settings->isRecaptchaEnabled()) { if ($settings->isRecaptchaEnabled()) {
$captcha = $this->getParam("captcha"); $captcha = $this->getParam("captcha");
$req = new VerifyCaptcha($this->user); $req = new VerifyCaptcha($this->context);
if (!$req->execute(array("captcha" => $captcha, "action" => "contact"))) { if (!$req->execute(array("captcha" => $captcha, "action" => "contact"))) {
return $this->createError($req->getLastError()); return $this->createError($req->getLastError());
} }
@ -80,23 +79,7 @@ namespace Api\Contact {
$email = $this->getParam("fromEmail"); $email = $this->getParam("fromEmail");
$sendMail = $this->sendMail($name, $email, "Contact Request", $message); $sendMail = $this->sendMail($name, $email, "Contact Request", $message);
$mailError = $this->getLastError();
$insertDB = $this->insertContactRequest(); $insertDB = $this->insertContactRequest();
$dbError = $this->getLastError();
// Create a log entry
if (!$sendMail || $mailError) {
$message = "Error processing contact request.";
if (!$sendMail) {
$message .= " Mail: $mailError";
}
if (!$insertDB) {
$message .= " Mail: $dbError";
}
}
if (!$sendMail && !$insertDB) { if (!$sendMail && !$insertDB) {
return $this->createError("The contact request could not be sent. The Administrator is already informed. Please try again later."); return $this->createError("The contact request could not be sent. The Administrator is already informed. Please try again later.");
} }
@ -105,7 +88,7 @@ namespace Api\Contact {
} }
private function insertContactRequest(): bool { private function insertContactRequest(): bool {
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$name = $this->getParam("fromName"); $name = $this->getParam("fromName");
$email = $this->getParam("fromEmail"); $email = $this->getParam("fromEmail");
$message = $this->getParam("message"); $message = $this->getParam("message");
@ -113,7 +96,7 @@ namespace Api\Contact {
$res = $sql->insert("ContactRequest", array("from_name", "from_email", "message", "messageId")) $res = $sql->insert("ContactRequest", array("from_name", "from_email", "message", "messageId"))
->addRow($name, $email, $message, $messageId) ->addRow($name, $email, $message, $messageId)
->returning("uid") ->returning("id")
->execute(); ->execute();
$this->success = ($res !== FALSE); $this->success = ($res !== FALSE);
@ -124,21 +107,20 @@ namespace Api\Contact {
class Respond extends ContactAPI { class Respond extends ContactAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($context, $externalCall, array(
"requestId" => new Parameter("requestId", Parameter::TYPE_INT), "requestId" => new Parameter("requestId", Parameter::TYPE_INT),
'message' => new StringType('message', 512), 'message' => new StringType('message', 512),
)); ));
$this->loginRequired = true; $this->loginRequired = true;
$this->csrfTokenRequired = false;
} }
private function getSenderMail(): ?string { private function getSenderMail(): ?string {
$requestId = $this->getParam("requestId"); $requestId = $this->getParam("requestId");
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$res = $sql->select("from_email") $res = $sql->select("from_email")
->from("ContactRequest") ->from("ContactRequest")
->where(new Compare("uid", $requestId)) ->where(new Compare("id", $requestId))
->execute(); ->execute();
$this->success = ($res !== false); $this->success = ($res !== false);
@ -156,12 +138,12 @@ namespace Api\Contact {
} }
private function insertResponseMessage(): bool { private function insertResponseMessage(): bool {
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$message = $this->getParam("message"); $message = $this->getParam("message");
$requestId = $this->getParam("requestId"); $requestId = $this->getParam("requestId");
$this->success = $sql->insert("ContactMessage", ["request_id", "user_id", "message", "messageId", "read"]) $this->success = $sql->insert("ContactMessage", ["request_id", "user_id", "message", "messageId", "read"])
->addRow($requestId, $this->user->getId(), $message, $this->messageId, true) ->addRow($requestId, $this->context->getUser()->getId(), $message, $this->messageId, true)
->execute(); ->execute();
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
@ -169,7 +151,7 @@ namespace Api\Contact {
} }
private function updateEntity() { private function updateEntity() {
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$requestId = $this->getParam("requestId"); $requestId = $this->getParam("requestId");
$sql->update("EntityLog") $sql->update("EntityLog")
@ -185,8 +167,9 @@ namespace Api\Contact {
return false; return false;
} }
$fromName = $this->user->getUsername(); $user = $this->context->getUser();
$fromEmail = $this->user->getEmail(); $fromName = $user->getUsername();
$fromEmail = $user->getEmail();
if (!$this->sendMail($fromName, $fromEmail, "Re: Contact Request", $message, $senderMail)) { if (!$this->sendMail($fromName, $fromEmail, "Re: Contact Request", $message, $senderMail)) {
return false; return false;
@ -203,19 +186,19 @@ namespace Api\Contact {
class Fetch extends ContactAPI { class Fetch extends ContactAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, array()); parent::__construct($context, $externalCall, array());
$this->loginRequired = true; $this->loginRequired = true;
$this->csrfTokenRequired = false; $this->csrfTokenRequired = false;
} }
public function _execute(): bool { public function _execute(): bool {
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$res = $sql->select("ContactRequest.uid", "from_name", "from_email", "from_name", $res = $sql->select("ContactRequest.id", "from_name", "from_email", "from_name",
new Sum(new CaseWhen(new CondNot("ContactMessage.read"), 1, 0), "unread")) new Sum(new CaseWhen(new CondNot("ContactMessage.read"), 1, 0), "unread"))
->from("ContactRequest") ->from("ContactRequest")
->groupBy("ContactRequest.uid") ->groupBy("ContactRequest.id")
->leftJoin("ContactMessage", "ContactRequest.uid", "ContactMessage.request_id") ->leftJoin("ContactMessage", "ContactRequest.id", "ContactMessage.request_id")
->execute(); ->execute();
$this->success = ($res !== false); $this->success = ($res !== false);
@ -225,7 +208,7 @@ namespace Api\Contact {
$this->result["contactRequests"] = []; $this->result["contactRequests"] = [];
foreach ($res as $row) { foreach ($res as $row) {
$this->result["contactRequests"][] = array( $this->result["contactRequests"][] = array(
"uid" => intval($row["uid"]), "id" => intval($row["id"]),
"from_name" => $row["from_name"], "from_name" => $row["from_name"],
"from_email" => $row["from_email"], "from_email" => $row["from_email"],
"unread" => intval($row["unread"]), "unread" => intval($row["unread"]),
@ -239,8 +222,8 @@ namespace Api\Contact {
class Get extends ContactAPI { class Get extends ContactAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($context, $externalCall, array(
"requestId" => new Parameter("requestId", Parameter::TYPE_INT), "requestId" => new Parameter("requestId", Parameter::TYPE_INT),
)); ));
$this->loginRequired = true; $this->loginRequired = true;
@ -249,7 +232,7 @@ namespace Api\Contact {
private function updateRead() { private function updateRead() {
$requestId = $this->getParam("requestId"); $requestId = $this->getParam("requestId");
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$sql->update("ContactMessage") $sql->update("ContactMessage")
->set("read", 1) ->set("read", 1)
->where(new Compare("request_id", $requestId)) ->where(new Compare("request_id", $requestId))
@ -258,11 +241,11 @@ namespace Api\Contact {
public function _execute(): bool { public function _execute(): bool {
$requestId = $this->getParam("requestId"); $requestId = $this->getParam("requestId");
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$res = $sql->select("from_name", "from_email", "message", "created_at") $res = $sql->select("from_name", "from_email", "message", "created_at")
->from("ContactRequest") ->from("ContactRequest")
->where(new Compare("uid", $requestId)) ->where(new Compare("id", $requestId))
->execute(); ->execute();
$this->success = ($res !== false); $this->success = ($res !== false);

@ -3,11 +3,16 @@
namespace Api { namespace Api {
use Driver\SQL\Condition\Compare; use Driver\SQL\Condition\Compare;
use Objects\Context;
abstract class GroupsAPI extends Request { abstract class GroupsAPI extends Request {
protected function groupExists($name) { public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
$sql = $this->user->getSQL(); parent::__construct($context, $externalCall, $params);
}
protected function groupExists($name): bool {
$sql = $this->context->getSQL();
$res = $sql->select($sql->count()) $res = $sql->select($sql->count())
->from("Group") ->from("Group")
->where(new Compare("name", $name)) ->where(new Compare("name", $name))
@ -25,14 +30,15 @@ namespace Api\Groups {
use Api\GroupsAPI; use Api\GroupsAPI;
use Api\Parameter\Parameter; use Api\Parameter\Parameter;
use Api\Parameter\StringType; use Api\Parameter\StringType;
use Driver\SQL\Condition\Compare; use Objects\Context;
use Objects\DatabaseEntity\Group;
class Fetch extends GroupsAPI { class Fetch extends GroupsAPI {
private int $groupCount; private int $groupCount;
public function __construct($user, $externalCall = false) { public function __construct(Context $context, $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($context, $externalCall, array(
'page' => new Parameter('page', Parameter::TYPE_INT, true, 1), 'page' => new Parameter('page', Parameter::TYPE_INT, true, 1),
'count' => new Parameter('count', Parameter::TYPE_INT, true, 20) 'count' => new Parameter('count', Parameter::TYPE_INT, true, 20)
)); ));
@ -40,9 +46,9 @@ namespace Api\Groups {
$this->groupCount = 0; $this->groupCount = 0;
} }
private function getGroupCount() { private function fetchGroupCount(): bool {
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$res = $sql->select($sql->count())->from("Group")->execute(); $res = $sql->select($sql->count())->from("Group")->execute();
$this->success = ($res !== FALSE); $this->success = ($res !== FALSE);
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
@ -65,16 +71,16 @@ namespace Api\Groups {
return $this->createError("Invalid fetch count"); return $this->createError("Invalid fetch count");
} }
if (!$this->getGroupCount()) { if (!$this->fetchGroupCount()) {
return false; return false;
} }
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$res = $sql->select("Group.uid as groupId", "Group.name as groupName", "Group.color as groupColor", $sql->count("UserGroup.user_id")) $res = $sql->select("Group.id as groupId", "Group.name as groupName", "Group.color as groupColor", $sql->count("UserGroup.user_id"))
->from("Group") ->from("Group")
->leftJoin("UserGroup", "UserGroup.group_id", "Group.uid") ->leftJoin("UserGroup", "UserGroup.group_id", "Group.id")
->groupBy("Group.uid") ->groupBy("Group.id")
->orderBy("Group.uid") ->orderBy("Group.id")
->ascending() ->ascending()
->limit($count) ->limit($count)
->offset(($page - 1) * $count) ->offset(($page - 1) * $count)
@ -105,8 +111,8 @@ namespace Api\Groups {
} }
class Create extends GroupsAPI { class Create extends GroupsAPI {
public function __construct($user, $externalCall = false) { public function __construct(Context $context, $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($context, $externalCall, array(
'name' => new StringType('name', 32), 'name' => new StringType('name', 32),
'color' => new StringType('color', 10), 'color' => new StringType('color', 10),
)); ));
@ -130,17 +136,17 @@ namespace Api\Groups {
return $this->createError("A group with this name already exists"); return $this->createError("A group with this name already exists");
} }
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$res = $sql->insert("Group", array("name", "color"))
->addRow($name, $color)
->returning("uid")
->execute();
$this->success = ($res !== FALSE); $group = new Group();
$group->name = $name;
$group->color = $color;
$this->success = ($group->save($sql) !== FALSE);
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
if ($this->success) { if ($this->success) {
$this->result["uid"] = $sql->getLastInsertId(); $this->result["id"] = $group->getId();
} }
return $this->success; return $this->success;
@ -148,33 +154,29 @@ namespace Api\Groups {
} }
class Delete extends GroupsAPI { class Delete extends GroupsAPI {
public function __construct($user, $externalCall = false) { public function __construct(Context $context, $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($context, $externalCall, array(
'uid' => new Parameter('uid', Parameter::TYPE_INT) 'id' => new Parameter('id', Parameter::TYPE_INT)
)); ));
} }
public function _execute(): bool { public function _execute(): bool {
$id = $this->getParam("uid"); $id = $this->getParam("id");
if (in_array($id, DEFAULT_GROUPS)) { if (in_array($id, DEFAULT_GROUPS)) {
return $this->createError("You cannot delete a default group."); return $this->createError("You cannot delete a default group.");
} }
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$res = $sql->select($sql->count()) $group = Group::find($sql, $id);
->from("Group")
->where(new Compare("uid", $id))
->execute();
$this->success = ($res !== FALSE); $this->success = ($group !== FALSE);
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
if ($this->success && $res[0]["count"] === 0) { if ($this->success && $group === null) {
return $this->createError("This group does not exist."); return $this->createError("This group does not exist.");
} }
$res = $sql->delete("Group")->where(new Compare("uid", $id))->execute(); $this->success = ($group->delete($sql) !== FALSE);
$this->success = ($res !== FALSE);
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
return $this->success; return $this->success;
} }

@ -2,10 +2,13 @@
namespace Api { namespace Api {
use Objects\Context;
abstract class LanguageAPI extends Request { abstract class LanguageAPI extends Request {
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
parent::__construct($context, $externalCall, $params);
}
} }
} }
namespace Api\Language { namespace Api\Language {
@ -15,30 +18,28 @@ namespace Api\Language {
use Api\Parameter\StringType; use Api\Parameter\StringType;
use Driver\SQL\Condition\Compare; use Driver\SQL\Condition\Compare;
use Driver\SQL\Condition\CondOr; use Driver\SQL\Condition\CondOr;
use Objects\Language; use Objects\Context;
use Objects\DatabaseEntity\Language;
class Get extends LanguageAPI { class Get extends LanguageAPI {
public function __construct($user, $externalCall = false) { public function __construct(Context $context, $externalCall = false) {
parent::__construct($user, $externalCall, array()); parent::__construct($context, $externalCall, array());
} }
public function _execute(): bool { public function _execute(): bool {
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$res = $sql->select("uid", "code", "name") $languages = Language::findAll($sql);
->from("Language") $this->success = ($languages !== null);
->execute();
$this->success = ($res !== FALSE);
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
if($this->success) { if ($this->success) {
$this->result['languages'] = array(); $this->result['languages'] = [];
if(empty($res) === 0) { if (count($languages) === 0) {
$this->lastError = L("No languages found"); $this->lastError = L("No languages found");
} else { } else {
foreach($res as $row) { foreach ($languages as $language) {
$this->result['languages'][$row['uid']] = $row; $this->result['languages'][$language->getId()] = $language->jsonSerialize();
} }
} }
} }
@ -51,70 +52,65 @@ namespace Api\Language {
private Language $language; private Language $language;
public function __construct($user, $externalCall = false) { public function __construct(Context $context, $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($context, $externalCall, array(
'langId' => new Parameter('langId', Parameter::TYPE_INT, true, NULL), 'langId' => new Parameter('langId', Parameter::TYPE_INT, true, NULL),
'langCode' => new StringType('langCode', 5, true, NULL), 'langCode' => new StringType('langCode', 5, true, NULL),
)); ));
} }
private function checkLanguage() { private function checkLanguage(): bool {
$langId = $this->getParam("langId"); $langId = $this->getParam("langId");
$langCode = $this->getParam("langCode"); $langCode = $this->getParam("langCode");
if(is_null($langId) && is_null($langCode)) { if (is_null($langId) && is_null($langCode)) {
return $this->createError(L("Either langId or langCode must be given")); return $this->createError(L("Either langId or langCode must be given"));
} }
$res = $this->user->getSQL() $sql = $this->context->getSQL();
->select("uid", "code", "name") $languages = Language::findAll($sql,
->from("Language") new CondOr(new Compare("id", $langId), new Compare("code", $langCode))
->where(new CondOr(new Compare("uid", $langId), new Compare("code", $langCode))) );
->execute();
$this->success = ($res !== FALSE); $this->success = ($languages !== null);
$this->lastError = $this->user->getSQL()->getLastError(); $this->lastError = $sql->getLastError();
if ($this->success) { if ($this->success) {
if(count($res) == 0) { if (count($languages) === 0) {
return $this->createError(L("This Language does not exist")); return $this->createError(L("This Language does not exist"));
} else { } else {
$row = $res[0]; $this->language = array_shift($languages);
$this->language = Language::newInstance($row['uid'], $row['code'], $row['name']);
if(!$this->language) {
return $this->createError(L("Error while loading language"));
}
} }
} }
return $this->success; return $this->success;
} }
private function updateLanguage() { private function updateLanguage(): bool {
$languageId = $this->language->getId(); $languageId = $this->language->getId();
$userId = $this->user->getId(); $userId = $this->context->getUser()->getId();
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$this->success = $sql->update("User") $this->success = $sql->update("User")
->set("language_id", $languageId) ->set("language_id", $languageId)
->where(new Compare("uid", $userId)) ->where(new Compare("id", $userId))
->execute(); ->execute();
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
return $this->success; return $this->success;
} }
public function _execute(): bool { public function _execute(): bool {
if(!$this->checkLanguage()) if (!$this->checkLanguage())
return false; return false;
if($this->user->isLoggedIn()) { if ($this->context->getSession()) {
$this->updateLanguage(); $this->updateLanguage();
} }
$this->user->setLanguage($this->language); $this->context->setLanguage($this->language);
return $this->success; return $this->success;
} }
} }
} }

@ -2,11 +2,11 @@
namespace Api { namespace Api {
use Objects\User; use Objects\Context;
abstract class LogsAPI extends Request { abstract class LogsAPI extends Request {
public function __construct(User $user, bool $externalCall = false, array $params = array()) { public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
parent::__construct($user, $externalCall, $params); parent::__construct($context, $externalCall, $params);
} }
} }
@ -21,20 +21,22 @@ namespace Api\Logs {
use Driver\SQL\Column\Column; use Driver\SQL\Column\Column;
use Driver\SQL\Condition\Compare; use Driver\SQL\Condition\Compare;
use Driver\SQL\Condition\CondIn; use Driver\SQL\Condition\CondIn;
use Objects\User; use Objects\Context;
use Objects\DatabaseEntity\SystemLog;
class Get extends LogsAPI { class Get extends LogsAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, [ parent::__construct($context, $externalCall, [
"since" => new Parameter("since", Parameter::TYPE_DATE_TIME, true), "since" => new Parameter("since", Parameter::TYPE_DATE_TIME, true),
"severity" => new StringType("severity", 32, true, "debug") "severity" => new StringType("severity", 32, true, "debug")
]); ]);
$this->csrfTokenRequired = false;
} }
protected function _execute(): bool { protected function _execute(): bool {
$since = $this->getParam("since"); $since = $this->getParam("since");
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$severity = strtolower(trim($this->getParam("severity"))); $severity = strtolower(trim($this->getParam("severity")));
$shownLogLevels = Logger::LOG_LEVELS; $shownLogLevels = Logger::LOG_LEVELS;
@ -45,8 +47,7 @@ namespace Api\Logs {
$shownLogLevels = array_slice(Logger::LOG_LEVELS, $logLevel); $shownLogLevels = array_slice(Logger::LOG_LEVELS, $logLevel);
} }
$query = $sql->select("id", "module", "message", "severity", "timestamp") $query = SystemLog::findAllBuilder($sql)
->from("SystemLog")
->orderBy("timestamp") ->orderBy("timestamp")
->descending(); ->descending();
@ -58,12 +59,15 @@ namespace Api\Logs {
$query->where(new CondIn(new Column("severity"), $shownLogLevels)); $query->where(new CondIn(new Column("severity"), $shownLogLevels));
} }
$res = $query->execute(); $logEntries = $query->execute();
$this->success = $res !== false; $this->success = $logEntries !== false;
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
if ($this->success) { if ($this->success) {
$this->result["logs"] = $res; $this->result["logs"] = [];
foreach ($logEntries as $logEntry) {
$this->result["logs"][] = $logEntry->jsonSerialize();
}
} else { } else {
// we couldn't fetch logs from database, return a message and proceed to log files // we couldn't fetch logs from database, return a message and proceed to log files
$this->result["logs"] = [ $this->result["logs"] = [

@ -3,10 +3,16 @@
namespace Api { namespace Api {
use Objects\ConnectionData; use Objects\ConnectionData;
use Objects\Context;
abstract class MailAPI extends Request { abstract class MailAPI extends Request {
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
parent::__construct($context, $externalCall, $params);
}
protected function getMailConfig(): ?ConnectionData { protected function getMailConfig(): ?ConnectionData {
$req = new \Api\Settings\Get($this->user); $req = new \Api\Settings\Get($this->context);
$this->success = $req->execute(array("key" => "^mail_")); $this->success = $req->execute(array("key" => "^mail_"));
$this->lastError = $req->getLastError(); $this->lastError = $req->getLastError();
@ -47,13 +53,13 @@ namespace Api\Mail {
use External\PHPMailer\Exception; use External\PHPMailer\Exception;
use External\PHPMailer\PHPMailer; use External\PHPMailer\PHPMailer;
use Objects\ConnectionData; use Objects\ConnectionData;
use Objects\GpgKey; use Objects\Context;
use Objects\User; use Objects\DatabaseEntity\GpgKey;
class Test extends MailAPI { class Test extends MailAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($context, $externalCall, array(
"receiver" => new Parameter("receiver", Parameter::TYPE_EMAIL), "receiver" => new Parameter("receiver", Parameter::TYPE_EMAIL),
"gpgFingerprint" => new StringType("gpgFingerprint", 64, true, null) "gpgFingerprint" => new StringType("gpgFingerprint", 64, true, null)
)); ));
@ -62,7 +68,7 @@ namespace Api\Mail {
public function _execute(): bool { public function _execute(): bool {
$receiver = $this->getParam("receiver"); $receiver = $this->getParam("receiver");
$req = new \Api\Mail\Send($this->user); $req = new \Api\Mail\Send($this->context);
$this->success = $req->execute(array( $this->success = $req->execute(array(
"to" => $receiver, "to" => $receiver,
"subject" => "Test E-Mail", "subject" => "Test E-Mail",
@ -76,16 +82,15 @@ namespace Api\Mail {
} }
} }
// TODO: expired gpg keys?
class Send extends MailAPI { class Send extends MailAPI {
public function __construct($user, $externalCall = false) { public function __construct(Context $context, $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($context, $externalCall, array(
'to' => new Parameter('to', Parameter::TYPE_EMAIL, true, null), 'to' => new Parameter('to', Parameter::TYPE_EMAIL, true, null),
'subject' => new StringType('subject', -1), 'subject' => new StringType('subject', -1),
'body' => new StringType('body', -1), 'body' => new StringType('body', -1),
'replyTo' => new Parameter('replyTo', Parameter::TYPE_EMAIL, true, null), 'replyTo' => new Parameter('replyTo', Parameter::TYPE_EMAIL, true, null),
'replyName' => new StringType('replyName', 32, true, ""), 'replyName' => new StringType('replyName', 32, true, ""),
"gpgFingerprint" => new StringType("gpgFingerprint", 64, true, null), 'gpgFingerprint' => new StringType("gpgFingerprint", 64, true, null),
'async' => new Parameter("async", Parameter::TYPE_BOOLEAN, true, true) 'async' => new Parameter("async", Parameter::TYPE_BOOLEAN, true, true)
)); ));
$this->isPublic = false; $this->isPublic = false;
@ -108,7 +113,7 @@ namespace Api\Mail {
$gpgFingerprint = $this->getParam("gpgFingerprint"); $gpgFingerprint = $this->getParam("gpgFingerprint");
if ($this->getParam("async")) { if ($this->getParam("async")) {
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$this->success = $sql->insert("MailQueue", ["from", "to", "subject", "body", $this->success = $sql->insert("MailQueue", ["from", "to", "subject", "body",
"replyTo", "replyName", "gpgFingerprint"]) "replyTo", "replyName", "gpgFingerprint"])
->addRow($fromMail, $toMail, $subject, $body, $replyTo, $replyName, $gpgFingerprint) ->addRow($fromMail, $toMail, $subject, $body, $replyTo, $replyName, $gpgFingerprint)
@ -200,14 +205,14 @@ namespace Api\Mail {
// TODO: attachments // TODO: attachments
class Sync extends MailAPI { class Sync extends MailAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, array()); parent::__construct($context, $externalCall, array());
$this->loginRequired = true; $this->loginRequired = true;
} }
private function fetchMessageIds() { private function fetchMessageIds() {
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$res = $sql->select("uid", "messageId") $res = $sql->select("id", "messageId")
->from("ContactRequest") ->from("ContactRequest")
->where(new Compare("messageId", NULL, "!=")) ->where(new Compare("messageId", NULL, "!="))
->execute(); ->execute();
@ -220,7 +225,7 @@ namespace Api\Mail {
$messageIds = []; $messageIds = [];
foreach ($res as $row) { foreach ($res as $row) {
$messageIds[$row["messageId"]] = $row["uid"]; $messageIds[$row["messageId"]] = $row["id"];
} }
return $messageIds; return $messageIds;
} }
@ -241,7 +246,7 @@ namespace Api\Mail {
} }
private function insertMessages($messages): bool { private function insertMessages($messages): bool {
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$query = $sql->insert("ContactMessage", ["request_id", "user_id", "message", "messageId", "created_at"]) $query = $sql->insert("ContactMessage", ["request_id", "user_id", "message", "messageId", "created_at"])
->onDuplicateKeyStrategy(new UpdateStrategy(["messageId"], ["message" => new Column("message")])); ->onDuplicateKeyStrategy(new UpdateStrategy(["messageId"], ["message" => new Column("message")]));
@ -251,7 +256,7 @@ namespace Api\Mail {
$requestId = $message["requestId"]; $requestId = $message["requestId"];
$query->addRow( $query->addRow(
$requestId, $requestId,
$sql->select("uid")->from("User")->where(new Compare("email", $message["from"]))->limit(1), $sql->select("id")->from("User")->where(new Compare("email", $message["from"]))->limit(1),
$message["body"], $message["body"],
$message["messageId"], $message["messageId"],
(new \DateTime())->setTimeStamp($message["timestamp"]), (new \DateTime())->setTimeStamp($message["timestamp"]),
@ -450,7 +455,7 @@ namespace Api\Mail {
return false; return false;
} }
$req = new \Api\Settings\Set($this->user); $req = new \Api\Settings\Set($this->context);
$this->success = $req->execute(array("settings" => array("mail_last_sync" => "$now"))); $this->success = $req->execute(array("settings" => array("mail_last_sync" => "$now")));
$this->lastError = $req->getLastError(); $this->lastError = $req->getLastError();
return $this->success; return $this->success;
@ -458,8 +463,8 @@ namespace Api\Mail {
} }
class SendQueue extends MailAPI { class SendQueue extends MailAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, [ parent::__construct($context, $externalCall, [
"debug" => new Parameter("debug", Parameter::TYPE_BOOLEAN, true, false) "debug" => new Parameter("debug", Parameter::TYPE_BOOLEAN, true, false)
]); ]);
$this->isPublic = false; $this->isPublic = false;
@ -473,8 +478,8 @@ namespace Api\Mail {
echo "Start of processing mail queue at $startTime" . PHP_EOL; echo "Start of processing mail queue at $startTime" . PHP_EOL;
} }
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$res = $sql->select("uid", "from", "to", "subject", "body", $res = $sql->select("id", "from", "to", "subject", "body",
"replyTo", "replyName", "gpgFingerprint", "retryCount") "replyTo", "replyName", "gpgFingerprint", "retryCount")
->from("MailQueue") ->from("MailQueue")
->where(new Compare("retryCount", 0, ">")) ->where(new Compare("retryCount", 0, ">"))
@ -505,9 +510,9 @@ namespace Api\Mail {
echo "Sending subject=$subject to=$to" . PHP_EOL; echo "Sending subject=$subject to=$to" . PHP_EOL;
} }
$mailId = intval($row["uid"]); $mailId = intval($row["id"]);
$retryCount = intval($row["retryCount"]); $retryCount = intval($row["retryCount"]);
$req = new Send($this->user); $req = new Send($this->context);
$args = [ $args = [
"to" => $to, "to" => $to,
"subject" => $subject, "subject" => $subject,
@ -529,7 +534,7 @@ namespace Api\Mail {
->set("status", "error") ->set("status", "error")
->set("errorMessage", $error) ->set("errorMessage", $error)
->set("nextTry", $nextTry) ->set("nextTry", $nextTry)
->where(new Compare("uid", $mailId)) ->where(new Compare("id", $mailId))
->execute(); ->execute();
} else { } else {
$successfulMails[] = $mailId; $successfulMails[] = $mailId;
@ -540,7 +545,7 @@ namespace Api\Mail {
if (!empty($successfulMails)) { if (!empty($successfulMails)) {
$res = $sql->update("MailQueue") $res = $sql->update("MailQueue")
->set("status", "success") ->set("status", "success")
->where(new CondIn(new Column("uid"), $successfulMails)) ->where(new CondIn(new Column("id"), $successfulMails))
->execute(); ->execute();
$this->success = $res !== false; $this->success = $res !== false;
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();

@ -2,11 +2,11 @@
namespace Api { namespace Api {
use Objects\User; use Objects\Context;
abstract class NewsAPI extends Request { abstract class NewsAPI extends Request {
public function __construct(User $user, bool $externalCall = false, array $params = array()) { public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
parent::__construct($user, $externalCall, $params); parent::__construct($context, $externalCall, $params);
$this->loginRequired = true; $this->loginRequired = true;
} }
} }
@ -18,57 +18,47 @@ namespace Api\News {
use Api\Parameter\Parameter; use Api\Parameter\Parameter;
use Api\Parameter\StringType; use Api\Parameter\StringType;
use Driver\SQL\Condition\Compare; use Driver\SQL\Condition\Compare;
use Objects\User; use Objects\Context;
use Objects\DatabaseEntity\News;
class Get extends NewsAPI { class Get extends NewsAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, [ parent::__construct($context, $externalCall, [
"since" => new Parameter("since", Parameter::TYPE_DATE_TIME, true, null), "since" => new Parameter("since", Parameter::TYPE_DATE_TIME, true, null),
"limit" => new Parameter("limit", Parameter::TYPE_INT, true, 10) "limit" => new Parameter("limit", Parameter::TYPE_INT, true, 10)
]); ]);
$this->loginRequired = false;
} }
public function _execute(): bool { public function _execute(): bool {
$sql = $this->user->getSQL();
$query = $sql->select("News.uid", "title", "text", "publishedAt",
"User.uid as publisherId", "User.name as publisherName", "User.fullName as publisherFullName")
->from("News")
->innerJoin("User", "User.uid", "News.publishedBy")
->orderBy("publishedAt")
->descending();
$since = $this->getParam("since"); $since = $this->getParam("since");
if ($since) {
$query->where(new Compare("publishedAt", $since, ">="));
}
$limit = $this->getParam("limit"); $limit = $this->getParam("limit");
if ($limit < 1 || $limit > 30) { if ($limit < 1 || $limit > 30) {
return $this->createError("Limit must be in range 1-30"); return $this->createError("Limit must be in range 1-30");
} else {
$query->limit($limit);
} }
$res = $query->execute(); $sql = $this->context->getSQL();
$this->success = $res !== false; $newsQuery = News::findAllBuilder($sql)
->limit($limit)
->orderBy("published_at")
->descending()
->fetchEntities();
if ($since) {
$newsQuery->where(new Compare("published_at", $since, ">="));
}
$newsArray = $newsQuery->execute();
$this->success = $newsArray !== null;
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
if ($this->success) { if ($this->success) {
$this->result["news"] = []; $this->result["news"] = [];
foreach ($res as $row) { foreach ($newsArray as $news) {
$newsId = intval($row["uid"]); $newsId = $news->getId();
$this->result["news"][$newsId] = [ $this->result["news"][$newsId] = $news->jsonSerialize();
"id" => $newsId,
"title" => $row["title"],
"text" => $row["text"],
"publishedAt" => $row["publishedAt"],
"publisher" => [
"id" => intval($row["publisherId"]),
"name" => $row["publisherName"],
"fullName" => $row["publisherFullName"]
]
];
} }
} }
@ -77,28 +67,27 @@ namespace Api\News {
} }
class Publish extends NewsAPI { class Publish extends NewsAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, [ parent::__construct($context, $externalCall, [
"title" => new StringType("title", 128), "title" => new StringType("title", 128),
"text" => new StringType("text", 1024) "text" => new StringType("text", 1024)
]); ]);
$this->loginRequired = true;
} }
public function _execute(): bool { public function _execute(): bool {
$sql = $this->user->getSQL();
$title = $this->getParam("title");
$text = $this->getParam("text");
$res = $sql->insert("News", ["title", "text"]) $news = new News();
->addRow($title, $text) $news->text = $this->getParam("text");
->returning("uid") $news->title = $this->getParam("title");
->execute(); $news->publishedBy = $this->context->getUser();
$this->success = $res !== false; $sql = $this->context->getSQL();
$this->success = $news->save($sql);
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
if ($this->success) { if ($this->success) {
$this->result["newsId"] = $sql->getLastInsertId(); $this->result["newsId"] = $news->getId();
} }
return $this->success; return $this->success;
@ -106,77 +95,62 @@ namespace Api\News {
} }
class Delete extends NewsAPI { class Delete extends NewsAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, [ parent::__construct($context, $externalCall, [
"id" => new Parameter("id", Parameter::TYPE_INT) "id" => new Parameter("id", Parameter::TYPE_INT)
]); ]);
$this->loginRequired = true;
} }
public function _execute(): bool { public function _execute(): bool {
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$id = $this->getParam("id"); $currentUser = $this->context->getUser();
$res = $sql->select("publishedBy")
->from("News")
->where(new Compare("uid", $id))
->execute();
$this->success = ($res !== false); $news = News::find($sql, $this->getParam("id"));
$this->success = ($news !== false);
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
if (!$this->success) { if (!$this->success) {
return false; return false;
} else if (empty($res) || !is_array($res)) { } else if ($news === null) {
return $this->createError("News Post not found"); return $this->createError("News Post not found");
} else if (intval($res[0]["publishedBy"]) !== $this->user->getId() && !$this->user->hasGroup(USER_GROUP_ADMIN)) { } else if ($news->publishedBy->getId() !== $currentUser->getId() && !$currentUser->hasGroup(USER_GROUP_ADMIN)) {
return $this->createError("You do not have permissions to delete news post of other users."); return $this->createError("You do not have permissions to delete news post of other users.");
} }
$res = $sql->delete("News") $this->success = $news->delete($sql);
->where(new Compare("uid", $id))
->execute();
$this->success = $res !== false;
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
return $this->success; return $this->success;
} }
} }
class Edit extends NewsAPI { class Edit extends NewsAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, [ parent::__construct($context, $externalCall, [
"id" => new Parameter("id", Parameter::TYPE_INT), "id" => new Parameter("id", Parameter::TYPE_INT),
"title" => new StringType("title", 128), "title" => new StringType("title", 128),
"text" => new StringType("text", 1024) "text" => new StringType("text", 1024)
]); ]);
$this->loginRequired = true;
} }
public function _execute(): bool { public function _execute(): bool {
$sql = $this->context->getSQL();
$currentUser = $this->context->getUser();
$sql = $this->user->getSQL(); $news = News::find($sql, $this->getParam("id"));
$id = $this->getParam("id"); $this->success = ($news !== false);
$text = $this->getParam("text");
$title = $this->getParam("title");
$res = $sql->select("publishedBy")
->from("News")
->where(new Compare("uid", $id))
->execute();
$this->success = ($res !== false);
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
if (!$this->success) { if (!$this->success) {
return false; return false;
} else if (empty($res) || !is_array($res)) { } else if ($news === null) {
return $this->createError("News Post not found"); return $this->createError("News Post not found");
} else if (intval($res[0]["publishedBy"]) !== $this->user->getId() && !$this->user->hasGroup(USER_GROUP_ADMIN)) { } else if ($news->publishedBy->getId() !== $currentUser->getId() && !$currentUser->hasGroup(USER_GROUP_ADMIN)) {
return $this->createError("You do not have permissions to edit news post of other users."); return $this->createError("You do not have permissions to edit news post of other users.");
} }
$res = $sql->update("News") $news->text = $this->getParam("text");
->set("title", $title) $news->title = $this->getParam("title");
->set("text", $text) $this->success = $news->save($sql);
->where(new Compare("uid", $id))
->execute();
$this->success = $res !== false;
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
return $this->success; return $this->success;
} }

@ -1,8 +1,13 @@
<?php <?php
namespace Api { namespace Api {
abstract class NotificationsAPI extends Request {
use Objects\Context;
abstract class NotificationsAPI extends Request {
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
parent::__construct($context, $externalCall, $params);
}
} }
} }
@ -15,12 +20,15 @@ namespace Api\Notifications {
use Driver\SQL\Condition\Compare; use Driver\SQL\Condition\Compare;
use Driver\SQL\Condition\CondIn; use Driver\SQL\Condition\CondIn;
use Driver\SQL\Query\Select; use Driver\SQL\Query\Select;
use Objects\User; use Objects\Context;
use Objects\DatabaseEntity\Group;
use Objects\DatabaseEntity\Notification;
use Objects\DatabaseEntity\User;
class Create extends NotificationsAPI { class Create extends NotificationsAPI {
public function __construct($user, $externalCall = false) { public function __construct(Context $context, $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($context, $externalCall, array(
'groupId' => new Parameter('groupId', Parameter::TYPE_INT, true), 'groupId' => new Parameter('groupId', Parameter::TYPE_INT, true),
'userId' => new Parameter('userId', Parameter::TYPE_INT, true), 'userId' => new Parameter('userId', Parameter::TYPE_INT, true),
'title' => new StringType('title', 32), 'title' => new StringType('title', 32),
@ -29,28 +37,8 @@ namespace Api\Notifications {
$this->isPublic = false; $this->isPublic = false;
} }
private function checkUser($userId) { private function insertUserNotification($userId, $notificationId): bool {
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$res = $sql->select($sql->count())
->from("User")
->where(new Compare("uid", $userId))
->execute();
$this->success = ($res !== FALSE);
$this->lastError = $sql->getLastError();
if ($this->success) {
if ($res[0]["count"] == 0) {
$this->success = false;
$this->lastError = "User not found";
}
}
return $this->success;
}
private function insertUserNotification($userId, $notificationId) {
$sql = $this->user->getSQL();
$res = $sql->insert("UserNotification", array("user_id", "notification_id")) $res = $sql->insert("UserNotification", array("user_id", "notification_id"))
->addRow($userId, $notificationId) ->addRow($userId, $notificationId)
->execute(); ->execute();
@ -60,28 +48,8 @@ namespace Api\Notifications {
return $this->success; return $this->success;
} }
private function checkGroup($groupId) { private function insertGroupNotification($groupId, $notificationId): bool {
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$res = $sql->select($sql->count())
->from("Group")
->where(new Compare("uid", $groupId))
->execute();
$this->success = ($res !== FALSE);
$this->lastError = $sql->getLastError();
if ($this->success) {
if ($res[0]["count"] == 0) {
$this->success = false;
$this->lastError = "Group not found";
}
}
return $this->success;
}
private function insertGroupNotification($groupId, $notificationId) {
$sql = $this->user->getSQL();
$res = $sql->insert("GroupNotification", array("group_id", "notification_id")) $res = $sql->insert("GroupNotification", array("group_id", "notification_id"))
->addRow($groupId, $notificationId) ->addRow($groupId, $notificationId)
->execute(); ->execute();
@ -91,24 +59,24 @@ namespace Api\Notifications {
return $this->success; return $this->success;
} }
private function createNotification($title, $message) { private function createNotification($title, $message): bool|int {
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$res = $sql->insert("Notification", array("title", "message")) $notification = new Notification();
->addRow($title, $message) $notification->title = $title;
->returning("uid") $notification->message = $message;
->execute();
$this->success = ($res !== FALSE); $this->success = ($notification->save($sql) !== FALSE);
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
if ($this->success) { if ($this->success) {
return $sql->getLastInsertId(); return $notification->getId();
} }
return $this->success; return $this->success;
} }
public function _execute(): bool { public function _execute(): bool {
$sql = $this->context->getSQL();
$userId = $this->getParam("userId"); $userId = $this->getParam("userId");
$groupId = $this->getParam("groupId"); $groupId = $this->getParam("groupId");
$title = $this->getParam("title"); $title = $this->getParam("title");
@ -119,18 +87,22 @@ namespace Api\Notifications {
} else if(!is_null($userId) && !is_null($groupId)) { } else if(!is_null($userId) && !is_null($groupId)) {
return $this->createError("Only one of userId and groupId must be specified."); return $this->createError("Only one of userId and groupId must be specified.");
} else if(!is_null($userId)) { } else if(!is_null($userId)) {
if ($this->checkUser($userId)) { if (User::exists($sql, $userId)) {
$id = $this->createNotification($title, $message); $id = $this->createNotification($title, $message);
if ($this->success) { if ($this->success) {
return $this->insertUserNotification($userId, $id); return $this->insertUserNotification($userId, $id);
} }
} else {
return $this->createError("User not found: $userId");
} }
} else if(!is_null($groupId)) { } else if(!is_null($groupId)) {
if ($this->checkGroup($groupId)) { if (Group::exists($sql, $groupId)) {
$id = $this->createNotification($title, $message); $id = $this->createNotification($title, $message);
if ($this->success) { if ($this->success) {
return $this->insertGroupNotification($groupId, $id); return $this->insertGroupNotification($groupId, $id);
} }
} else {
return $this->createError("Group not found: $groupId");
} }
} }
@ -141,22 +113,22 @@ namespace Api\Notifications {
class Fetch extends NotificationsAPI { class Fetch extends NotificationsAPI {
private array $notifications; private array $notifications;
private array $notificationids; private array $notificationIds;
public function __construct($user, $externalCall = false) { public function __construct(Context $context, $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($context, $externalCall, array(
'new' => new Parameter('new', Parameter::TYPE_BOOLEAN, true, true) 'new' => new Parameter('new', Parameter::TYPE_BOOLEAN, true, true)
)); ));
$this->loginRequired = true; $this->loginRequired = true;
} }
private function fetchUserNotifications() { private function fetchUserNotifications(): bool {
$onlyNew = $this->getParam('new'); $onlyNew = $this->getParam('new');
$userId = $this->user->getId(); $userId = $this->context->getUser()->getId();
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$query = $sql->select($sql->distinct("Notification.uid"), "created_at", "title", "message", "type") $query = $sql->select($sql->distinct("Notification.id"), "created_at", "title", "message", "type")
->from("Notification") ->from("Notification")
->innerJoin("UserNotification", "UserNotification.notification_id", "Notification.uid") ->innerJoin("UserNotification", "UserNotification.notification_id", "Notification.id")
->where(new Compare("UserNotification.user_id", $userId)) ->where(new Compare("UserNotification.user_id", $userId))
->orderBy("created_at")->descending(); ->orderBy("created_at")->descending();
@ -167,13 +139,13 @@ namespace Api\Notifications {
return $this->fetchNotifications($query); return $this->fetchNotifications($query);
} }
private function fetchGroupNotifications() { private function fetchGroupNotifications(): bool {
$onlyNew = $this->getParam('new'); $onlyNew = $this->getParam('new');
$userId = $this->user->getId(); $userId = $this->context->getUser()->getId();
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$query = $sql->select($sql->distinct("Notification.uid"), "created_at", "title", "message", "type") $query = $sql->select($sql->distinct("Notification.id"), "created_at", "title", "message", "type")
->from("Notification") ->from("Notification")
->innerJoin("GroupNotification", "GroupNotification.notification_id", "Notification.uid") ->innerJoin("GroupNotification", "GroupNotification.notification_id", "Notification.id")
->innerJoin("UserGroup", "GroupNotification.group_id", "UserGroup.group_id") ->innerJoin("UserGroup", "GroupNotification.group_id", "UserGroup.group_id")
->where(new Compare("UserGroup.user_id", $userId)) ->where(new Compare("UserGroup.user_id", $userId))
->orderBy("created_at")->descending(); ->orderBy("created_at")->descending();
@ -185,19 +157,19 @@ namespace Api\Notifications {
return $this->fetchNotifications($query); return $this->fetchNotifications($query);
} }
private function fetchNotifications(Select $query) { private function fetchNotifications(Select $query): bool {
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$res = $query->execute(); $res = $query->execute();
$this->success = ($res !== FALSE); $this->success = ($res !== FALSE);
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
if ($this->success) { if ($this->success) {
foreach($res as $row) { foreach($res as $row) {
$id = $row["uid"]; $id = $row["id"];
if (!in_array($id, $this->notificationids)) { if (!in_array($id, $this->notificationIds)) {
$this->notificationids[] = $id; $this->notificationIds[] = $id;
$this->notifications[] = array( $this->notifications[] = array(
"uid" => $id, "id" => $id,
"title" => $row["title"], "title" => $row["title"],
"message" => $row["message"], "message" => $row["message"],
"created_at" => $row["created_at"], "created_at" => $row["created_at"],
@ -212,7 +184,7 @@ namespace Api\Notifications {
public function _execute(): bool { public function _execute(): bool {
$this->notifications = array(); $this->notifications = array();
$this->notificationids = array(); $this->notificationIds = array();
if ($this->fetchUserNotifications() && $this->fetchGroupNotifications()) { if ($this->fetchUserNotifications() && $this->fetchGroupNotifications()) {
$this->result["notifications"] = $this->notifications; $this->result["notifications"] = $this->notifications;
} }
@ -223,17 +195,18 @@ namespace Api\Notifications {
class Seen extends NotificationsAPI { class Seen extends NotificationsAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, array()); parent::__construct($context, $externalCall, array());
$this->loginRequired = true; $this->loginRequired = true;
} }
public function _execute(): bool { public function _execute(): bool {
$sql = $this->user->getSQL(); $currentUser = $this->context->getUser();
$sql = $this->context->getSQL();
$res = $sql->update("UserNotification") $res = $sql->update("UserNotification")
->set("seen", true) ->set("seen", true)
->where(new Compare("user_id", $this->user->getId())) ->where(new Compare("user_id", $currentUser->getId()))
->execute(); ->execute();
$this->success = ($res !== FALSE); $this->success = ($res !== FALSE);
@ -245,7 +218,7 @@ namespace Api\Notifications {
->where(new CondIn(new Column("group_id"), ->where(new CondIn(new Column("group_id"),
$sql->select("group_id") $sql->select("group_id")
->from("UserGroup") ->from("UserGroup")
->where(new Compare("user_id", $this->user->getId())))) ->where(new Compare("user_id", $currentUser->getId()))))
->execute(); ->execute();
$this->success = ($res !== FALSE); $this->success = ($res !== FALSE);

@ -2,9 +2,17 @@
namespace Api { namespace Api {
use Objects\Context;
abstract class PermissionAPI extends Request { abstract class PermissionAPI extends Request {
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
parent::__construct($context, $externalCall, $params);
}
protected function checkStaticPermission(): bool { protected function checkStaticPermission(): bool {
if (!$this->user->isLoggedIn() || !$this->user->hasGroup(USER_GROUP_ADMIN)) { $user = $this->context->getUser();
if (!$user || !$user->hasGroup(USER_GROUP_ADMIN)) {
return $this->createError("Permission denied."); return $this->createError("Permission denied.");
} }
@ -24,12 +32,14 @@ namespace Api\Permission {
use Driver\SQL\Condition\CondLike; use Driver\SQL\Condition\CondLike;
use Driver\SQL\Condition\CondNot; use Driver\SQL\Condition\CondNot;
use Driver\SQL\Strategy\UpdateStrategy; use Driver\SQL\Strategy\UpdateStrategy;
use Objects\User; use Objects\Context;
use Objects\DatabaseEntity\Group;
use Objects\DatabaseEntity\User;
class Check extends PermissionAPI { class Check extends PermissionAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($context, $externalCall, array(
'method' => new StringType('method', 323) 'method' => new StringType('method', 323)
)); ));
@ -39,7 +49,7 @@ namespace Api\Permission {
public function _execute(): bool { public function _execute(): bool {
$method = $this->getParam("method"); $method = $this->getParam("method");
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$res = $sql->select("groups") $res = $sql->select("groups")
->from("ApiPermission") ->from("ApiPermission")
->where(new CondLike($method, new Column("method"))) ->where(new CondLike($method, new Column("method")))
@ -58,8 +68,9 @@ namespace Api\Permission {
return true; return true;
} }
$userGroups = $this->user->getGroups(); $currentUser = $this->context->getUser();
if (empty($userGroups) || empty(array_intersect($groups, array_keys($this->user->getGroups())))) { $userGroups = $currentUser ? $currentUser->getGroups() : [];
if (empty($userGroups) || empty(array_intersect($groups, array_keys($userGroups)))) {
http_response_code(401); http_response_code(401);
return $this->createError("Permission denied."); return $this->createError("Permission denied.");
} }
@ -71,33 +82,17 @@ namespace Api\Permission {
class Fetch extends PermissionAPI { class Fetch extends PermissionAPI {
private array $groups; private ?array $groups;
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, array()); parent::__construct($context, $externalCall, array());
} }
private function fetchGroups() { private function fetchGroups(): bool {
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$res = $sql->select("uid", "name", "color") $this->groups = Group::findAll($sql);
->from("Group") $this->success = ($this->groups !== FALSE);
->orderBy("uid")
->ascending()
->execute();
$this->success = ($res !== FALSE);
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
if ($this->success) {
$this->groups = array();
foreach($res as $row) {
$groupId = $row["uid"];
$groupName = $row["name"];
$groupColor = $row["color"];
$this->groups[$groupId] = array("name" => $groupName, "color" => $groupColor);
}
}
return $this->success; return $this->success;
} }
@ -107,7 +102,7 @@ namespace Api\Permission {
return false; return false;
} }
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$res = $sql->select("method", "groups", "description") $res = $sql->select("method", "groups", "description")
->from("ApiPermission") ->from("ApiPermission")
->execute(); ->execute();
@ -137,8 +132,8 @@ namespace Api\Permission {
class Save extends PermissionAPI { class Save extends PermissionAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($context, $externalCall, array(
'permissions' => new Parameter('permissions', Parameter::TYPE_ARRAY) 'permissions' => new Parameter('permissions', Parameter::TYPE_ARRAY)
)); ));
} }
@ -150,27 +145,27 @@ namespace Api\Permission {
} }
$permissions = $this->getParam("permissions"); $permissions = $this->getParam("permissions");
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$methodParam = new StringType('method', 32); $methodParam = new StringType('method', 32);
$groupsParam = new Parameter('groups', Parameter::TYPE_ARRAY); $groupsParam = new Parameter('groups', Parameter::TYPE_ARRAY);
$updateQuery = $sql->insert("ApiPermission", array("method", "groups")) $updateQuery = $sql->insert("ApiPermission", array("method", "groups"))
->onDuplicateKeyStrategy(new UpdateStrategy(array("method"), array( "groups" => new Column("groups") ))); ->onDuplicateKeyStrategy(new UpdateStrategy(array("method"), array("groups" => new Column("groups"))));
$insertedMethods = array(); $insertedMethods = array();
foreach($permissions as $permission) { foreach ($permissions as $permission) {
if (!is_array($permission)) { if (!is_array($permission)) {
return $this->createError("Invalid data type found in parameter: permissions, expected: object"); return $this->createError("Invalid data type found in parameter: permissions, expected: object");
} else if(!isset($permission["method"]) || !array_key_exists("groups", $permission)) { } else if (!isset($permission["method"]) || !array_key_exists("groups", $permission)) {
return $this->createError("Invalid object found in parameter: permissions, expected keys 'method' and 'groups'"); return $this->createError("Invalid object found in parameter: permissions, expected keys 'method' and 'groups'");
} else if (!$methodParam->parseParam($permission["method"])) { } else if (!$methodParam->parseParam($permission["method"])) {
$expectedType = $methodParam->getTypeName(); $expectedType = $methodParam->getTypeName();
return $this->createError("Invalid data type found for attribute 'method', expected: $expectedType"); return $this->createError("Invalid data type found for attribute 'method', expected: $expectedType");
} else if(!$groupsParam->parseParam($permission["groups"])) { } else if (!$groupsParam->parseParam($permission["groups"])) {
$expectedType = $groupsParam->getTypeName(); $expectedType = $groupsParam->getTypeName();
return $this->createError("Invalid data type found for attribute 'groups', expected: $expectedType"); return $this->createError("Invalid data type found for attribute 'groups', expected: $expectedType");
} else if(empty(trim($methodParam->value))) { } else if (empty(trim($methodParam->value))) {
return $this->createError("Method cannot be empty."); return $this->createError("Method cannot be empty.");
} else { } else {
$method = $methodParam->value; $method = $methodParam->value;

@ -3,7 +3,7 @@
namespace Api; namespace Api;
use Driver\Logger\Logger; use Driver\Logger\Logger;
use Objects\User; use Objects\Context;
use PhpMqtt\Client\MqttClient; use PhpMqtt\Client\MqttClient;
/** /**
@ -14,7 +14,7 @@ use PhpMqtt\Client\MqttClient;
abstract class Request { abstract class Request {
protected User $user; protected Context $context;
protected Logger $logger; protected Logger $logger;
protected array $params; protected array $params;
protected string $lastError; protected string $lastError;
@ -31,9 +31,9 @@ abstract class Request {
private array $allowedMethods; private array $allowedMethods;
private bool $externalCall; private bool $externalCall;
public function __construct(User $user, bool $externalCall = false, array $params = array()) { public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
$this->user = $user; $this->context = $context;
$this->logger = new Logger($this->getAPIName(), $this->user->getSQL()); $this->logger = new Logger($this->getAPIName(), $this->context->getSQL());
$this->defaultParams = $params; $this->defaultParams = $params;
$this->success = false; $this->success = false;
@ -137,8 +137,9 @@ abstract class Request {
$this->result = array(); $this->result = array();
$this->lastError = ''; $this->lastError = '';
if ($this->user->isLoggedIn()) { $session = $this->context->getSession();
$this->result['logoutIn'] = $this->user->getSession()->getExpiresSeconds(); if ($session) {
$this->result['logoutIn'] = $session->getExpiresSeconds();
} }
if ($this->externalCall) { if ($this->externalCall) {
@ -183,25 +184,24 @@ abstract class Request {
} }
$apiKeyAuthorized = false; $apiKeyAuthorized = false;
if (!$session && $this->apiKeyAllowed) {
if (!$this->user->isLoggedIn() && $this->apiKeyAllowed) {
if (isset($_SERVER["HTTP_AUTHORIZATION"])) { if (isset($_SERVER["HTTP_AUTHORIZATION"])) {
$authHeader = $_SERVER["HTTP_AUTHORIZATION"]; $authHeader = $_SERVER["HTTP_AUTHORIZATION"];
if (startsWith($authHeader, "Bearer ")) { if (startsWith($authHeader, "Bearer ")) {
$apiKey = substr($authHeader, strlen("Bearer ")); $apiKey = substr($authHeader, strlen("Bearer "));
$apiKeyAuthorized = $this->user->loadApiKey($apiKey); $apiKeyAuthorized = $this->context->loadApiKey($apiKey);
} }
} }
} }
// Logged in or api key authorized? // Logged in or api key authorized?
if ($this->loginRequired) { if ($this->loginRequired) {
if (!$this->user->isLoggedIn() && !$apiKeyAuthorized) { if (!$session && !$apiKeyAuthorized) {
$this->lastError = 'You are not logged in.'; $this->lastError = 'You are not logged in.';
http_response_code(401); http_response_code(401);
return false; return false;
} else if ($this->user->isLoggedIn()) { } else if ($session) {
$tfaToken = $this->user->getTwoFactorToken(); $tfaToken = $session->getUser()->getTwoFactorToken();
if ($tfaToken && $tfaToken->isConfirmed() && !$tfaToken->isAuthenticated()) { if ($tfaToken && $tfaToken->isConfirmed() && !$tfaToken->isAuthenticated()) {
$this->lastError = '2FA-Authorization is required'; $this->lastError = '2FA-Authorization is required';
http_response_code(401); http_response_code(401);
@ -211,11 +211,11 @@ abstract class Request {
} }
// CSRF Token // CSRF Token
if ($this->csrfTokenRequired && $this->user->isLoggedIn()) { if ($this->csrfTokenRequired && $session) {
// csrf token required + external call // csrf token required + external call
// if it's not a call with API_KEY, check for csrf_token // if it's not a call with API_KEY, check for csrf_token
$csrfToken = $values["csrf_token"] ?? $_SERVER["HTTP_XSRF_TOKEN"] ?? null; $csrfToken = $values["csrf_token"] ?? $_SERVER["HTTP_XSRF_TOKEN"] ?? null;
if (!$csrfToken || strcmp($csrfToken, $this->user->getSession()->getCsrfToken()) !== 0) { if (!$csrfToken || strcmp($csrfToken, $session->getCsrfToken()) !== 0) {
$this->lastError = "CSRF-Token mismatch"; $this->lastError = "CSRF-Token mismatch";
http_response_code(403); http_response_code(403);
return false; return false;
@ -224,7 +224,7 @@ abstract class Request {
// Check for permission // Check for permission
if (!($this instanceof \Api\Permission\Save)) { if (!($this instanceof \Api\Permission\Save)) {
$req = new \Api\Permission\Check($this->user); $req = new \Api\Permission\Check($this->context);
$this->success = $req->execute(array("method" => $this->getMethod())); $this->success = $req->execute(array("method" => $this->getMethod()));
$this->lastError = $req->getLastError(); $this->lastError = $req->getLastError();
if (!$this->success) { if (!$this->success) {
@ -241,8 +241,9 @@ abstract class Request {
$this->parseVariableParams($values); $this->parseVariableParams($values);
} }
if (!$this->user->getSQL()->isConnected()) { $sql = $this->context->getSQL();
$this->lastError = $this->user->getSQL()->getLastError(); if (!$sql->isConnected()) {
$this->lastError = $sql->getLastError();
return false; return false;
} }
@ -254,7 +255,7 @@ abstract class Request {
$this->success = $success; $this->success = $success;
} }
$this->user->getSQL()->setLastError(''); $sql->setLastError('');
return $this->success; return $this->success;
} }
@ -331,8 +332,8 @@ abstract class Request {
} }
protected function setupSSE() { protected function setupSSE() {
$this->user->getSQL()->close(); $this->context->sendCookies();
$this->user->sendCookies(); $this->context->getSQL()?->close();
set_time_limit(0); set_time_limit(0);
ignore_user_abort(true); ignore_user_abort(true);
header('Content-Type: text/event-stream'); header('Content-Type: text/event-stream');

@ -4,7 +4,7 @@ namespace Api {
use Api\Routes\GenerateCache; use Api\Routes\GenerateCache;
use Driver\SQL\Condition\Compare; use Driver\SQL\Condition\Compare;
use Objects\User; use Objects\Context;
abstract class RoutesAPI extends Request { abstract class RoutesAPI extends Request {
@ -13,16 +13,16 @@ namespace Api {
protected string $routerCachePath; protected string $routerCachePath;
public function __construct(User $user, bool $externalCall, array $params) { public function __construct(Context $context, bool $externalCall, array $params) {
parent::__construct($user, $externalCall, $params); parent::__construct($context, $externalCall, $params);
$this->routerCachePath = getClassPath(self::ROUTER_CACHE_CLASS); $this->routerCachePath = getClassPath(self::ROUTER_CACHE_CLASS);
} }
protected function routeExists($uid): bool { protected function routeExists($uid): bool {
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$res = $sql->select($sql->count()) $res = $sql->select($sql->count())
->from("Route") ->from("Route")
->where(new Compare("uid", $uid)) ->where(new Compare("id", $uid))
->execute(); ->execute();
$this->success = ($res !== false); $this->success = ($res !== false);
@ -41,10 +41,10 @@ namespace Api {
return false; return false;
} }
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$this->success = $sql->update("Route") $this->success = $sql->update("Route")
->set("active", $active) ->set("active", $active)
->where(new Compare("uid", $uid)) ->where(new Compare("id", $uid))
->execute(); ->execute();
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
@ -53,7 +53,7 @@ namespace Api {
} }
protected function regenerateCache(): bool { protected function regenerateCache(): bool {
$req = new GenerateCache($this->user); $req = new GenerateCache($this->context);
$this->success = $req->execute(); $this->success = $req->execute();
$this->lastError = $req->getLastError(); $this->lastError = $req->getLastError();
return $this->success; return $this->success;
@ -68,25 +68,25 @@ namespace Api\Routes {
use Api\RoutesAPI; use Api\RoutesAPI;
use Driver\SQL\Condition\Compare; use Driver\SQL\Condition\Compare;
use Driver\SQL\Condition\CondBool; use Driver\SQL\Condition\CondBool;
use Objects\Context;
use Objects\Router\DocumentRoute; use Objects\Router\DocumentRoute;
use Objects\Router\RedirectRoute; use Objects\Router\RedirectRoute;
use Objects\Router\Router; use Objects\Router\Router;
use Objects\Router\StaticFileRoute; use Objects\Router\StaticFileRoute;
use Objects\User;
class Fetch extends RoutesAPI { class Fetch extends RoutesAPI {
public function __construct($user, $externalCall = false) { public function __construct(Context $context, $externalCall = false) {
parent::__construct($user, $externalCall, array()); parent::__construct($context, $externalCall, array());
} }
public function _execute(): bool { public function _execute(): bool {
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$res = $sql $res = $sql
->select("uid", "request", "action", "target", "extra", "active", "exact") ->select("id", "request", "action", "target", "extra", "active", "exact")
->from("Route") ->from("Route")
->orderBy("uid") ->orderBy("id")
->ascending() ->ascending()
->execute(); ->execute();
@ -97,7 +97,7 @@ namespace Api\Routes {
$routes = array(); $routes = array();
foreach ($res as $row) { foreach ($res as $row) {
$routes[] = array( $routes[] = array(
"uid" => intval($row["uid"]), "id" => intval($row["id"]),
"request" => $row["request"], "request" => $row["request"],
"action" => $row["action"], "action" => $row["action"],
"target" => $row["target"], "target" => $row["target"],
@ -118,8 +118,8 @@ namespace Api\Routes {
private array $routes; private array $routes;
public function __construct($user, $externalCall = false) { public function __construct(Context $context, $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($context, $externalCall, array(
'routes' => new Parameter('routes', Parameter::TYPE_ARRAY, false) 'routes' => new Parameter('routes', Parameter::TYPE_ARRAY, false)
)); ));
} }
@ -129,7 +129,7 @@ namespace Api\Routes {
return false; return false;
} }
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
// DELETE old rules // DELETE old rules
$this->success = ($sql->truncate("Route")->execute() !== FALSE); $this->success = ($sql->truncate("Route")->execute() !== FALSE);
@ -210,8 +210,8 @@ namespace Api\Routes {
class Add extends RoutesAPI { class Add extends RoutesAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($context, $externalCall, array(
"request" => new StringType("request", 128), "request" => new StringType("request", 128),
"action" => new StringType("action"), "action" => new StringType("action"),
"target" => new StringType("target", 128), "target" => new StringType("target", 128),
@ -231,7 +231,7 @@ namespace Api\Routes {
return $this->createError("Invalid action: $action"); return $this->createError("Invalid action: $action");
} }
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$this->success = $sql->insert("Route", ["request", "action", "target", "extra"]) $this->success = $sql->insert("Route", ["request", "action", "target", "extra"])
->addRow($request, $action, $target, $extra) ->addRow($request, $action, $target, $extra)
->execute(); ->execute();
@ -243,9 +243,9 @@ namespace Api\Routes {
} }
class Update extends RoutesAPI { class Update extends RoutesAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($context, $externalCall, array(
"uid" => new Parameter("uid", Parameter::TYPE_INT), "id" => new Parameter("id", Parameter::TYPE_INT),
"request" => new StringType("request", 128), "request" => new StringType("request", 128),
"action" => new StringType("action"), "action" => new StringType("action"),
"target" => new StringType("target", 128), "target" => new StringType("target", 128),
@ -256,8 +256,8 @@ namespace Api\Routes {
public function _execute(): bool { public function _execute(): bool {
$uid = $this->getParam("uid"); $id = $this->getParam("id");
if (!$this->routeExists($uid)) { if (!$this->routeExists($id)) {
return false; return false;
} }
@ -269,13 +269,13 @@ namespace Api\Routes {
return $this->createError("Invalid action: $action"); return $this->createError("Invalid action: $action");
} }
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$this->success = $sql->update("Route") $this->success = $sql->update("Route")
->set("request", $request) ->set("request", $request)
->set("action", $action) ->set("action", $action)
->set("target", $target) ->set("target", $target)
->set("extra", $extra) ->set("extra", $extra)
->where(new Compare("uid", $uid)) ->where(new Compare("id", $id))
->execute(); ->execute();
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
@ -285,23 +285,23 @@ namespace Api\Routes {
} }
class Remove extends RoutesAPI { class Remove extends RoutesAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($context, $externalCall, array(
"uid" => new Parameter("uid", Parameter::TYPE_INT) "id" => new Parameter("id", Parameter::TYPE_INT)
)); ));
$this->isPublic = false; $this->isPublic = false;
} }
public function _execute(): bool { public function _execute(): bool {
$uid = $this->getParam("uid"); $uid = $this->getParam("id");
if (!$this->routeExists($uid)) { if (!$this->routeExists($uid)) {
return false; return false;
} }
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$this->success = $sql->delete("Route") $this->success = $sql->delete("Route")
->where(new Compare("uid", $uid)) ->where(new Compare("id", $uid))
->execute(); ->execute();
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
@ -311,29 +311,29 @@ namespace Api\Routes {
} }
class Enable extends RoutesAPI { class Enable extends RoutesAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($context, $externalCall, array(
"uid" => new Parameter("uid", Parameter::TYPE_INT) "id" => new Parameter("id", Parameter::TYPE_INT)
)); ));
$this->isPublic = false; $this->isPublic = false;
} }
public function _execute(): bool { public function _execute(): bool {
$uid = $this->getParam("uid"); $uid = $this->getParam("id");
return $this->toggleRoute($uid, true); return $this->toggleRoute($uid, true);
} }
} }
class Disable extends RoutesAPI { class Disable extends RoutesAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($context, $externalCall, array(
"uid" => new Parameter("uid", Parameter::TYPE_INT) "id" => new Parameter("id", Parameter::TYPE_INT)
)); ));
$this->isPublic = false; $this->isPublic = false;
} }
public function _execute(): bool { public function _execute(): bool {
$uid = $this->getParam("uid"); $uid = $this->getParam("id");
return $this->toggleRoute($uid, false); return $this->toggleRoute($uid, false);
} }
} }
@ -342,19 +342,19 @@ namespace Api\Routes {
private ?Router $router; private ?Router $router;
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, []); parent::__construct($context, $externalCall, []);
$this->isPublic = false; $this->isPublic = false;
$this->router = null; $this->router = null;
} }
protected function _execute(): bool { protected function _execute(): bool {
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$res = $sql $res = $sql
->select("uid", "request", "action", "target", "extra", "exact") ->select("id", "request", "action", "target", "extra", "exact")
->from("Route") ->from("Route")
->where(new CondBool("active")) ->where(new CondBool("active"))
->orderBy("uid")->ascending() ->orderBy("id")->ascending()
->execute(); ->execute();
$this->success = $res !== false; $this->success = $res !== false;
@ -363,7 +363,7 @@ namespace Api\Routes {
return false; return false;
} }
$this->router = new Router($this->user); $this->router = new Router($this->context);
foreach ($res as $row) { foreach ($res as $row) {
$request = $row["request"]; $request = $row["request"];
$target = $row["target"]; $target = $row["target"];

@ -2,10 +2,13 @@
namespace Api { namespace Api {
use Objects\Context;
abstract class SettingsAPI extends Request { abstract class SettingsAPI extends Request {
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
parent::__construct($context, $externalCall, $params);
}
} }
} }
namespace Api\Settings { namespace Api\Settings {
@ -19,19 +22,19 @@ namespace Api\Settings {
use Driver\SQL\Condition\CondNot; use Driver\SQL\Condition\CondNot;
use Driver\SQL\Condition\CondRegex; use Driver\SQL\Condition\CondRegex;
use Driver\SQL\Strategy\UpdateStrategy; use Driver\SQL\Strategy\UpdateStrategy;
use Objects\User; use Objects\Context;
class Get extends SettingsAPI { class Get extends SettingsAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($context, $externalCall, array(
'key' => new StringType('key', -1, true, NULL) 'key' => new StringType('key', -1, true, NULL)
)); ));
} }
public function _execute(): bool { public function _execute(): bool {
$key = $this->getParam("key"); $key = $this->getParam("key");
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$query = $sql->select("name", "value") ->from("Settings"); $query = $sql->select("name", "value") ->from("Settings");
@ -62,8 +65,8 @@ namespace Api\Settings {
} }
class Set extends SettingsAPI { class Set extends SettingsAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($context, $externalCall, array(
'settings' => new Parameter('settings', Parameter::TYPE_ARRAY) 'settings' => new Parameter('settings', Parameter::TYPE_ARRAY)
)); ));
} }
@ -77,7 +80,7 @@ namespace Api\Settings {
$paramKey = new StringType('key', 32); $paramKey = new StringType('key', 32);
$paramValue = new StringType('value', 1024, true, NULL); $paramValue = new StringType('value', 1024, true, NULL);
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$query = $sql->insert("Settings", array("name", "value")); $query = $sql->insert("Settings", array("name", "value"));
$keys = array(); $keys = array();
$deleteKeys = array(); $deleteKeys = array();
@ -129,7 +132,7 @@ namespace Api\Settings {
} }
private function checkReadonly(array $keys) { private function checkReadonly(array $keys) {
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$res = $sql->select("name") $res = $sql->select("name")
->from("Settings") ->from("Settings")
->where(new CondBool("readonly")) ->where(new CondBool("readonly"))
@ -148,7 +151,7 @@ namespace Api\Settings {
} }
private function deleteKeys(array $keys) { private function deleteKeys(array $keys) {
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$res = $sql->delete("Settings") $res = $sql->delete("Settings")
->where(new CondIn(new Column("name"), $keys)) ->where(new CondIn(new Column("name"), $keys))
->execute(); ->execute();

@ -5,18 +5,20 @@ namespace Api;
use DateTime; use DateTime;
use Driver\SQL\Condition\Compare; use Driver\SQL\Condition\Compare;
use Driver\SQL\Condition\CondBool; use Driver\SQL\Condition\CondBool;
use Objects\Context;
use Objects\DatabaseEntity\User;
class Stats extends Request { class Stats extends Request {
private bool $mailConfigured; private bool $mailConfigured;
private bool $recaptchaConfigured; private bool $recaptchaConfigured;
public function __construct($user, $externalCall = false) { public function __construct(Context $context, $externalCall = false) {
parent::__construct($user, $externalCall, array()); parent::__construct($context, $externalCall, array());
} }
private function getUserCount() { private function getUserCount() {
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$res = $sql->select($sql->count())->from("User")->execute(); $res = $sql->select($sql->count())->from("User")->execute();
$this->success = $this->success && ($res !== FALSE); $this->success = $this->success && ($res !== FALSE);
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
@ -25,7 +27,7 @@ class Stats extends Request {
} }
private function getPageCount() { private function getPageCount() {
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$res = $sql->select($sql->count())->from("Route") $res = $sql->select($sql->count())->from("Route")
->where(new CondBool("active")) ->where(new CondBool("active"))
->execute(); ->execute();
@ -36,7 +38,7 @@ class Stats extends Request {
} }
private function checkSettings(): bool { private function checkSettings(): bool {
$req = new \Api\Settings\Get($this->user); $req = new \Api\Settings\Get($this->context);
$this->success = $req->execute(array("key" => "^(mail_enabled|recaptcha_enabled)$")); $this->success = $req->execute(array("key" => "^(mail_enabled|recaptcha_enabled)$"));
$this->lastError = $req->getLastError(); $this->lastError = $req->getLastError();
@ -50,7 +52,7 @@ class Stats extends Request {
} }
private function getVisitorCount() { private function getVisitorCount() {
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$date = new DateTime(); $date = new DateTime();
$monthStart = $date->format("Ym00"); $monthStart = $date->format("Ym00");
$monthEnd = $date->modify("+1 month")->format("Ym00"); $monthEnd = $date->modify("+1 month")->format("Ym00");
@ -69,7 +71,7 @@ class Stats extends Request {
public function _execute(): bool { public function _execute(): bool {
$userCount = $this->getUserCount(); $userCount = $this->getUserCount();
$pageCount = $this->getPageCount(); $pageCount = $this->getPageCount();
$req = new \Api\Visitors\Stats($this->user); $req = new \Api\Visitors\Stats($this->context);
$this->success = $req->execute(array("type"=>"monthly")); $this->success = $req->execute(array("type"=>"monthly"));
$this->lastError = $req->getLastError(); $this->lastError = $req->getLastError();
if (!$this->success) { if (!$this->success) {
@ -100,7 +102,7 @@ class Stats extends Request {
"server" => $_SERVER["SERVER_SOFTWARE"] ?? "Unknown", "server" => $_SERVER["SERVER_SOFTWARE"] ?? "Unknown",
"memory_usage" => memory_get_usage(), "memory_usage" => memory_get_usage(),
"load_avg" => $loadAvg, "load_avg" => $loadAvg,
"database" => $this->user->getSQL()->getStatus(), "database" => $this->context->getSQL()->getStatus(),
"mail" => $this->mailConfigured, "mail" => $this->mailConfigured,
"reCaptcha" => $this->recaptchaConfigured "reCaptcha" => $this->recaptchaConfigured
); );

@ -3,12 +3,13 @@
namespace Api; namespace Api;
use Api\Parameter\StringType; use Api\Parameter\StringType;
use Objects\User; use Objects\Context;
use Objects\DatabaseEntity\User;
class Swagger extends Request { class Swagger extends Request {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, []); parent::__construct($context, $externalCall, []);
$this->csrfTokenRequired = false; $this->csrfTokenRequired = false;
} }
@ -61,7 +62,7 @@ class Swagger extends Request {
} }
private function fetchPermissions(): array { private function fetchPermissions(): array {
$req = new Permission\Fetch($this->user); $req = new Permission\Fetch($this->context);
$this->success = $req->execute(); $this->success = $req->execute();
$permissions = []; $permissions = [];
foreach( $req->getResult()["permissions"] as $permission) { foreach( $req->getResult()["permissions"] as $permission) {
@ -76,17 +77,19 @@ class Swagger extends Request {
return false; return false;
} }
if (($request->loginRequired() || !empty($requiredGroups)) && !$this->user->isLoggedIn()) { $currentUser = $this->context->getUser();
if (($request->loginRequired() || !empty($requiredGroups)) && !$currentUser) {
return false; return false;
} }
// special case: hardcoded permission // special case: hardcoded permission
if ($request instanceof Permission\Save && (!$this->user->isLoggedIn() || !$this->user->hasGroup(USER_GROUP_ADMIN))) { if ($request instanceof Permission\Save && (!$currentUser || !$currentUser->hasGroup(USER_GROUP_ADMIN))) {
return false; return false;
} }
if (!empty($requiredGroups)) { if (!empty($requiredGroups)) {
return !empty(array_intersect($requiredGroups, $this->user->getGroups())); $userGroups = array_keys($currentUser?->getGroups() ?? []);
return !empty(array_intersect($requiredGroups, $userGroups));
} }
return true; return true;
@ -94,7 +97,7 @@ class Swagger extends Request {
private function getDocumentation(): string { private function getDocumentation(): string {
$settings = $this->user->getConfiguration()->getSettings(); $settings = $this->context->getSettings();
$siteName = $settings->getSiteName(); $siteName = $settings->getSiteName();
$domain = parse_url($settings->getBaseUrl(), PHP_URL_HOST); $domain = parse_url($settings->getBaseUrl(), PHP_URL_HOST);
@ -105,7 +108,7 @@ class Swagger extends Request {
foreach (self::getApiEndpoints() as $endpoint => $apiClass) { foreach (self::getApiEndpoints() as $endpoint => $apiClass) {
$body = null; $body = null;
$requiredProperties = []; $requiredProperties = [];
$apiObject = $apiClass->newInstance($this->user, false); $apiObject = $apiClass->newInstance($this->context, false);
if (!$this->canView($permissions[strtolower($endpoint)] ?? [], $apiObject)) { if (!$this->canView($permissions[strtolower($endpoint)] ?? [], $apiObject)) {
continue; continue;
} }

@ -2,11 +2,11 @@
namespace Api { namespace Api {
use Objects\User; use Objects\Context;
abstract class TemplateAPI extends Request { abstract class TemplateAPI extends Request {
function __construct(User $user, bool $externalCall = false, array $params = array()) { function __construct(Context $context, bool $externalCall = false, array $params = array()) {
parent::__construct($user, $externalCall, $params); parent::__construct($context, $externalCall, $params);
$this->isPublic = false; // internal API $this->isPublic = false; // internal API
} }
} }
@ -19,7 +19,7 @@ namespace Api\Template {
use Api\Parameter\Parameter; use Api\Parameter\Parameter;
use Api\Parameter\StringType; use Api\Parameter\StringType;
use Api\TemplateAPI; use Api\TemplateAPI;
use Objects\User; use Objects\Context;
use Twig\Environment; use Twig\Environment;
use Twig\Error\LoaderError; use Twig\Error\LoaderError;
use Twig\Error\RuntimeError; use Twig\Error\RuntimeError;
@ -28,8 +28,8 @@ namespace Api\Template {
class Render extends TemplateAPI { class Render extends TemplateAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, [ parent::__construct($context, $externalCall, [
"file" => new StringType("file"), "file" => new StringType("file"),
"parameters" => new ArrayType("parameters", Parameter::TYPE_MIXED, false, true, []) "parameters" => new ArrayType("parameters", Parameter::TYPE_MIXED, false, true, [])
]); ]);

@ -2,22 +2,23 @@
namespace Api { namespace Api {
use Objects\Context;
use Objects\TwoFactor\AuthenticationData; use Objects\TwoFactor\AuthenticationData;
use Objects\TwoFactor\KeyBasedTwoFactorToken; use Objects\TwoFactor\KeyBasedTwoFactorToken;
use Objects\User;
abstract class TfaAPI extends Request { abstract class TfaAPI extends Request {
private bool $userVerficiationRequired; private bool $userVerificationRequired;
public function __construct(User $user, bool $externalCall = false, array $params = array()) { public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
parent::__construct($user, $externalCall, $params); parent::__construct($context, $externalCall, $params);
$this->loginRequired = true; $this->loginRequired = true;
$this->userVerficiationRequired = false; $this->apiKeyAllowed = false;
$this->userVerificationRequired = false;
} }
protected function verifyAuthData(AuthenticationData $authData): bool { protected function verifyAuthData(AuthenticationData $authData): bool {
$settings = $this->user->getConfiguration()->getSettings(); $settings = $this->context->getSettings();
// $relyingParty = $settings->getSiteName(); // $relyingParty = $settings->getSiteName();
$domain = parse_url($settings->getBaseUrl(), PHP_URL_HOST); $domain = parse_url($settings->getBaseUrl(), PHP_URL_HOST);
// $domain = "localhost"; // $domain = "localhost";
@ -26,7 +27,7 @@ namespace Api {
return $this->createError("mismatched rpIDHash. expected: " . hash("sha256", $domain) . " got: " . bin2hex($authData->getHash())); return $this->createError("mismatched rpIDHash. expected: " . hash("sha256", $domain) . " got: " . bin2hex($authData->getHash()));
} else if (!$authData->isUserPresent()) { } else if (!$authData->isUserPresent()) {
return $this->createError("No user present"); return $this->createError("No user present");
} else if ($this->userVerficiationRequired && !$authData->isUserVerified()) { } else if ($this->userVerificationRequired && !$authData->isUserVerified()) {
return $this->createError("user was not verified on device (PIN/Biometric/...)"); return $this->createError("user was not verified on device (PIN/Biometric/...)");
} else if ($authData->hasExtensionData()) { } else if ($authData->hasExtensionData()) {
return $this->createError("No extensions supported"); return $this->createError("No extensions supported");
@ -36,7 +37,7 @@ namespace Api {
} }
protected function verifyClientDataJSON($jsonData, KeyBasedTwoFactorToken $token): bool { protected function verifyClientDataJSON($jsonData, KeyBasedTwoFactorToken $token): bool {
$settings = $this->user->getConfiguration()->getSettings(); $settings = $this->context->getSettings();
$expectedType = $token->isConfirmed() ? "webauthn.get" : "webauthn.create"; $expectedType = $token->isConfirmed() ? "webauthn.get" : "webauthn.create";
$type = $jsonData["type"] ?? "null"; $type = $jsonData["type"] ?? "null";
if ($type !== $expectedType) { if ($type !== $expectedType) {
@ -58,33 +59,34 @@ namespace Api\TFA {
use Api\Parameter\StringType; use Api\Parameter\StringType;
use Api\TfaAPI; use Api\TfaAPI;
use Driver\SQL\Condition\Compare; use Driver\SQL\Condition\Compare;
use Objects\Context;
use Objects\TwoFactor\AttestationObject; use Objects\TwoFactor\AttestationObject;
use Objects\TwoFactor\AuthenticationData; use Objects\TwoFactor\AuthenticationData;
use Objects\TwoFactor\KeyBasedTwoFactorToken; use Objects\TwoFactor\KeyBasedTwoFactorToken;
use Objects\TwoFactor\TimeBasedTwoFactorToken; use Objects\TwoFactor\TimeBasedTwoFactorToken;
use Objects\User;
// General // General
class Remove extends TfaAPI { class Remove extends TfaAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, [ parent::__construct($context, $externalCall, [
"password" => new StringType("password", 0, true) "password" => new StringType("password", 0, true)
]); ]);
} }
public function _execute(): bool { public function _execute(): bool {
$token = $this->user->getTwoFactorToken(); $currentUser = $this->context->getUser();
$token = $currentUser->getTwoFactorToken();
if (!$token) { if (!$token) {
return $this->createError("You do not have an active 2FA-Token"); return $this->createError("You do not have an active 2FA-Token");
} }
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$password = $this->getParam("password"); $password = $this->getParam("password");
if ($password) { if ($password) {
$res = $sql->select("password") $res = $sql->select("password")
->from("User") ->from("User")
->where(new Compare("uid", $this->user->getId())) ->where(new Compare("id", $currentUser->getId()))
->execute(); ->execute();
$this->success = !empty($res); $this->success = !empty($res);
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
@ -99,7 +101,7 @@ namespace Api\TFA {
} }
$res = $sql->delete("2FA") $res = $sql->delete("2FA")
->where(new Compare("uid", $token->getId())) ->where(new Compare("id", $token->getId()))
->execute(); ->execute();
$this->success = $res !== false; $this->success = $res !== false;
@ -107,12 +109,12 @@ namespace Api\TFA {
if ($this->success && $token->isConfirmed()) { if ($this->success && $token->isConfirmed()) {
// send an email // send an email
$settings = $this->user->getConfiguration()->getSettings(); $settings = $this->context->getSettings();
$req = new \Api\Template\Render($this->user); $req = new \Api\Template\Render($this->context);
$this->success = $req->execute([ $this->success = $req->execute([
"file" => "mail/2fa_remove.twig", "file" => "mail/2fa_remove.twig",
"parameters" => [ "parameters" => [
"username" => $this->user->getFullName() ?? $this->user->getUsername(), "username" => $currentUser->getFullName() ?? $currentUser->getUsername(),
"site_name" => $settings->getSiteName(), "site_name" => $settings->getSiteName(),
"sender_mail" => $settings->getMailSender() "sender_mail" => $settings->getMailSender()
] ]
@ -120,13 +122,13 @@ namespace Api\TFA {
if ($this->success) { if ($this->success) {
$body = $req->getResult()["html"]; $body = $req->getResult()["html"];
$gpg = $this->user->getGPG(); $gpg = $currentUser->getGPG();
$req = new \Api\Mail\Send($this->user); $req = new \Api\Mail\Send($this->context);
$this->success = $req->execute([ $this->success = $req->execute([
"to" => $this->user->getEmail(), "to" => $currentUser->getEmail(),
"subject" => "[Security Lab] 2FA-Authentication removed", "subject" => "[Security Lab] 2FA-Authentication removed",
"body" => $body, "body" => $body,
"gpgFingerprint" => $gpg ? $gpg->getFingerprint() : null "gpgFingerprint" => $gpg?->getFingerprint()
]); ]);
} }
@ -140,27 +142,28 @@ namespace Api\TFA {
// TOTP // TOTP
class GenerateQR extends TfaAPI { class GenerateQR extends TfaAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall); parent::__construct($context, $externalCall);
$this->csrfTokenRequired = false; $this->csrfTokenRequired = false;
} }
public function _execute(): bool { public function _execute(): bool {
$twoFactorToken = $this->user->getTwoFactorToken(); $currentUser = $this->context->getUser();
$twoFactorToken = $currentUser->getTwoFactorToken();
if ($twoFactorToken && $twoFactorToken->isConfirmed()) { if ($twoFactorToken && $twoFactorToken->isConfirmed()) {
return $this->createError("You already added a two factor token"); return $this->createError("You already added a two factor token");
} else if (!($twoFactorToken instanceof TimeBasedTwoFactorToken)) { } else if (!($twoFactorToken instanceof TimeBasedTwoFactorToken)) {
$twoFactorToken = new TimeBasedTwoFactorToken(generateRandomString(32, "base32")); $twoFactorToken = new TimeBasedTwoFactorToken(generateRandomString(32, "base32"));
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$this->success = $sql->insert("2FA", ["type", "data"]) $this->success = $sql->insert("2FA", ["type", "data"])
->addRow("totp", $twoFactorToken->getData()) ->addRow("totp", $twoFactorToken->getData())
->returning("uid") ->returning("id")
->execute() !== false; ->execute() !== false;
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
if ($this->success) { if ($this->success) {
$this->success = $sql->update("User") $this->success = $sql->update("User")
->set("2fa_id", $sql->getLastInsertId())->where(new Compare("uid", $this->user->getId())) ->set("2fa_id", $sql->getLastInsertId())->where(new Compare("id", $currentUser->getId()))
->execute() !== false; ->execute() !== false;
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
} }
@ -172,27 +175,27 @@ namespace Api\TFA {
header("Content-Type: image/png"); header("Content-Type: image/png");
$this->disableCache(); $this->disableCache();
die($twoFactorToken->generateQRCode($this->user)); die($twoFactorToken->generateQRCode($this->context));
} }
} }
class ConfirmTotp extends VerifyTotp { class ConfirmTotp extends VerifyTotp {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall); parent::__construct($context, $externalCall);
$this->loginRequired = true;
} }
public function _execute(): bool { public function _execute(): bool {
$twoFactorToken = $this->user->getTwoFactorToken(); $currentUser = $this->context->getUser();
$twoFactorToken = $currentUser->getTwoFactorToken();
if ($twoFactorToken->isConfirmed()) { if ($twoFactorToken->isConfirmed()) {
return $this->createError("Your two factor token is already confirmed."); return $this->createError("Your two factor token is already confirmed.");
} }
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$this->success = $sql->update("2FA") $this->success = $sql->update("2FA")
->set("confirmed", true) ->set("confirmed", true)
->where(new Compare("uid", $twoFactorToken->getId())) ->where(new Compare("id", $twoFactorToken->getId()))
->execute() !== false; ->execute() !== false;
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
return $this->success; return $this->success;
@ -201,22 +204,22 @@ namespace Api\TFA {
class VerifyTotp extends TfaAPI { class VerifyTotp extends TfaAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, [ parent::__construct($context, $externalCall, [
"code" => new StringType("code", 6) "code" => new StringType("code", 6)
]); ]);
$this->loginRequired = false; $this->loginRequired = true;
$this->csrfTokenRequired = false; $this->csrfTokenRequired = false;
} }
public function _execute(): bool { public function _execute(): bool {
$session = $this->user->getSession(); $currentUser = $this->context->getUser();
if (!$session) { if (!$currentUser) {
return $this->createError("You are not logged in."); return $this->createError("You are not logged in.");
} }
$twoFactorToken = $this->user->getTwoFactorToken(); $twoFactorToken = $currentUser->getTwoFactorToken();
if (!$twoFactorToken) { if (!$twoFactorToken) {
return $this->createError("You did not add a two factor token yet."); return $this->createError("You did not add a two factor token yet.");
} else if (!($twoFactorToken instanceof TimeBasedTwoFactorToken)) { } else if (!($twoFactorToken instanceof TimeBasedTwoFactorToken)) {
@ -235,21 +238,23 @@ namespace Api\TFA {
// Key // Key
class RegisterKey extends TfaAPI { class RegisterKey extends TfaAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, [ parent::__construct($context, $externalCall, [
"clientDataJSON" => new StringType("clientDataJSON", 0, true, "{}"), "clientDataJSON" => new StringType("clientDataJSON", 0, true, "{}"),
"attestationObject" => new StringType("attestationObject", 0, true, "") "attestationObject" => new StringType("attestationObject", 0, true, "")
]); ]);
$this->loginRequired = true;
} }
public function _execute(): bool { public function _execute(): bool {
$currentUser = $this->context->getUser();
$clientDataJSON = json_decode($this->getParam("clientDataJSON"), true); $clientDataJSON = json_decode($this->getParam("clientDataJSON"), true);
$attestationObjectRaw = base64_decode($this->getParam("attestationObject")); $attestationObjectRaw = base64_decode($this->getParam("attestationObject"));
$twoFactorToken = $this->user->getTwoFactorToken(); $twoFactorToken = $currentUser->getTwoFactorToken();
$settings = $this->user->getConfiguration()->getSettings(); $settings = $this->context->getSettings();
$relyingParty = $settings->getSiteName(); $relyingParty = $settings->getSiteName();
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
// TODO: for react development, localhost / HTTP_HOST is required, otherwise a DOMException is thrown // TODO: for react development, localhost / HTTP_HOST is required, otherwise a DOMException is thrown
$domain = parse_url($settings->getBaseUrl(), PHP_URL_HOST); $domain = parse_url($settings->getBaseUrl(), PHP_URL_HOST);
@ -266,7 +271,7 @@ namespace Api\TFA {
$challenge = base64_encode(generateRandomString(32, "raw")); $challenge = base64_encode(generateRandomString(32, "raw"));
$res = $sql->insert("2FA", ["type", "data"]) $res = $sql->insert("2FA", ["type", "data"])
->addRow("fido", $challenge) ->addRow("fido", $challenge)
->returning("uid") ->returning("id")
->execute(); ->execute();
$this->success = ($res !== false); $this->success = ($res !== false);
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
@ -276,7 +281,7 @@ namespace Api\TFA {
$this->success = $sql->update("User") $this->success = $sql->update("User")
->set("2fa_id", $sql->getLastInsertId()) ->set("2fa_id", $sql->getLastInsertId())
->where(new Compare("uid", $this->user->getId())) ->where(new Compare("id", $currentUser->getId()))
->execute() !== false; ->execute() !== false;
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
if (!$this->success) { if (!$this->success) {
@ -286,7 +291,7 @@ namespace Api\TFA {
$this->result["data"] = [ $this->result["data"] = [
"challenge" => $challenge, "challenge" => $challenge,
"id" => $this->user->getId() . "@" . $domain, // <userId>@<domain> "id" => $currentUser->getId() . "@" . $domain, // <userId>@<domain>
"relyingParty" => [ "relyingParty" => [
"name" => $relyingParty, "name" => $relyingParty,
"id" => $domain "id" => $domain
@ -322,7 +327,7 @@ namespace Api\TFA {
$this->success = $sql->update("2FA") $this->success = $sql->update("2FA")
->set("data", json_encode($data)) ->set("data", json_encode($data))
->set("confirmed", true) ->set("confirmed", true)
->where(new Compare("uid", $twoFactorToken->getId())) ->where(new Compare("id", $twoFactorToken->getId()))
->execute() !== false; ->execute() !== false;
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
} }
@ -332,25 +337,25 @@ namespace Api\TFA {
} }
class VerifyKey extends TfaAPI { class VerifyKey extends TfaAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, [ parent::__construct($context, $externalCall, [
"credentialID" => new StringType("credentialID"), "credentialID" => new StringType("credentialID"),
"clientDataJSON" => new StringType("clientDataJSON"), "clientDataJSON" => new StringType("clientDataJSON"),
"authData" => new StringType("authData"), "authData" => new StringType("authData"),
"signature" => new StringType("signature"), "signature" => new StringType("signature"),
]); ]);
$this->loginRequired = false; $this->loginRequired = true;
$this->csrfTokenRequired = false; $this->csrfTokenRequired = false;
} }
public function _execute(): bool { public function _execute(): bool {
$session = $this->user->getSession(); $currentUser = $this->context->getUser();
if (!$session) { if (!$currentUser) {
return $this->createError("You are not logged in."); return $this->createError("You are not logged in.");
} }
$twoFactorToken = $this->user->getTwoFactorToken(); $twoFactorToken = $currentUser->getTwoFactorToken();
if (!$twoFactorToken) { if (!$twoFactorToken) {
return $this->createError("You did not add a two factor token yet."); return $this->createError("You did not add a two factor token yet.");
} else if (!($twoFactorToken instanceof KeyBasedTwoFactorToken)) { } else if (!($twoFactorToken instanceof KeyBasedTwoFactorToken)) {

File diff suppressed because it is too large Load Diff

@ -3,12 +3,12 @@
namespace Api; namespace Api;
use Api\Parameter\StringType; use Api\Parameter\StringType;
use Objects\User; use Objects\Context;
class VerifyCaptcha extends Request { class VerifyCaptcha extends Request {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($context, $externalCall, array(
"captcha" => new StringType("captcha"), "captcha" => new StringType("captcha"),
"action" => new StringType("action"), "action" => new StringType("action"),
)); ));
@ -17,7 +17,7 @@ class VerifyCaptcha extends Request {
} }
public function _execute(): bool { public function _execute(): bool {
$settings = $this->user->getConfiguration()->getSettings(); $settings = $this->context->getSettings();
if (!$settings->isRecaptchaEnabled()) { if (!$settings->isRecaptchaEnabled()) {
return $this->createError("Google reCaptcha is not enabled."); return $this->createError("Google reCaptcha is not enabled.");
} }

@ -2,11 +2,11 @@
namespace Api { namespace Api {
use Objects\User; use Objects\Context;
abstract class VisitorsAPI extends Request { abstract class VisitorsAPI extends Request {
public function __construct(User $user, bool $externalCall = false, array $params = []) { public function __construct(Context $context, bool $externalCall = false, array $params = []) {
parent::__construct($user, $externalCall, $params); parent::__construct($context, $externalCall, $params);
} }
} }
} }
@ -21,18 +21,18 @@ namespace Api\Visitors {
use Driver\SQL\Expression\Add; use Driver\SQL\Expression\Add;
use Driver\SQL\Query\Select; use Driver\SQL\Query\Select;
use Driver\SQL\Strategy\UpdateStrategy; use Driver\SQL\Strategy\UpdateStrategy;
use Objects\User; use Objects\Context;
class ProcessVisit extends VisitorsAPI { class ProcessVisit extends VisitorsAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($context, $externalCall, array(
"cookie" => new StringType("cookie") "cookie" => new StringType("cookie")
)); ));
$this->isPublic = false; $this->isPublic = false;
} }
public function _execute(): bool { public function _execute(): bool {
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$cookie = $this->getParam("cookie"); $cookie = $this->getParam("cookie");
$day = (new DateTime())->format("Ymd"); $day = (new DateTime())->format("Ymd");
$sql->insert("Visitor", array("cookie", "day")) $sql->insert("Visitor", array("cookie", "day"))
@ -47,8 +47,8 @@ namespace Api\Visitors {
} }
class Stats extends VisitorsAPI { class Stats extends VisitorsAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, array( parent::__construct($context, $externalCall, array(
'type' => new StringType('type', 32), 'type' => new StringType('type', 32),
'date' => new Parameter('date', Parameter::TYPE_DATE, true, new DateTime()) 'date' => new Parameter('date', Parameter::TYPE_DATE, true, new DateTime())
)); ));
@ -81,7 +81,7 @@ namespace Api\Visitors {
$date = $this->getParam("date"); $date = $this->getParam("date");
$type = $this->getParam("type"); $type = $this->getParam("type");
$sql = $this->user->getSQL(); $sql = $this->context->getSQL();
$query = $sql->select($sql->count(), "day") $query = $sql->select($sql->count(), "day")
->from("Visitor") ->from("Visitor")
->where(new Compare("count", 1, ">")) ->where(new Compare("count", 1, ">"))

@ -5,90 +5,27 @@ namespace Configuration;
use Driver\SQL\SQL; use Driver\SQL\SQL;
use \Driver\SQL\Strategy\SetNullStrategy; use \Driver\SQL\Strategy\SetNullStrategy;
use \Driver\SQL\Strategy\CascadeStrategy; use \Driver\SQL\Strategy\CascadeStrategy;
use Objects\DatabaseEntity\DatabaseEntity;
use PHPUnit\Util\Exception;
class CreateDatabase extends DatabaseScript { class CreateDatabase extends DatabaseScript {
public static function createQueries(SQL $sql): array { public static function createQueries(SQL $sql): array {
$queries = array(); $queries = array();
// Language self::loadEntities($queries, $sql);
$queries[] = $sql->createTable("Language")
->addSerial("uid")
->addString("code", 5)
->addString("name", 32)
->primaryKey("uid")
->unique("code")
->unique("name");
$queries[] = $sql->insert("Language", array("code", "name")) $queries[] = $sql->insert("Language", array("code", "name"))
->addRow("en_US", 'American English') ->addRow("en_US", 'American English')
->addRow("de_DE", 'Deutsch Standard'); ->addRow("de_DE", 'Deutsch Standard');
$queries[] = $sql->createTable("GpgKey")
->addSerial("uid")
->addString("fingerprint", 64)
->addDateTime("added", false, $sql->now())
->addDateTime("expires")
->addBool("confirmed")
->addString("algorithm", 32)
->primaryKey("uid");
$queries[] = $sql->createTable("2FA")
->addSerial("uid")
->addEnum("type", ["totp","fido"])
->addString("data", 512) // either totp secret, fido challenge or fido public key information
->addBool("confirmed", false)
->addDateTime("added", false, $sql->now())
->primaryKey("uid");
$queries[] = $sql->createTable("User")
->addSerial("uid")
->addString("email", 64, true)
->addString("name", 32)
->addString("password", 128)
->addString("fullName", 64, false, "")
->addString("profilePicture", 64, true)
->addDateTime("last_online", true, NULL)
->addBool("confirmed", false)
->addInt("language_id", true, 1)
->addInt("gpg_id", true)
->addInt("2fa_id", true)
->addDateTime("registered_at", false, $sql->currentTimestamp())
->primaryKey("uid")
->unique("email")
->unique("name")
->foreignKey("language_id", "Language", "uid", new SetNullStrategy())
->foreignKey("gpg_id", "GpgKey", "uid", new SetNullStrategy())
->foreignKey("2fa_id", "2FA", "uid", new SetNullStrategy());
$queries[] = $sql->createTable("Session")
->addSerial("uid")
->addBool("active", true)
->addDateTime("expires")
->addInt("user_id")
->addString("ipAddress", 45)
->addString("os", 64)
->addString("browser", 64)
->addJson("data", false, '{}')
->addBool("stay_logged_in", true)
->addString("csrf_token", 16)
->primaryKey("uid", "user_id")
->foreignKey("user_id", "User", "uid", new CascadeStrategy());
$queries[] = $sql->createTable("UserToken") $queries[] = $sql->createTable("UserToken")
->addInt("user_id") ->addInt("user_id")
->addString("token", 36) ->addString("token", 36)
->addEnum("token_type", array("password_reset", "email_confirm", "invite", "gpg_confirm")) ->addEnum("token_type", array("password_reset", "email_confirm", "invite", "gpg_confirm"))
->addDateTime("valid_until") ->addDateTime("valid_until")
->addBool("used", false) ->addBool("used", false)
->foreignKey("user_id", "User", "uid", new CascadeStrategy()); ->foreignKey("user_id", "User", "id", new CascadeStrategy());
$queries[] = $sql->createTable("Group")
->addSerial("uid")
->addString("name", 32)
->addString("color", 10)
->primaryKey("uid")
->unique("name");
$queries[] = $sql->insert("Group", array("name", "color")) $queries[] = $sql->insert("Group", array("name", "color"))
->addRow(USER_GROUP_MODERATOR_NAME, "#007bff") ->addRow(USER_GROUP_MODERATOR_NAME, "#007bff")
@ -99,42 +36,25 @@ class CreateDatabase extends DatabaseScript {
->addInt("user_id") ->addInt("user_id")
->addInt("group_id") ->addInt("group_id")
->unique("user_id", "group_id") ->unique("user_id", "group_id")
->foreignKey("user_id", "User", "uid", new CascadeStrategy()) ->foreignKey("user_id", "User", "id", new CascadeStrategy())
->foreignKey("group_id", "Group", "uid", new CascadeStrategy()); ->foreignKey("group_id", "Group", "id", new CascadeStrategy());
$queries[] = $sql->createTable("Notification")
->addSerial("uid")
->addEnum("type", array("default", "message", "warning"), false, "default")
->addDateTime("created_at", false, $sql->currentTimestamp())
->addString("title", 32)
->addString("message", 256)
->primaryKey("uid");
$queries[] = $sql->createTable("UserNotification") $queries[] = $sql->createTable("UserNotification")
->addInt("user_id") ->addInt("user_id")
->addInt("notification_id") ->addInt("notification_id")
->addBool("seen", false) ->addBool("seen", false)
->foreignKey("user_id", "User", "uid") ->foreignKey("user_id", "User", "id")
->foreignKey("notification_id", "Notification", "uid") ->foreignKey("notification_id", "Notification", "id")
->unique("user_id", "notification_id"); ->unique("user_id", "notification_id");
$queries[] = $sql->createTable("GroupNotification") $queries[] = $sql->createTable("GroupNotification")
->addInt("group_id") ->addInt("group_id")
->addInt("notification_id") ->addInt("notification_id")
->addBool("seen", false) ->addBool("seen", false)
->foreignKey("group_id", "Group", "uid") ->foreignKey("group_id", "Group", "id")
->foreignKey("notification_id", "Notification", "uid") ->foreignKey("notification_id", "Notification", "id")
->unique("group_id", "notification_id"); ->unique("group_id", "notification_id");
$queries[] = $sql->createTable("ApiKey")
->addSerial("uid")
->addInt("user_id")
->addBool("active", true)
->addString("api_key", 64)
->addDateTime("valid_until")
->primaryKey("uid")
->foreignKey("user_id", "User", "uid");
$queries[] = $sql->createTable("Visitor") $queries[] = $sql->createTable("Visitor")
->addInt("day") ->addInt("day")
->addInt("count", false, 1) ->addInt("count", false, 1)
@ -142,14 +62,14 @@ class CreateDatabase extends DatabaseScript {
->unique("day", "cookie"); ->unique("day", "cookie");
$queries[] = $sql->createTable("Route") $queries[] = $sql->createTable("Route")
->addSerial("uid") ->addSerial("id")
->addString("request", 128) ->addString("request", 128)
->addEnum("action", array("redirect_temporary", "redirect_permanently", "static", "dynamic")) ->addEnum("action", array("redirect_temporary", "redirect_permanently", "static", "dynamic"))
->addString("target", 128) ->addString("target", 128)
->addString("extra", 64, true) ->addString("extra", 64, true)
->addBool("active", true) ->addBool("active", true)
->addBool("exact", true) ->addBool("exact", true)
->primaryKey("uid") ->primaryKey("id")
->unique("request"); ->unique("request");
$queries[] = $sql->insert("Route", ["request", "action", "target", "extra", "exact"]) $queries[] = $sql->insert("Route", ["request", "action", "target", "extra", "exact"])
@ -184,17 +104,17 @@ class CreateDatabase extends DatabaseScript {
$queries[] = $settingsQuery; $queries[] = $settingsQuery;
$queries[] = $sql->createTable("ContactRequest") $queries[] = $sql->createTable("ContactRequest")
->addSerial("uid") ->addSerial("id")
->addString("from_name", 32) ->addString("from_name", 32)
->addString("from_email", 64) ->addString("from_email", 64)
->addString("message", 512) ->addString("message", 512)
->addString("messageId", 78, true) # null = don't sync with mails (usually if mail could not be sent) ->addString("messageId", 78, true) # null = don't sync with mails (usually if mail could not be sent)
->addDateTime("created_at", false, $sql->currentTimestamp()) ->addDateTime("created_at", false, $sql->currentTimestamp())
->unique("messageId") ->unique("messageId")
->primaryKey("uid"); ->primaryKey("id");
$queries[] = $sql->createTable("ContactMessage") $queries[] = $sql->createTable("ContactMessage")
->addSerial("uid") ->addSerial("id")
->addInt("request_id") ->addInt("request_id")
->addInt("user_id", true) # null = customer has sent this message ->addInt("user_id", true) # null = customer has sent this message
->addString("message", 512) ->addString("message", 512)
@ -202,9 +122,9 @@ class CreateDatabase extends DatabaseScript {
->addDateTime("created_at", false, $sql->currentTimestamp()) ->addDateTime("created_at", false, $sql->currentTimestamp())
->addBool("read", false) ->addBool("read", false)
->unique("messageId") ->unique("messageId")
->primaryKey("uid") ->primaryKey("id")
->foreignKey("request_id", "ContactRequest", "uid", new CascadeStrategy()) ->foreignKey("request_id", "ContactRequest", "id", new CascadeStrategy())
->foreignKey("user_id", "User", "uid", new SetNullStrategy()); ->foreignKey("user_id", "User", "id", new SetNullStrategy());
$queries[] = $sql->createTable("ApiPermission") $queries[] = $sql->createTable("ApiPermission")
->addString("method", 32) ->addString("method", 32)
@ -213,7 +133,7 @@ class CreateDatabase extends DatabaseScript {
->primaryKey("method"); ->primaryKey("method");
$queries[] = $sql->createTable("MailQueue") $queries[] = $sql->createTable("MailQueue")
->addSerial("uid") ->addSerial("id")
->addString("from", 64) ->addString("from", 64)
->addString("to", 64) ->addString("to", 64)
->addString("subject") ->addString("subject")
@ -225,18 +145,9 @@ class CreateDatabase extends DatabaseScript {
->addInt("retryCount", false, 5) ->addInt("retryCount", false, 5)
->addDateTime("nextTry", false, $sql->now()) ->addDateTime("nextTry", false, $sql->now())
->addString("errorMessage", NULL, true) ->addString("errorMessage", NULL, true)
->primaryKey("uid"); ->primaryKey("id");
$queries = array_merge($queries, \Configuration\Patch\EntityLog_2021_04_08::createTableLog($sql, "MailQueue", 30)); $queries = array_merge($queries, \Configuration\Patch\EntityLog_2021_04_08::createTableLog($sql, "MailQueue", 30));
$queries[] = $sql->createTable("News")
->addSerial("uid")
->addInt("publishedBy")
->addDateTime("publishedAt", false, $sql->now())
->addString("title", 128)
->addString("text", 1024)
->foreignKey("publishedBy", "User", "uid", new CascadeStrategy())
->primaryKey("uid");
$queries[] = $sql->insert("ApiPermission", array("method", "groups", "description")) $queries[] = $sql->insert("ApiPermission", array("method", "groups", "description"))
->addRow("ApiKey/create", array(), "Allows users to create API-Keys for themselves") ->addRow("ApiKey/create", array(), "Allows users to create API-Keys for themselves")
->addRow("ApiKey/fetch", array(), "Allows users to list their API-Keys") ->addRow("ApiKey/fetch", array(), "Allows users to list their API-Keys")
@ -265,7 +176,6 @@ class CreateDatabase extends DatabaseScript {
->addRow("Contact/get", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT), "Allows users to see messages within a contact request"); ->addRow("Contact/get", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT), "Allows users to see messages within a contact request");
self::loadPatches($queries, $sql); self::loadPatches($queries, $sql);
self::loadEntities($queries, $sql);
return $queries; return $queries;
} }
@ -293,18 +203,47 @@ class CreateDatabase extends DatabaseScript {
if (file_exists($entityDirectory) && is_dir($entityDirectory)) { if (file_exists($entityDirectory) && is_dir($entityDirectory)) {
$scan_arr = scandir($entityDirectory); $scan_arr = scandir($entityDirectory);
$files_arr = array_diff($scan_arr, array('.', '..')); $files_arr = array_diff($scan_arr, array('.', '..'));
$handlers = [];
foreach ($files_arr as $file) { foreach ($files_arr as $file) {
$suffix = ".class.php"; $suffix = ".class.php";
if (endsWith($file, $suffix)) { if (endsWith($file, $suffix)) {
$className = substr($file, 0, strlen($file) - strlen($suffix)); $className = substr($file, 0, strlen($file) - strlen($suffix));
if (!in_array($className, ["DatabaseEntity", "DatabaseEntityHandler"])) { if (!in_array($className, ["DatabaseEntity", "DatabaseEntityQuery", "DatabaseEntityHandler"])) {
$className = "\\Objects\\DatabaseEntity\\$className"; $className = "\\Objects\\DatabaseEntity\\$className";
$reflectionClass = new \ReflectionClass($className);
if ($reflectionClass->isSubclassOf(DatabaseEntity::class)) {
$method = "$className::getHandler"; $method = "$className::getHandler";
$handler = call_user_func($method, $sql); $handler = call_user_func($method, $sql);
$queries[] = $handler->getTableQuery(); $handlers[$handler->getTableName()] = $handler;
} }
} }
} }
} }
$tableCount = count($handlers);
$createdTables = [];
while (!empty($handlers)) {
$prevCount = $tableCount;
$unmetDependenciesTotal = [];
foreach ($handlers as $tableName => $handler) {
$dependsOn = $handler->dependsOn();
$unmetDependencies = array_diff($dependsOn, $createdTables);
if (empty($unmetDependencies)) {
$queries[] = $handler->getTableQuery();
$createdTables[] = $tableName;
unset($handlers[$tableName]);
} else {
$unmetDependenciesTotal = array_merge($unmetDependenciesTotal, $unmetDependencies);
}
}
$tableCount = count($handlers);
if ($tableCount === $prevCount) {
throw new Exception("Circular or unmet table dependency detected. Unmet dependencies: "
. implode(", ", $unmetDependenciesTotal));
}
}
}
} }
} }

@ -19,7 +19,7 @@ class EntityLog_2021_04_08 extends DatabaseScript {
->after()->insert($table) ->after()->insert($table)
->exec(new CreateProcedure($sql, "InsertEntityLog"), [ ->exec(new CreateProcedure($sql, "InsertEntityLog"), [
"tableName" => new CurrentTable(), "tableName" => new CurrentTable(),
"entityId" => new CurrentColumn("uid"), "entityId" => new CurrentColumn("id"),
"lifetime" => $lifetime, "lifetime" => $lifetime,
]), ]),
@ -27,14 +27,14 @@ class EntityLog_2021_04_08 extends DatabaseScript {
->after()->update($table) ->after()->update($table)
->exec(new CreateProcedure($sql, "UpdateEntityLog"), [ ->exec(new CreateProcedure($sql, "UpdateEntityLog"), [
"tableName" => new CurrentTable(), "tableName" => new CurrentTable(),
"entityId" => new CurrentColumn("uid"), "entityId" => new CurrentColumn("id"),
]), ]),
$sql->createTrigger("${table}_trg_delete") $sql->createTrigger("${table}_trg_delete")
->after()->delete($table) ->after()->delete($table)
->exec(new CreateProcedure($sql, "DeleteEntityLog"), [ ->exec(new CreateProcedure($sql, "DeleteEntityLog"), [
"tableName" => new CurrentTable(), "tableName" => new CurrentTable(),
"entityId" => new CurrentColumn("uid"), "entityId" => new CurrentColumn("id"),
]) ])
]; ];
} }
@ -51,32 +51,32 @@ class EntityLog_2021_04_08 extends DatabaseScript {
$insertProcedure = $sql->createProcedure("InsertEntityLog") $insertProcedure = $sql->createProcedure("InsertEntityLog")
->param(new CurrentTable()) ->param(new CurrentTable())
->param(new IntColumn("uid")) ->param(new IntColumn("id"))
->param(new IntColumn("lifetime", false, 90)) ->param(new IntColumn("lifetime", false, 90))
->returns(new Trigger()) ->returns(new Trigger())
->exec(array( ->exec(array(
$sql->insert("EntityLog", ["entityId", "tableName", "lifetime"]) $sql->insert("EntityLog", ["entityId", "tableName", "lifetime"])
->addRow(new CurrentColumn("uid"), new CurrentTable(), new CurrentColumn("lifetime")) ->addRow(new CurrentColumn("id"), new CurrentTable(), new CurrentColumn("lifetime"))
)); ));
$updateProcedure = $sql->createProcedure("UpdateEntityLog") $updateProcedure = $sql->createProcedure("UpdateEntityLog")
->param(new CurrentTable()) ->param(new CurrentTable())
->param(new IntColumn("uid")) ->param(new IntColumn("id"))
->returns(new Trigger()) ->returns(new Trigger())
->exec(array( ->exec(array(
$sql->update("EntityLog") $sql->update("EntityLog")
->set("modified", $sql->now()) ->set("modified", $sql->now())
->where(new Compare("entityId", new CurrentColumn("uid"))) ->where(new Compare("entityId", new CurrentColumn("id")))
->where(new Compare("tableName", new CurrentTable())) ->where(new Compare("tableName", new CurrentTable()))
)); ));
$deleteProcedure = $sql->createProcedure("DeleteEntityLog") $deleteProcedure = $sql->createProcedure("DeleteEntityLog")
->param(new CurrentTable()) ->param(new CurrentTable())
->param(new IntColumn("uid")) ->param(new IntColumn("id"))
->returns(new Trigger()) ->returns(new Trigger())
->exec(array( ->exec(array(
$sql->delete("EntityLog") $sql->delete("EntityLog")
->where(new Compare("entityId", new CurrentColumn("uid"))) ->where(new Compare("entityId", new CurrentColumn("id")))
->where(new Compare("tableName", new CurrentTable())) ->where(new Compare("tableName", new CurrentTable()))
)); ));

@ -9,14 +9,6 @@ class SystemLog_2022_03_30 extends DatabaseScript {
public static function createQueries(SQL $sql): array { public static function createQueries(SQL $sql): array {
return [ return [
$sql->createTable("SystemLog")
->onlyIfNotExists()
->addSerial("id")
->addDateTime("timestamp", false, $sql->now())
->addString("message")
->addString("module", 64, false, "global")
->addEnum("severity", ["debug", "info", "warning", "error", "severe"])
->primaryKey("id"),
$sql->insert("ApiPermission", ["method", "groups", "description"]) $sql->insert("ApiPermission", ["method", "groups", "description"])
->addRow("Logs/get", [USER_GROUP_ADMIN], "Allows users to fetch system logs") ->addRow("Logs/get", [USER_GROUP_ADMIN], "Allows users to fetch system logs")
]; ];

@ -7,7 +7,7 @@
namespace Configuration; namespace Configuration;
use Driver\SQL\Query\Insert; use Driver\SQL\Query\Insert;
use Objects\User; use Objects\Context;
class Settings { class Settings {
@ -58,8 +58,8 @@ class Settings {
return $settings; return $settings;
} }
public function loadFromDatabase(User $user): bool { public function loadFromDatabase(Context $context): bool {
$req = new \Api\Settings\Get($user); $req = new \Api\Settings\Get($context);
$success = $req->execute(); $success = $req->execute();
if ($success) { if ($success) {
@ -78,7 +78,7 @@ class Settings {
$this->allowedExtensions = explode(",", $result["allowed_extensions"] ?? strtolower(implode(",", $this->allowedExtensions))); $this->allowedExtensions = explode(",", $result["allowed_extensions"] ?? strtolower(implode(",", $this->allowedExtensions)));
if (!isset($result["jwt_secret"])) { if (!isset($result["jwt_secret"])) {
$req = new \Api\Settings\Set($user); $req = new \Api\Settings\Set($context);
$req->execute(array("settings" => array( $req->execute(array("settings" => array(
"jwt_secret" => $this->jwtSecret "jwt_secret" => $this->jwtSecret
))); )));
@ -135,4 +135,8 @@ class Settings {
public function isExtensionAllowed(string $ext): bool { public function isExtensionAllowed(string $ext): bool {
return empty($this->allowedExtensions) || in_array(strtolower(trim($ext)), $this->allowedExtensions); return empty($this->allowedExtensions) || in_array(strtolower(trim($ext)), $this->allowedExtensions);
} }
public function getDomain(): string {
return parse_url($this->getBaseUrl(), PHP_URL_HOST);
}
} }

@ -23,7 +23,7 @@ class Account extends TemplateDocument {
if ($this->getTemplateName() === "account/reset_password.twig") { if ($this->getTemplateName() === "account/reset_password.twig") {
if (isset($_GET["token"]) && is_string($_GET["token"]) && !empty($_GET["token"])) { if (isset($_GET["token"]) && is_string($_GET["token"]) && !empty($_GET["token"])) {
$this->parameters["view"]["token"] = $_GET["token"]; $this->parameters["view"]["token"] = $_GET["token"];
$req = new \Api\User\CheckToken($this->getUser()); $req = new \Api\User\CheckToken($this->getContext());
$this->parameters["view"]["success"] = $req->execute(array("token" => $_GET["token"])); $this->parameters["view"]["success"] = $req->execute(array("token" => $_GET["token"]));
if ($this->parameters["view"]["success"]) { if ($this->parameters["view"]["success"]) {
if (strcmp($req->getResult()["token"]["type"], "password_reset") !== 0) { if (strcmp($req->getResult()["token"]["type"], "password_reset") !== 0) {
@ -35,18 +35,18 @@ class Account extends TemplateDocument {
} }
} else if ($this->getTemplateName() === "account/register.twig") { } else if ($this->getTemplateName() === "account/register.twig") {
$settings = $this->getSettings(); $settings = $this->getSettings();
if ($this->getUser()->isLoggedIn()) { if ($this->getUser()) {
$this->createError("You are already logged in."); $this->createError("You are already logged in.");
} else if (!$settings->isRegistrationAllowed()) { } else if (!$settings->isRegistrationAllowed()) {
$this->createError("Registration is not enabled on this website."); $this->createError("Registration is not enabled on this website.");
} }
} else if ($this->getTemplateName() === "account/login.twig" && $this->getUser()->isLoggedIn()) { } else if ($this->getTemplateName() === "account/login.twig" && $this->getUser()) {
header("Location: /admin"); header("Location: /admin");
exit(); exit();
} else if ($this->getTemplateName() === "account/accept_invite.twig") { } else if ($this->getTemplateName() === "account/accept_invite.twig") {
if (isset($_GET["token"]) && is_string($_GET["token"]) && !empty($_GET["token"])) { if (isset($_GET["token"]) && is_string($_GET["token"]) && !empty($_GET["token"])) {
$this->parameters["view"]["token"] = $_GET["token"]; $this->parameters["view"]["token"] = $_GET["token"];
$req = new \Api\User\CheckToken($this->getUser()); $req = new \Api\User\CheckToken($this->getContext());
$this->parameters["view"]["success"] = $req->execute(array("token" => $_GET["token"])); $this->parameters["view"]["success"] = $req->execute(array("token" => $_GET["token"]));
if ($this->parameters["view"]["success"]) { if ($this->parameters["view"]["success"]) {
if (strcmp($req->getResult()["token"]["type"], "invite") !== 0) { if (strcmp($req->getResult()["token"]["type"], "invite") !== 0) {

@ -7,9 +7,9 @@ use Objects\Router\Router;
class Admin extends TemplateDocument { class Admin extends TemplateDocument {
public function __construct(Router $router) { public function __construct(Router $router) {
$user = $router->getUser(); $user = $router->getContext()->getUser();
$template = $user->isLoggedIn() ? "admin.twig" : "redirect.twig"; $template = $user ? "admin.twig" : "redirect.twig";
$params = $user->isLoggedIn() ? [] : ["url" => "/login"]; $params = $user ? [] : ["url" => "/login"];
parent::__construct($router, $template, $params); parent::__construct($router, $template, $params);
$this->enableCSP(); $this->enableCSP();
} }

@ -16,7 +16,7 @@ class Info extends HtmlDocument {
class InfoBody extends SimpleBody { class InfoBody extends SimpleBody {
protected function getContent(): string { protected function getContent(): string {
$user = $this->getDocument()->getUser(); $user = $this->getDocument()->getUser();
if ($user->isLoggedIn() && $user->hasGroup(USER_GROUP_ADMIN)) { if ($user && $user->hasGroup(USER_GROUP_ADMIN)) {
phpinfo(); phpinfo();
return ""; return "";
} else { } else {

@ -159,15 +159,15 @@ namespace Documents\Install {
} }
} }
$user = $this->getDocument()->getUser(); $context = $this->getDocument()->getContext();
$config = $user->getConfiguration(); $config = $context->getConfig();
// Check if database configuration exists // Check if database configuration exists
if (!$config->getDatabase()) { if (!$config->getDatabase()) {
return self::DATABASE_CONFIGURATION; return self::DATABASE_CONFIGURATION;
} }
$sql = $user->getSQL(); $sql = $context->getSQL();
if (!$sql || !$sql->isConnected()) { if (!$sql || !$sql->isConnected()) {
return self::DATABASE_CONFIGURATION; return self::DATABASE_CONFIGURATION;
} }
@ -185,7 +185,7 @@ namespace Documents\Install {
} }
if ($step === self::ADD_MAIL_SERVICE) { if ($step === self::ADD_MAIL_SERVICE) {
$req = new \Api\Settings\Get($user); $req = new \Api\Settings\Get($context);
$success = $req->execute(array("key" => "^mail_enabled$")); $success = $req->execute(array("key" => "^mail_enabled$"));
if (!$success) { if (!$success) {
$this->errorString = $req->getLastError(); $this->errorString = $req->getLastError();
@ -193,12 +193,12 @@ namespace Documents\Install {
} else if (isset($req->getResult()["settings"]["mail_enabled"])) { } else if (isset($req->getResult()["settings"]["mail_enabled"])) {
$step = self::FINISH_INSTALLATION; $step = self::FINISH_INSTALLATION;
$req = new \Api\Settings\Set($user); $req = new \Api\Settings\Set($context);
$success = $req->execute(array("settings" => array("installation_completed" => "1"))); $success = $req->execute(array("settings" => array("installation_completed" => "1")));
if (!$success) { if (!$success) {
$this->errorString = $req->getLastError(); $this->errorString = $req->getLastError();
} else { } else {
$req = new \Api\Notifications\Create($user); $req = new \Api\Notifications\Create($context);
$req->execute(array( $req->execute(array(
"title" => "Welcome", "title" => "Welcome",
"message" => "Your Web-base was successfully installed. Check out the admin dashboard. Have fun!", "message" => "Your Web-base was successfully installed. Check out the admin dashboard. Have fun!",
@ -365,24 +365,26 @@ namespace Documents\Install {
} }
} }
$user = $this->getDocument()->getUser(); if ($success) {
$config = $user->getConfiguration(); $context = $this->getDocument()->getContext();
$config = $context->getConfig();
if (Configuration::create("Database", $connectionData) === false) { if (Configuration::create("Database", $connectionData) === false) {
$success = false; $success = false;
$msg = "Unable to write database file"; $msg = "Unable to write database file";
} else { } else {
$config->setDatabase($connectionData); $config->setDatabase($connectionData);
if (!$user->connectDB()) { if (!$context->initSQL()) {
$success = false; $success = false;
$msg = "Unable to verify database connection after installation"; $msg = "Unable to verify database connection after installation";
} else { } else {
$req = new \Api\Routes\GenerateCache($user); $req = new \Api\Routes\GenerateCache($context);
if (!$req->execute()) { if (!$req->execute()) {
$success = false; $success = false;
$msg = "Unable to write route file: " . $req->getLastError(); $msg = "Unable to write route file: " . $req->getLastError();
} }
} }
} }
}
$sql->close(); $sql->close();
} }
@ -393,9 +395,9 @@ namespace Documents\Install {
private function createUser(): array { private function createUser(): array {
$user = $this->getDocument()->getUser(); $context = $this->getDocument()->getContext();
if ($this->getParameter("prev") === "true") { if ($this->getParameter("prev") === "true") {
$success = $user->getConfiguration()->delete("Database"); $success = $context->getConfig()->delete("Database");
$msg = $success ? "" : error_get_last(); $msg = $success ? "" : error_get_last();
return array("success" => $success, "msg" => $msg); return array("success" => $success, "msg" => $msg);
} }
@ -427,8 +429,7 @@ namespace Documents\Install {
$msg = "Please fill out the following inputs:<br>" . $msg = "Please fill out the following inputs:<br>" .
$this->createUnorderedList($missingInputs); $this->createUnorderedList($missingInputs);
} else { } else {
$sql = $user->getSQL(); $req = new \Api\User\Create($context);
$req = new \Api\User\Create($user);
$success = $req->execute(array( $success = $req->execute(array(
'username' => $username, 'username' => $username,
'email' => $email, 'email' => $email,
@ -438,6 +439,7 @@ namespace Documents\Install {
$msg = $req->getLastError(); $msg = $req->getLastError();
if ($success) { if ($success) {
$sql = $context->getSQL();
$success = $sql->insert("UserGroup", array("group_id", "user_id")) $success = $sql->insert("UserGroup", array("group_id", "user_id"))
->addRow(USER_GROUP_ADMIN, $req->getResult()["userId"]) ->addRow(USER_GROUP_ADMIN, $req->getResult()["userId"])
->execute(); ->execute();
@ -450,18 +452,16 @@ namespace Documents\Install {
private function addMailService(): array { private function addMailService(): array {
$user = $this->getDocument()->getUser(); $context = $this->getDocument()->getContext();
if ($this->getParameter("prev") === "true") { if ($this->getParameter("prev") === "true") {
$sql = $user->getSQL(); $sql = $context->getSQL();
$success = $sql->delete("User")->execute(); $success = $sql->delete("User")->execute();
$msg = $sql->getLastError(); $msg = $sql->getLastError();
return array("success" => $success, "msg" => $msg); return array("success" => $success, "msg" => $msg);
} }
$success = true;
$msg = $this->errorString;
if ($this->getParameter("skip") === "true") { if ($this->getParameter("skip") === "true") {
$req = new \Api\Settings\Set($user); $req = new \Api\Settings\Set($context);
$success = $req->execute(array("settings" => array("mail_enabled" => "0"))); $success = $req->execute(array("settings" => array("mail_enabled" => "0")));
$msg = $req->getLastError(); $msg = $req->getLastError();
} else { } else {
@ -473,17 +473,17 @@ namespace Documents\Install {
$success = true; $success = true;
$missingInputs = array(); $missingInputs = array();
if (is_null($address) || empty($address)) { if (empty($address)) {
$success = false; $success = false;
$missingInputs[] = "SMTP Address"; $missingInputs[] = "SMTP Address";
} }
if (is_null($port) || empty($port)) { if (empty($port)) {
$success = false; $success = false;
$missingInputs[] = "Port"; $missingInputs[] = "Port";
} }
if (is_null($username) || empty($username)) { if (empty($username)) {
$success = false; $success = false;
$missingInputs[] = "Username"; $missingInputs[] = "Username";
} }
@ -527,7 +527,7 @@ namespace Documents\Install {
} }
if ($success) { if ($success) {
$req = new \Api\Settings\Set($user); $req = new \Api\Settings\Set($context);
$success = $req->execute(array("settings" => array( $success = $req->execute(array("settings" => array(
"mail_enabled" => "1", "mail_enabled" => "1",
"mail_host" => "$address", "mail_host" => "$address",
@ -655,39 +655,25 @@ namespace Documents\Install {
} }
} }
$replacements = array("+" => " ", "&" => "\" ", "=" => "=\""); // $attributes = html_attributes($attributes);
$attributes = http_build_query($attributes) . "\"";
foreach ($replacements as $key => $val) {
$attributes = str_replace($key, $val, $attributes);
}
if ($type === "select") { if ($type === "select") {
$items = $formItem["items"] ?? array(); $items = $formItem["items"] ?? array();
$element = "<select $attributes>"; $options = [];
foreach ($items as $key => $val) { foreach ($items as $key => $val) {
$element .= "<option value=\"$key\">$val</option>"; $options[] = html_tag_ex("option", ["value" => $key], $val, true, false);
}
$element .= "</select>";
} else {
$element = "<input $attributes>";
} }
if (!$inline) { $element = html_tag_ex("select", $attributes, $options, false);
return
"<div class=\"d-block my-3\">
<label for=\"$name\">$title</label>
$element
</div>";
} else { } else {
return $element = html_tag_short("input", $attributes);
"<div class=\"col-md-6 mb-3\">
<label for=\"$name\">$title</label>
$element
</div>";
}
} }
private function createProgessMainview(): string { $label = html_tag_ex("label", ["for" => $name], $title, true, false);
$className = ($inline ? "col-md-6 mb-3" : "d-block my-3");
return html_tag_ex("div", ["class" => $className], $label . $element, false);
}
private function createProgressMainview(): string {
$isDocker = $this->isDocker(); $isDocker = $this->isDocker();
$defaultHost = ($isDocker ? "db" : "localhost"); $defaultHost = ($isDocker ? "db" : "localhost");
@ -773,7 +759,7 @@ namespace Documents\Install {
$spinnerIcon = $this->createIcon("spinner"); $spinnerIcon = $this->createIcon("spinner");
$title = $currentView["title"]; $title = $currentView["title"];
$html = "<h4 class=\"mb-3\">$title</h4><hr class=\"mb-4\">"; $html = "<h4 class=\"mb-3\">$title</h4><hr class=\"mb-4\" />";
if (isset($currentView["text"])) { if (isset($currentView["text"])) {
$text = $currentView["text"]; $text = $currentView["text"];
@ -905,7 +891,7 @@ namespace Documents\Install {
} }
$progressSidebar = $this->createProgressSidebar(); $progressSidebar = $this->createProgressSidebar();
$progressMainview = $this->createProgessMainview(); $progressMainView = $this->createProgressMainview();
$errorStyle = ($this->errorString ? '' : ' style="display:none"'); $errorStyle = ($this->errorString ? '' : ' style="display:none"');
$errorClass = ($this->errorString ? ' alert-danger' : ''); $errorClass = ($this->errorString ? ' alert-danger' : '');
@ -931,7 +917,7 @@ namespace Documents\Install {
</ul> </ul>
</div> </div>
<div class=\"col-md-8 order-md-1\"> <div class=\"col-md-8 order-md-1\">
$progressMainview $progressMainView
<div class=\"alert$errorClass mt-4\" id=\"status\"$errorStyle>$this->errorString</div> <div class=\"alert$errorClass mt-4\" id=\"status\"$errorStyle>$this->errorString</div>
</div> </div>
</div> </div>

@ -34,13 +34,16 @@ class Logger {
} }
protected function getStackTrace(int $pop = 2): string { protected function getStackTrace(int $pop = 2): string {
$debugTrace = debug_backtrace(); $debugTrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
if ($pop > 0) { if ($pop > 0) {
array_splice($debugTrace, 0, $pop); array_splice($debugTrace, 0, $pop);
} }
return implode("\n", array_map(function ($trace) { return implode("\n", array_map(function ($trace) {
if (isset($trace["file"])) {
return $trace["file"] . "#" . $trace["line"] . ": " . $trace["function"] . "()"; return $trace["file"] . "#" . $trace["line"] . ": " . $trace["function"] . "()";
} else {
return $trace["function"] . "()";
}
}, $debugTrace)); }, $debugTrace));
} }
@ -93,8 +96,8 @@ class Logger {
return $message; return $message;
} }
public function debug(string $message): string { public function debug(string $message, bool $appendStackTrace = false): string {
$this->log($message, "debug"); $this->log($message, "debug", $appendStackTrace);
return $message; return $message;
} }

@ -43,6 +43,11 @@ class Select extends Query {
return $this; return $this;
} }
public function addValue($value): Select {
$this->selectValues[] = $value;
return $this;
}
public function where(...$conditions): Select { public function where(...$conditions): Select {
$this->conditions[] = (count($conditions) === 1 ? $conditions : new CondOr($conditions)); $this->conditions[] = (count($conditions) === 1 ? $conditions : new CondOr($conditions));
return $this; return $this;
@ -63,6 +68,11 @@ class Select extends Query {
return $this; return $this;
} }
public function addJoin(Join $join): Select {
$this->joins[] = $join;
return $this;
}
public function groupBy(...$columns): Select { public function groupBy(...$columns): Select {
$this->groupColumns = $columns; $this->groupColumns = $columns;
return $this; return $this;

@ -52,16 +52,19 @@ abstract class SQL {
protected ConnectionData $connectionData; protected ConnectionData $connectionData;
protected int $lastInsertId; protected int $lastInsertId;
protected bool $logQueries;
public function __construct($connectionData) { public function __construct($connectionData) {
$this->connection = NULL; $this->connection = NULL;
$this->lastError = 'Unknown Error'; $this->lastError = 'Unknown Error';
$this->connectionData = $connectionData; $this->connectionData = $connectionData;
$this->lastInsertId = 0; $this->lastInsertId = 0;
$this->logger = new Logger(getClassName($this), $this); $this->logger = new Logger(getClassName($this), $this);
$this->logQueries = false;
} }
public function isConnected(): bool { public function isConnected(): bool {
return !is_null($this->connection); return !is_null($this->connection) && !is_bool($this->connection);
} }
public function getLastError(): string { public function getLastError(): string {
@ -131,7 +134,7 @@ abstract class SQL {
$parameters = []; $parameters = [];
$queryStr = $query->build($parameters); $queryStr = $query->build($parameters);
if($query->dump) { if ($query->dump) {
var_dump($queryStr); var_dump($queryStr);
var_dump($parameters); var_dump($parameters);
} }
@ -149,6 +152,31 @@ abstract class SQL {
$this->fetchReturning($res, $generatedColumn); $this->fetchReturning($res, $generatedColumn);
} }
if ($this->logQueries && (!($query instanceof Insert) || $query->getTableName() !== "SystemLog")) {
if ($success === false || $fetchType == self::FETCH_NONE) {
$result = var_export($success, true);
} else if ($fetchType === self::FETCH_ALL) {
$result = count($res) . " rows";
} else if ($fetchType === self::FETCH_ONE) {
$result = ($res === null ? "(empty)" : "1 row");
} else if ($fetchType === self::FETCH_ITERATIVE) {
$result = $res->getNumRows() . " rows (iterative)";
} else {
$result = "Unknown";
}
$message = sprintf("Query: %s, Parameters: %s, Result: %s",
var_export($queryStr, true), var_export($parameters, true), $result
);
if ($success === false) {
$message .= "Error: " . var_export($this->lastError, true);
}
$this->logger->debug($message);
}
return $fetchType === self::FETCH_NONE ? $success : $res; return $fetchType === self::FETCH_NONE ? $success : $res;
} }

@ -5,8 +5,9 @@ namespace Elements;
use Configuration\Settings; use Configuration\Settings;
use Driver\Logger\Logger; use Driver\Logger\Logger;
use Driver\SQL\SQL; use Driver\SQL\SQL;
use Objects\Context;
use Objects\Router\Router; use Objects\Router\Router;
use Objects\User; use Objects\DatabaseEntity\User;
abstract class Document { abstract class Document {
@ -32,16 +33,20 @@ abstract class Document {
return $this->logger; return $this->logger;
} }
public function getUser(): User { public function getUser(): ?User {
return $this->router->getUser(); return $this->getContext()->getUser();
}
public function getContext(): Context {
return $this->router->getContext();
} }
public function getSQL(): ?SQL { public function getSQL(): ?SQL {
return $this->getUser()->getSQL(); return $this->getContext()->getSQL();
} }
public function getSettings(): Settings { public function getSettings(): Settings {
return $this->getUser()->getConfiguration()->getSettings(); return $this->getContext()->getSettings();
} }
public function getCSPNonce(): ?string { public function getCSPNonce(): ?string {

@ -65,10 +65,10 @@ class HtmlDocument extends Document {
} }
$head = $this->head->getCode(); $head = $this->head->getCode();
$lang = $this->getUser()->getLanguage()->getShortCode(); $lang = $this->getContext()->getLanguage();
$code = "<!DOCTYPE html>"; $code = "<!DOCTYPE html>";
$code .= html_tag("html", ["lang" => $lang], $head . $body, false); $code .= html_tag("html", ["lang" => $lang->getShortCode()], $head . $body, false);
return $code; return $code;
} }

@ -46,13 +46,14 @@ class TemplateDocument extends Document {
public function renderTemplate(string $name, array $params = []): string { public function renderTemplate(string $name, array $params = []): string {
try { try {
$user = $this->getUser(); $context = $this->getContext();
$session = $context->getSession();
$params["user"] = [ $params["user"] = [
"lang" => $user->getLanguage()->getShortCode(), "lang" => $context->getLanguage()->getShortCode(),
"loggedIn" => $user->isLoggedIn(), "loggedIn" => $session !== null,
"session" => (!$user->isLoggedIn() ? null : [ "session" => ($session ? [
"csrfToken" => $user->getSession()->getCsrfToken() "csrfToken" => $session->getCsrfToken()
]) ] : null)
]; ];
$settings = $this->getSettings(); $settings = $this->getSettings();

@ -23,8 +23,7 @@ abstract class View extends StaticView {
public function isSearchable(): bool { return $this->searchable; } public function isSearchable(): bool { return $this->searchable; }
public function getSiteName(): string { public function getSiteName(): string {
// what a chain lol return $this->getDocument()->getSettings()->getSiteName();
return $this->getDocument()->getUser()->getConfiguration()->getSettings()->getSiteName();
} }
protected function load(string $viewClass) : string { protected function load(string $viewClass) : string {
@ -43,7 +42,7 @@ abstract class View extends StaticView {
} }
private function loadLanguageModules() { private function loadLanguageModules() {
$lang = $this->document->getUser()->getLanguage(); $lang = $this->document->getContext()->getLanguage();
foreach ($this->langModules as $langModule) { foreach ($this->langModules as $langModule) {
$lang->loadModule($langModule); $lang->loadModule($langModule);
} }

@ -0,0 +1,202 @@
<?php
namespace Objects;
use Configuration\Configuration;
use Configuration\Settings;
use Driver\SQL\Condition\Compare;
use Driver\SQL\Condition\CondLike;
use Driver\SQL\Condition\CondOr;
use Driver\SQL\SQL;
use Firebase\JWT\JWT;
use Objects\DatabaseEntity\Language;
use Objects\DatabaseEntity\Session;
use Objects\DatabaseEntity\User;
class Context {
private ?SQL $sql;
private ?Session $session;
private ?User $user;
private Configuration $configuration;
private Language $language;
public function __construct() {
$this->sql = null;
$this->session = null;
$this->user = 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->getJwtKey());
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 \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;
}
}

@ -0,0 +1,27 @@
<?php
namespace Objects\DatabaseEntity;
use 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()
];
}
}

@ -0,0 +1,21 @@
<?php
namespace 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;
}
}

@ -0,0 +1,17 @@
<?php
namespace 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;
}
}

@ -0,0 +1,7 @@
<?php
namespace Objects\DatabaseEntity\Attribute;
#[\Attribute(\Attribute::TARGET_PROPERTY)] class Json {
}

@ -0,0 +1,16 @@
<?php
namespace 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;
}
}

@ -0,0 +1,15 @@
<?php
namespace 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 Objects\DatabaseEntity\Attribute;
#[\Attribute(\Attribute::TARGET_PROPERTY)] class Transient {
}

@ -0,0 +1,7 @@
<?php
namespace Objects\DatabaseEntity\Attribute;
#[\Attribute(\Attribute::TARGET_PROPERTY)] class Unique {
}

@ -2,30 +2,71 @@
namespace Objects\DatabaseEntity; namespace Objects\DatabaseEntity;
use Driver\SQL\Condition\Compare;
use Driver\SQL\Condition\Condition; use Driver\SQL\Condition\Condition;
use Driver\SQL\SQL; use Driver\SQL\SQL;
abstract class DatabaseEntity { abstract class DatabaseEntity {
private static array $handlers = []; private static array $handlers = [];
private ?int $id = null; protected ?int $id;
public function __construct() { public function __construct(?int $id = null) {
$this->id = $id;
} }
public static function find(SQL $sql, int $id): ?DatabaseEntity { 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); $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); 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 { public static function findAll(SQL $sql, ?Condition $condition = null): ?array {
$handler = self::getHandler($sql); $handler = self::getHandler($sql);
return $handler->fetchMultiple($condition); return $handler->fetchMultiple($condition);
} }
public function save(SQL $sql): bool { 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); $handler = self::getHandler($sql);
$res = $handler->insertOrUpdate($this); $res = $handler->insertOrUpdate($this, $columns);
if ($res === false) { if ($res === false) {
return false; return false;
} else if ($this->id === null) { } else if ($this->id === null) {

@ -5,7 +5,9 @@ namespace Objects\DatabaseEntity;
use Driver\Logger\Logger; use Driver\Logger\Logger;
use Driver\SQL\Column\BoolColumn; use Driver\SQL\Column\BoolColumn;
use Driver\SQL\Column\DateTimeColumn; use Driver\SQL\Column\DateTimeColumn;
use Driver\SQL\Column\EnumColumn;
use Driver\SQL\Column\IntColumn; use Driver\SQL\Column\IntColumn;
use Driver\SQL\Column\JsonColumn;
use Driver\SQL\Column\StringColumn; use Driver\SQL\Column\StringColumn;
use Driver\SQL\Condition\Compare; use Driver\SQL\Condition\Compare;
use Driver\SQL\Condition\Condition; use Driver\SQL\Condition\Condition;
@ -13,19 +15,27 @@ use Driver\SQL\Column\DoubleColumn;
use Driver\SQL\Column\FloatColumn; use Driver\SQL\Column\FloatColumn;
use Driver\SQL\Constraint\ForeignKey; use Driver\SQL\Constraint\ForeignKey;
use Driver\SQL\Query\CreateTable; use Driver\SQL\Query\CreateTable;
use Driver\SQL\Query\Select;
use Driver\SQL\SQL; use Driver\SQL\SQL;
use Driver\SQL\Strategy\CascadeStrategy; use Driver\SQL\Strategy\CascadeStrategy;
use Driver\SQL\Strategy\SetNullStrategy; use Driver\SQL\Strategy\SetNullStrategy;
use Objects\DatabaseEntity\Attribute\Enum;
use Objects\DatabaseEntity\Attribute\DefaultValue;
use Objects\DatabaseEntity\Attribute\Json;
use Objects\DatabaseEntity\Attribute\Many;
use Objects\DatabaseEntity\Attribute\MaxLength;
use Objects\DatabaseEntity\Attribute\Transient;
use Objects\DatabaseEntity\Attribute\Unique;
use PHPUnit\Util\Exception; use PHPUnit\Util\Exception;
class DatabaseEntityHandler { class DatabaseEntityHandler {
private \ReflectionClass $entityClass; private \ReflectionClass $entityClass;
private static \ReflectionProperty $ID_FIELD;
private string $tableName; private string $tableName;
private array $columns; private array $columns;
private array $properties; private array $properties;
private array $relations; private array $relations;
private array $constraints;
private SQL $sql; private SQL $sql;
private Logger $logger; private Logger $logger;
@ -34,22 +44,23 @@ class DatabaseEntityHandler {
$className = $entityClass->getName(); $className = $entityClass->getName();
$this->logger = new Logger($entityClass->getShortName(), $sql); $this->logger = new Logger($entityClass->getShortName(), $sql);
$this->entityClass = $entityClass; $this->entityClass = $entityClass;
if (!$this->entityClass->isSubclassOf(DatabaseEntity::class) || if (!$this->entityClass->isSubclassOf(DatabaseEntity::class)) {
!$this->entityClass->isInstantiable()) {
$this->raiseError("Cannot persist class '$className': Not an instance of DatabaseEntity or not instantiable."); $this->raiseError("Cannot persist class '$className': Not an instance of DatabaseEntity or not instantiable.");
} }
$this->tableName = $this->entityClass->getShortName(); $this->tableName = $this->entityClass->getShortName();
$this->columns = []; $this->columns = []; // property name => database column name
$this->properties = []; $this->properties = []; // property name => \ReflectionProperty
$this->relations = []; $this->relations = []; // property name => referenced table name
$this->constraints = []; // \Driver\SQL\Constraint\Constraint
if (!isset(self::$ID_FIELD)) {
self::$ID_FIELD = (new \ReflectionClass(DatabaseEntity::class))->getProperty("id");
}
foreach ($this->entityClass->getProperties() as $property) { foreach ($this->entityClass->getProperties() as $property) {
$propertyName = $property->getName(); $propertyName = $property->getName();
if ($propertyName === "id") {
$this->properties[$propertyName] = $property;
continue;
}
$propertyType = $property->getType(); $propertyType = $property->getType();
$columnName = self::getColumnName($propertyName); $columnName = self::getColumnName($propertyName);
if (!($propertyType instanceof \ReflectionNamedType)) { if (!($propertyType instanceof \ReflectionNamedType)) {
@ -58,38 +69,83 @@ class DatabaseEntityHandler {
$nullable = $propertyType->allowsNull(); $nullable = $propertyType->allowsNull();
$propertyTypeName = $propertyType->getName(); $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') { if ($propertyTypeName === 'string') {
$this->columns[$propertyName] = new StringColumn($columnName, null, $nullable); $enum = self::getAttribute($property, Enum::class);
} else if ($propertyTypeName === 'int') { if ($enum) {
$this->columns[$propertyName] = new IntColumn($columnName, $nullable); $this->columns[$propertyName] = new EnumColumn($columnName, $enum->getValues(), $nullable, $defaultValue);
} else if ($propertyTypeName === 'float') {
$this->columns[$propertyName] = new FloatColumn($columnName, $nullable);
} else if ($propertyTypeName === 'double') {
$this->columns[$propertyName] = new DoubleColumn($columnName, $nullable);
} else if ($propertyTypeName === 'bool') {
$this->columns[$propertyName] = new BoolColumn($columnName, $nullable);
} else if ($propertyTypeName === 'DateTime') {
$this->columns[$propertyName] = new DateTimeColumn($columnName, $nullable);
} else { } 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 { try {
$requestedClass = new \ReflectionClass($propertyTypeName); $requestedClass = new \ReflectionClass($propertyTypeName);
if ($requestedClass->isSubclassOf(DatabaseEntity::class)) { if ($requestedClass->isSubclassOf(DatabaseEntity::class)) {
$columnName .= "_id";
$requestedHandler = ($requestedClass->getName() === $this->entityClass->getName()) ? $requestedHandler = ($requestedClass->getName() === $this->entityClass->getName()) ?
$this : DatabaseEntity::getHandler($this->sql, $requestedClass); $this : DatabaseEntity::getHandler($this->sql, $requestedClass);
$strategy = $nullable ? new SetNullStrategy() : new CascadeStrategy(); $strategy = $nullable ? new SetNullStrategy() : new CascadeStrategy();
$this->columns[$propertyName] = new IntColumn($columnName, $nullable); $this->columns[$propertyName] = new IntColumn($columnName, $nullable, $defaultValue);
$this->relations[$propertyName] = new ForeignKey($columnName, $requestedHandler->tableName, "id", $strategy); $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) { } 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->raiseError("Cannot persist class '$className': Property '$propertyName' has non persist-able type: $propertyTypeName");
} }
} }
$this->properties[$propertyName] = $property; $this->properties[$propertyName] = $property;
if ($isUnique) {
$this->constraints[] = new \Driver\SQL\Constraint\Unique($columnName);
}
} }
} }
private static function getColumnName(string $propertyName): string { 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 // abcTestLOL => abc_test_lol
return strtolower(preg_replace_callback("/([a-z])([A-Z]+)/", function ($m) { return strtolower(preg_replace_callback("/([a-z])([A-Z]+)/", function ($m) {
return $m[1] . "_" . strtolower($m[2]); return $m[1] . "_" . strtolower($m[2]);
@ -108,46 +164,111 @@ class DatabaseEntityHandler {
return $this->tableName; return $this->tableName;
} }
private function entityFromRow(array $row): DatabaseEntity { 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 { try {
$entity = $this->entityClass->newInstanceWithoutConstructor();
$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) { foreach ($this->columns as $propertyName => $column) {
$value = $row[$column->getName()]; $columnName = $column->getName();
if (array_key_exists($columnName, $row)) {
$value = $row[$columnName];
$property = $this->properties[$propertyName]; $property = $this->properties[$propertyName];
if ($property->getType()->getName() === "DateTime") { if ($column instanceof DateTimeColumn) {
$value = new \DateTime($value); $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); $property->setValue($entity, $value);
} }
}
self::$ID_FIELD->setAccessible(true); $this->properties["id"]->setAccessible(true);
self::$ID_FIELD->setValue($entity, $row["id"]); $this->properties["id"]->setValue($entity, $row["id"]);
$entity->postFetch($this->sql, $row);
return $entity; return $entity;
} catch (\Exception $exception) { } catch (\Exception $exception) {
$this->logger->error("Error creating entity from database row: " . $exception->getMessage()); $this->logger->error("Error creating entity from database row: " . $exception->getMessage());
throw $exception; return null;
} }
} }
public function fetchOne(int $id): ?DatabaseEntity { public function getSelectQuery(): Select {
$res = $this->sql->select("id", ...array_keys($this->columns)) return $this->sql->select(...$this->getColumnNames())
->from($this->tableName) ->from($this->tableName);
->where(new Compare("id", $id)) }
public function fetchOne(int $id): DatabaseEntity|bool|null {
$res = $this->getSelectQuery()
->where(new Compare($this->tableName . ".id", $id))
->first() ->first()
->execute(); ->execute();
if (empty($res)) { if ($res === false || $res === null) {
return null; return $res;
} else { } else {
return $this->entityFromRow($res); return $this->entityFromRow($res);
} }
} }
public function fetchMultiple(?Condition $condition = null): ?array { public function fetchMultiple(?Condition $condition = null): ?array {
$query = $this->sql->select("id", ...array_keys($this->columns)) $query = $this->getSelectQuery();
->from($this->tableName);
if ($condition) { if ($condition) {
$query->where($condition); $query->where($condition);
@ -159,7 +280,10 @@ class DatabaseEntityHandler {
} else { } else {
$entities = []; $entities = [];
foreach ($res as $row) { foreach ($res as $row) {
$entities[] = $this->entityFromRow($row); $entity = $this->entityFromRow($row);
if ($entity) {
$entities[$entity->getId()] = $entity;
}
} }
return $entities; return $entities;
} }
@ -175,7 +299,7 @@ class DatabaseEntityHandler {
$query->addColumn($column); $query->addColumn($column);
} }
foreach ($this->relations as $constraint) { foreach ($this->constraints as $constraint) {
$query->addConstraint($constraint); $query->addConstraint($constraint);
} }
@ -187,29 +311,43 @@ class DatabaseEntityHandler {
return $query->execute(); return $query->execute();
} }
public function insertOrUpdate(DatabaseEntity $entity) { public function insertOrUpdate(DatabaseEntity $entity, ?array $columns = null) {
$id = $entity->getId(); $id = $entity->getId();
if ($id === null) { $action = $id === null ? "insert" : "update";
$columns = [];
$row = [];
$row = [];
foreach ($this->columns as $propertyName => $column) { foreach ($this->columns as $propertyName => $column) {
$columns[] = $column->getName(); if ($columns && !in_array($column->getName(), $columns)) {
continue;
}
$property = $this->properties[$propertyName]; $property = $this->properties[$propertyName];
$property->setAccessible(true);
if ($property->isInitialized($entity)) { if ($property->isInitialized($entity)) {
$value = $property->getValue($entity); $value = $property->getValue($entity);
if (isset($this->relations[$propertyName])) {
$value = $value->getId();
}
} else if (!$this->columns[$propertyName]->notNull()) { } else if (!$this->columns[$propertyName]->notNull()) {
$value = null; $value = null;
} else { } else {
$this->logger->error("Cannot insert entity: property '$propertyName' was not initialized yet."); if ($action !== "update") {
$this->logger->error("Cannot $action entity: property '$propertyName' was not initialized yet.");
return false; return false;
} else {
continue;
}
} }
$row[] = $value; $row[$column->getName()] = $value;
} }
$res = $this->sql->insert($this->tableName, $columns) $entity->preInsert($row);
->addRow(...$row)
if ($id === null) {
$res = $this->sql->insert($this->tableName, array_keys($row))
->addRow(...array_values($row))
->returning("id") ->returning("id")
->execute(); ->execute();
@ -220,20 +358,9 @@ class DatabaseEntityHandler {
} }
} else { } else {
$query = $this->sql->update($this->tableName) $query = $this->sql->update($this->tableName)
->where(new Compare("id", $id)); ->where(new Compare($this->tableName . ".id", $id));
foreach ($this->columns as $propertyName => $column) {
$columnName = $column->getName();
$property = $this->properties[$propertyName];
if ($property->isInitialized($entity)) {
$value = $property->getValue($entity);
} else if (!$this->columns[$propertyName]->notNull()) {
$value = null;
} else {
$this->logger->error("Cannot update entity: property '$propertyName' was not initialized yet.");
return false;
}
foreach ($row as $columnName => $value) {
$query->set($columnName, $value); $query->set($columnName, $value);
} }
@ -242,15 +369,14 @@ class DatabaseEntityHandler {
} }
public function delete(int $id) { public function delete(int $id) {
return $this->sql->delete($this->tableName)->where(new Compare("id", $id))->execute(); return $this->sql
->delete($this->tableName)
->where(new Compare($this->tableName . ".id", $id))
->execute();
} }
private function raiseError(string $message) { private function raiseError(string $message) {
$this->logger->error($message); $this->logger->error($message);
throw new Exception($message); throw new Exception($message);
} }
private function getPropertyValue() {
}
} }

@ -0,0 +1,135 @@
<?php
namespace Objects\DatabaseEntity;
use Driver\SQL\Condition\Condition;
use Driver\SQL\Join;
use Driver\SQL\Query\Select;
use 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;
}
}

@ -1,19 +1,23 @@
<?php <?php
namespace Objects; namespace Objects\DatabaseEntity;
class GpgKey extends ApiObject { use Driver\SQL\Expression\CurrentTimeStamp;
use Objects\DatabaseEntity\Attribute\MaxLength;
use Objects\DatabaseEntity\Attribute\DefaultValue;
class GpgKey extends DatabaseEntity {
const GPG2 = "/usr/bin/gpg2"; const GPG2 = "/usr/bin/gpg2";
private int $id;
private bool $confirmed; private bool $confirmed;
private string $fingerprint; #[MaxLength(64)] private string $fingerprint;
private string $algorithm; #[MaxLength(64)] private string $algorithm;
private \DateTime $expires; private \DateTime $expires;
#[DefaultValue(CurrentTimeStamp::class)] private \DateTime $added;
public function __construct(int $id, bool $confirmed, string $fingerprint, string $algorithm, string $expires) { public function __construct(int $id, bool $confirmed, string $fingerprint, string $algorithm, string $expires) {
$this->id = $id; parent::__construct($id);
$this->confirmed = $confirmed; $this->confirmed = $confirmed;
$this->fingerprint = $fingerprint; $this->fingerprint = $fingerprint;
$this->algorithm = $algorithm; $this->algorithm = $algorithm;
@ -25,26 +29,17 @@ class GpgKey extends ApiObject {
$cmd = self::GPG2 . " --encrypt --output - --recipient $gpgFingerprint --trust-model always --batch --armor"; $cmd = self::GPG2 . " --encrypt --output - --recipient $gpgFingerprint --trust-model always --batch --armor";
list($out, $err) = self::proc_exec($cmd, $body, true); list($out, $err) = self::proc_exec($cmd, $body, true);
if ($out === null) { if ($out === null) {
return self::createError("Error while communicating with GPG agent"); return createError("Error while communicating with GPG agent");
} else if ($err) { } else if ($err) {
return self::createError($err); return createError($err);
} else { } else {
return ["success" => true, "data" => $out]; return ["success" => true, "data" => $out];
} }
} }
public function jsonSerialize(): array {
return array(
"fingerprint" => $this->fingerprint,
"algorithm" => $this->algorithm,
"expires" => $this->expires->getTimestamp(),
"confirmed" => $this->confirmed
);
}
private static function proc_exec(string $cmd, ?string $stdin = null, bool $raw = false): ?array { 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"]); $descriptorSpec = array(0 => ["pipe", "r"], 1 => ["pipe", "w"], 2 => ["pipe", "w"]);
$process = proc_open($cmd, $descriptorSpec,$pipes); $process = proc_open($cmd, $descriptorSpec, $pipes);
if (!is_resource($process)) { if (!is_resource($process)) {
return null; return null;
} }
@ -62,25 +57,21 @@ class GpgKey extends ApiObject {
return [($raw ? $out : trim($out)), $err]; return [($raw ? $out : trim($out)), $err];
} }
private static function createError(string $error) : array {
return ["success" => false, "error" => $error];
}
public static function getKeyInfo(string $key): array { public static function getKeyInfo(string $key): array {
list($out, $err) = self::proc_exec(self::GPG2 . " --show-key", $key); list($out, $err) = self::proc_exec(self::GPG2 . " --show-key", $key);
if ($out === null) { if ($out === null) {
return self::createError("Error while communicating with GPG agent"); return createError("Error while communicating with GPG agent");
} }
if ($err) { if ($err) {
return self::createError($err); return createError($err);
} }
$lines = explode("\n", $out); $lines = explode("\n", $out);
if (count($lines) > 4) { if (count($lines) > 4) {
return self::createError("It seems like you have uploaded more than one GPG-Key"); 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)) { } else if (count($lines) !== 4 || !preg_match("/(\S+)\s+(\w+)\s+.*\[expires: ([0-9-]+)]/", $lines[0], $matches)) {
return self::createError("Error parsing GPG output"); return createError("Error parsing GPG output");
} }
$keyType = $matches[1]; $keyType = $matches[1];
@ -94,7 +85,7 @@ class GpgKey extends ApiObject {
public static function importKey(string $key): array { public static function importKey(string $key): array {
list($out, $err) = self::proc_exec(self::GPG2 . " --import", $key); list($out, $err) = self::proc_exec(self::GPG2 . " --import", $key);
if ($out === null) { if ($out === null) {
return self::createError("Error while communicating with GPG agent"); 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("/gpg:\s+Total number processed:\s+(\d+)/", $err, $matches) && intval($matches[1]) > 0) {
@ -104,7 +95,7 @@ class GpgKey extends ApiObject {
} }
} }
return self::createError($err); return createError($err);
} }
public static function export($gpgFingerprint, bool $armored): array { public static function export($gpgFingerprint, bool $armored): array {
@ -115,7 +106,7 @@ class GpgKey extends ApiObject {
$cmd .= escapeshellarg($gpgFingerprint); $cmd .= escapeshellarg($gpgFingerprint);
list($out, $err) = self::proc_exec($cmd); list($out, $err) = self::proc_exec($cmd);
if ($err) { if ($err) {
return self::createError($err); return createError($err);
} }
return ["success" => true, "data" => $out]; return ["success" => true, "data" => $out];
@ -125,12 +116,18 @@ class GpgKey extends ApiObject {
return $this->confirmed; return $this->confirmed;
} }
public function getId(): int {
return $this->id;
}
public function getFingerprint(): string { public function getFingerprint(): string {
return $this->fingerprint; 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
];
}
} }

@ -0,0 +1,23 @@
<?php
namespace Objects\DatabaseEntity;
use 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
];
}
}

@ -0,0 +1,103 @@
<?php
namespace Objects\DatabaseEntity {
use Objects\DatabaseEntity\Attribute\MaxLength;
use Objects\DatabaseEntity\Attribute\Transient;
use 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;
$this->entries = array();
$this->modules = array();
}
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();
}
$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");
}
}
}
namespace {
function L($key) {
if (!array_key_exists('LANGUAGE', $GLOBALS))
return $key;
global $LANGUAGE;
return $LANGUAGE->translate($key);
}
}

@ -0,0 +1,30 @@
<?php
namespace Objects\DatabaseEntity;
use Api\Parameter\Parameter;
use Driver\SQL\Expression\CurrentTimeStamp;
use Objects\DatabaseEntity\Attribute\DefaultValue;
use 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
];
}
}

@ -0,0 +1,30 @@
<?php
namespace Objects\DatabaseEntity;
use Api\Parameter\Parameter;
use Driver\SQL\Expression\CurrentTimeStamp;
use Objects\DatabaseEntity\Attribute\DefaultValue;
use Objects\DatabaseEntity\Attribute\Enum;
use 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
];
}
}

@ -0,0 +1,134 @@
<?php
namespace Objects\DatabaseEntity;
use DateTime;
use Exception;
use Firebase\JWT\JWT;
use Objects\Context;
use Objects\DatabaseEntity\Attribute\DefaultValue;
use Objects\DatabaseEntity\Attribute\Json;
use Objects\DatabaseEntity\Attribute\MaxLength;
use 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()];
$jwtKey = $settings->getJwtKey();
return JWT::encode($token, $jwtKey->getKeyMaterial(), $jwtKey->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 insert(bool $stayLoggedIn = false): bool {
$this->stayLoggedIn = $stayLoggedIn;
$this->active = true;
return $this->update();
}
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;
}
}

@ -0,0 +1,31 @@
<?php
namespace Objects\DatabaseEntity;
use Api\Parameter\Parameter;
use Driver\SQL\Expression\CurrentTimeStamp;
use Objects\DatabaseEntity\Attribute\DefaultValue;
use Objects\DatabaseEntity\Attribute\Enum;
use 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
];
}
}

@ -0,0 +1,78 @@
<?php
namespace Objects\DatabaseEntity;
use Driver\SQL\SQL;
use Objects\DatabaseEntity\Attribute\Enum;
use Objects\DatabaseEntity\Attribute\MaxLength;
use Objects\TwoFactor\KeyBasedTwoFactorToken;
use 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;
}
}

@ -0,0 +1,109 @@
<?php
namespace Objects\DatabaseEntity;
use Driver\SQL\Condition\Compare;
use Driver\SQL\Expression\CurrentTimeStamp;
use Driver\SQL\Join;
use Driver\SQL\SQL;
use Objects\DatabaseEntity\Attribute\DefaultValue;
use Objects\DatabaseEntity\Attribute\MaxLength;
use Objects\DatabaseEntity\Attribute\Transient;
use 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"]);
}
}

@ -1,139 +0,0 @@
<?php
namespace Objects {
use Objects\lang\LanguageModule;
class Language extends ApiObject {
const LANG_CODE_PATTERN = "/^[a-zA-Z]+_[a-zA-Z]+$/";
private int $languageId;
private string $langCode;
private string $langName;
private array $modules;
protected array $entries;
public function __construct($languageId, $langCode, $langName) {
$this->languageId = $languageId;
$this->langCode = $langCode;
$this->langName = $langName;
$this->entries = array();
$this->modules = array();
}
public function getId() { return $this->languageId; }
public function getCode(): string { return $this->langCode; }
public function getShortCode() { return substr($this->langCode, 0, 2); }
public function getName() { return $this->langName; }
/**
* @param $module LanguageModule class or object
*/
public function loadModule($module) {
if(!is_object($module))
$module = new $module;
$aModuleEntries = $module->getEntries($this->langCode);
$this->entries = array_merge($this->entries, $aModuleEntries);
$this->modules[] = $module;
}
public function translate(string $key): string {
if(isset($this->entries[$key]))
return $this->entries[$key];
return $key;
}
public function sendCookie(?string $domain = null) {
$domain = empty($domain) ? "" : $domain;
setcookie('lang', $this->langCode, 0, "/", $domain, false, false);
}
public function jsonSerialize(): array {
return array(
'uid' => $this->languageId,
'code' => $this->langCode,
'shortCode' => explode("_", $this->langCode)[0],
'name' => $this->langName,
);
}
public static function newInstance($languageId, $langCode, $langName) {
if(!preg_match(Language::LANG_CODE_PATTERN, $langCode)) {
return false;
}
// TODO: include dynamically wanted Language
return new Language($languageId, $langCode, $langName);
// $className = $langCode
// return new $className($languageId, $langCode);
}
public function load() {
global $LANGUAGE;
$LANGUAGE = $this;
}
public static function DEFAULT_LANGUAGE() {
if(isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
$acceptLanguage = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
$aSplit = explode(',',$acceptLanguage);
foreach($aSplit as $code) {
if(strlen($code) == 2) {
$code = $code . '_' . strtoupper($code);
}
$code = str_replace("-", "_", $code);
if(strlen($code) != 5)
continue;
$lang = Language::newInstance(0, $code, "");
if($lang)
return $lang;
}
}
return Language::newInstance(1, "en_US", "American English");
}
};
}
namespace {
function L($key) {
if(!array_key_exists('LANGUAGE', $GLOBALS))
return $key;
global $LANGUAGE;
return $LANGUAGE->translate($key);
}
function LANG_NAME() {
if(!array_key_exists('LANGUAGE', $GLOBALS))
return "LANG_NAME";
global $LANGUAGE;
return $LANGUAGE->getName();
}
function LANG_CODE() {
if(!array_key_exists('LANGUAGE', $GLOBALS))
return "LANG_CODE";
global $LANGUAGE;
return $LANGUAGE->getCode();
}
function SHORT_LANG_CODE() {
if(!array_key_exists('LANGUAGE', $GLOBALS))
return "SHORT_LANG_CODE";
global $LANGUAGE;
return $LANGUAGE->getShortCode();
}
}

@ -13,7 +13,6 @@ class ApiRoute extends AbstractRoute {
} }
public function call(Router $router, array $params): string { public function call(Router $router, array $params): string {
$user = $router->getUser();
if (empty($params["endpoint"])) { if (empty($params["endpoint"])) {
header("Content-Type: text/html"); header("Content-Type: text/html");
$document = new \Elements\TemplateDocument($router, "swagger.twig"); $document = new \Elements\TemplateDocument($router, "swagger.twig");
@ -43,9 +42,11 @@ class ApiRoute extends AbstractRoute {
http_response_code(400); http_response_code(400);
$response = createError("Invalid Method"); $response = createError("Invalid Method");
} else { } else {
$request = $apiClass->newInstanceArgs(array($user, true)); $request = $apiClass->newInstanceArgs(array($router->getContext(), true));
$request->execute(); $success = $request->execute();
$response = $request->getJsonResult(); $response = $request->getResult();
$response["success"] = $success;
$response["msg"] = $request->getLastError();
} }
} }
} catch (ReflectionException $e) { } catch (ReflectionException $e) {
@ -55,6 +56,6 @@ class ApiRoute extends AbstractRoute {
} }
header("Content-Type: application/json"); header("Content-Type: application/json");
return $response; return json_encode($response);
} }
} }

@ -3,23 +3,24 @@
namespace Objects\Router; namespace Objects\Router;
use Driver\Logger\Logger; use Driver\Logger\Logger;
use Objects\User; use Objects\Context;
class Router { class Router {
private ?User $user; private Context $context;
private Logger $logger; private Logger $logger;
protected array $routes; protected array $routes;
protected array $statusCodeRoutes; protected array $statusCodeRoutes;
public function __construct(?User $user = null) { public function __construct(Context $context) {
$this->user = $user; $this->context = $context;
$this->routes = []; $this->routes = [];
$this->statusCodeRoutes = []; $this->statusCodeRoutes = [];
if ($user) { $sql = $context->getSQL();
if ($sql) {
$this->addRoute(new ApiRoute()); $this->addRoute(new ApiRoute());
$this->logger = new Logger("Router", $user->getSQL()); $this->logger = new Logger("Router", $sql);
} else { } else {
$this->logger = new Logger("Router"); $this->logger = new Logger("Router");
} }
@ -48,7 +49,7 @@ class Router {
if ($route) { if ($route) {
return $route->call($this, $params); return $route->call($this, $params);
} else { } else {
$req = new \Api\Template\Render($this->user); $req = new \Api\Template\Render($this->context);
$res = $req->execute(["file" => "error_document.twig", "parameters" => $params]); $res = $req->execute(["file" => "error_document.twig", "parameters" => $params]);
if ($res) { if ($res) {
return $req->getResult()["html"]; return $req->getResult()["html"];
@ -90,13 +91,13 @@ class Router {
*/ */
namespace Cache; namespace Cache;
use Objects\User; use Objects\Context;
use Objects\Router\Router; use Objects\Router\Router;
class RouterCache extends Router { class RouterCache extends Router {
public function __construct(User \$user) { public function __construct(Context \$context) {
parent::__construct(\$user);$routes parent::__construct(\$context);$routes
} }
} }
"; ";
@ -109,8 +110,8 @@ class RouterCache extends Router {
return true; return true;
} }
public function getUser(): User { public function getContext(): Context {
return $this->user; return $this->context;
} }
public function getLogger(): Logger { public function getLogger(): Logger {

@ -37,9 +37,9 @@ class StaticFileRoute extends AbstractRoute {
} }
$pathInfo = pathinfo($path); $pathInfo = pathinfo($path);
if ($router !== null && ($user = $router->getUser()) !== null) { if ($router !== null) {
$ext = $pathInfo["extension"] ?? ""; $ext = $pathInfo["extension"] ?? "";
if (!$user->getConfiguration()->getSettings()->isExtensionAllowed($ext)) { if (!$router->getContext()->getSettings()->isExtensionAllowed($ext)) {
http_response_code(406); http_response_code(406);
echo "<b>Access restricted:</b> Extension '" . htmlspecialchars($ext) . "' not allowed to serve."; echo "<b>Access restricted:</b> Extension '" . htmlspecialchars($ext) . "' not allowed to serve.";
} }

@ -1,164 +0,0 @@
<?php
namespace Objects;
use DateTime;
use \Driver\SQL\Condition\Compare;
use Driver\SQL\Expression\CurrentTimeStamp;
use Exception;
use Firebase\JWT\JWT;
class Session extends ApiObject {
# in minutes
const DURATION = 60*60*24*14;
private ?int $sessionId;
private User $user;
private int $expires;
private string $ipAddress;
private ?string $os;
private ?string $browser;
private bool $stayLoggedIn;
private string $csrfToken;
public function __construct(User $user, ?int $sessionId, ?string $csrfToken) {
$this->user = $user;
$this->sessionId = $sessionId;
$this->stayLoggedIn = false;
$this->csrfToken = $csrfToken ?? generateRandomString(16);
}
public static function create(User $user, bool $stayLoggedIn = false): ?Session {
$session = new Session($user, null, null);
if ($session->insert($stayLoggedIn)) {
$session->stayLoggedIn = $stayLoggedIn;
return $session;
}
return null;
}
private function updateMetaData() {
$this->expires = time() + Session::DURATION;
$this->ipAddress = is_cli() ? "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 stayLoggedIn(bool $val) {
$this->stayLoggedIn = $val;
}
public function getCookie(): string {
$this->updateMetaData();
$settings = $this->user->getConfiguration()->getSettings();
$token = ['userId' => $this->user->getId(), 'sessionId' => $this->sessionId];
$jwtKey = $settings->getJwtKey();
return JWT::encode($token, $jwtKey->getKeyMaterial(), $jwtKey->getAlgorithm());
}
public function sendCookie(?string $domain = null) {
$domain = empty($domain) ? "" : $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 : 0);
}
public function getExpiresSeconds(): int {
return ($this->stayLoggedIn ? $this->expires - time() : -1);
}
public function jsonSerialize(): array {
return array(
'uid' => $this->sessionId,
'user_id' => $this->user->getId(),
'expires' => $this->expires,
'ipAddress' => $this->ipAddress,
'os' => $this->os,
'browser' => $this->browser,
'csrf_token' => $this->csrfToken
);
}
public function insert(bool $stayLoggedIn = false): bool {
$this->updateMetaData();
$sql = $this->user->getSQL();
$minutes = Session::DURATION;
$data = [
"expires" => (new DateTime())->modify("+$minutes minute"),
"user_id" => $this->user->getId(),
"ipAddress" => $this->ipAddress,
"os" => $this->os,
"browser" => $this->browser,
"data" => json_encode($_SESSION ?? []),
"stay_logged_in" => $stayLoggedIn,
"csrf_token" => $this->csrfToken
];
$success = $sql
->insert("Session", array_keys($data))
->addRow(...array_values($data))
->returning("uid")
->execute();
if ($success) {
$this->sessionId = $this->user->getSQL()->getLastInsertId();
return true;
}
return false;
}
public function destroy(): bool {
session_destroy();
return $this->user->getSQL()->update("Session")
->set("active", false)
->where(new Compare("Session.uid", $this->sessionId))
->where(new Compare("Session.user_id", $this->user->getId()))
->execute();
}
public function update(): bool {
$this->updateMetaData();
$minutes = Session::DURATION;
$sql = $this->user->getSQL();
return
$sql->update("User")
->set("last_online", new CurrentTimeStamp())
->where(new Compare("uid", $this->user->getId()))
->execute() &&
$sql->update("Session")
->set("Session.expires", (new DateTime())->modify("+$minutes second"))
->set("Session.ipAddress", $this->ipAddress)
->set("Session.os", $this->os)
->set("Session.browser", $this->browser)
->set("Session.data", json_encode($_SESSION ?? []))
->set("Session.csrf_token", $this->csrfToken)
->where(new Compare("Session.uid", $this->sessionId))
->where(new Compare("Session.user_id", $this->user->getId()))
->execute();
}
public function getCsrfToken(): string {
return $this->csrfToken;
}
}

@ -3,18 +3,19 @@
namespace Objects\TwoFactor; namespace Objects\TwoFactor;
use Cose\Algorithm\Signature\ECDSA\ECSignature; use Cose\Algorithm\Signature\ECDSA\ECSignature;
use Objects\DatabaseEntity\Attribute\Transient;
use Objects\DatabaseEntity\TwoFactorToken;
class KeyBasedTwoFactorToken extends TwoFactorToken { class KeyBasedTwoFactorToken extends TwoFactorToken {
const TYPE = "fido"; const TYPE = "fido";
private ?string $challenge; #[Transient] private ?string $challenge;
private ?string $credentialId; #[Transient] private ?string $credentialId;
private ?PublicKey $publicKey; #[Transient] private ?PublicKey $publicKey;
public function __construct(string $data, ?int $id = null, bool $confirmed = false) { protected function readData(string $data) {
parent::__construct(self::TYPE, $id, $confirmed); if ($this->isConfirmed()) {
if (!$confirmed) {
$this->challenge = base64_decode($data); $this->challenge = base64_decode($data);
$this->credentialId = null; $this->credentialId = null;
$this->publicKey = null; $this->publicKey = null;
@ -34,7 +35,7 @@ class KeyBasedTwoFactorToken extends TwoFactorToken {
return $this->publicKey; return $this->publicKey;
} }
public function getCredentialId() { public function getCredentialId(): ?string {
return $this->credentialId; return $this->credentialId;
} }

@ -5,22 +5,29 @@ namespace Objects\TwoFactor;
use Base32\Base32; use Base32\Base32;
use chillerlan\QRCode\QRCode; use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions; use chillerlan\QRCode\QROptions;
use Objects\User; use Objects\Context;
use Objects\DatabaseEntity\Attribute\Transient;
use Objects\DatabaseEntity\TwoFactorToken;
use Objects\DatabaseEntity\User;
class TimeBasedTwoFactorToken extends TwoFactorToken { class TimeBasedTwoFactorToken extends TwoFactorToken {
const TYPE = "totp"; const TYPE = "totp";
private string $secret; #[Transient] private string $secret;
public function __construct(string $secret, ?int $id = null, bool $confirmed = false) { public function __construct(string $secret) {
parent::__construct(self::TYPE, $id, $confirmed); parent::__construct(self::TYPE);
$this->secret = $secret; $this->secret = $secret;
} }
public function getUrl(User $user): string { protected function readData(string $data) {
$this->secret = $data;
}
public function getUrl(Context $context): string {
$otpType = self::TYPE; $otpType = self::TYPE;
$name = rawurlencode($user->getUsername()); $name = rawurlencode($context->getUser()->getUsername());
$settings = $user->getConfiguration()->getSettings(); $settings = $context->getSettings();
$urlArgs = [ $urlArgs = [
"secret" => $this->secret, "secret" => $this->secret,
"issuer" => $settings->getSiteName(), "issuer" => $settings->getSiteName(),
@ -30,10 +37,10 @@ class TimeBasedTwoFactorToken extends TwoFactorToken {
return "otpauth://$otpType/$name?$urlArgs"; return "otpauth://$otpType/$name?$urlArgs";
} }
public function generateQRCode(User $user) { public function generateQRCode(Context $context) {
$options = new QROptions(['outputType' => QRCode::OUTPUT_IMAGE_PNG, "imageBase64" => false]); $options = new QROptions(['outputType' => QRCode::OUTPUT_IMAGE_PNG, "imageBase64" => false]);
$qrcode = new QRCode($options); $qrcode = new QRCode($options);
return $qrcode->render($this->getUrl($user)); return $qrcode->render($this->getUrl($context));
} }
public function generate(?int $at = null, int $length = 6, int $period = 30): string { public function generate(?int $at = null, int $length = 6, int $period = 30): string {

@ -1,62 +0,0 @@
<?php
namespace Objects\TwoFactor;
use Objects\ApiObject;
abstract class TwoFactorToken extends ApiObject {
private ?int $id;
private string $type;
private bool $confirmed;
private bool $authenticated;
public function __construct(string $type, ?int $id = null, bool $confirmed = false) {
$this->id = $id;
$this->type = $type;
$this->confirmed = $confirmed;
$this->authenticated = $_SESSION["2faAuthenticated"] ?? false;
}
public function jsonSerialize(): array {
return [
"id" => $this->id,
"type" => $this->type,
"confirmed" => $this->confirmed,
"authenticated" => $this->authenticated,
];
}
public abstract function getData(): string;
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(string $type, string $data, ?int $id = null, bool $confirmed = false) {
if ($type === TimeBasedTwoFactorToken::TYPE) {
return new TimeBasedTwoFactorToken($data, $id, $confirmed);
} else if ($type === KeyBasedTwoFactorToken::TYPE) {
return new KeyBasedTwoFactorToken($data, $id, $confirmed);
} else {
// TODO: error message
return null;
}
}
public function isAuthenticated(): bool {
return $this->authenticated;
}
}

@ -1,376 +0,0 @@
<?php
namespace Objects;
use Configuration\Configuration;
use Driver\SQL\Condition\CondAnd;
use Exception;
use Driver\SQL\SQL;
use Driver\SQL\Condition\Compare;
use Firebase\JWT\JWT;
use Objects\TwoFactor\TwoFactorToken;
class User extends ApiObject {
private ?SQL $sql;
private Configuration $configuration;
private bool $loggedIn;
private ?Session $session;
private int $uid;
private string $username;
private string $fullName;
private ?string $email;
private ?string $profilePicture;
private Language $language;
private array $groups;
private ?GpgKey $gpgKey;
private ?TwoFactorToken $twoFactorToken;
public function __construct($configuration) {
$this->configuration = $configuration;
$this->reset();
$this->connectDB();
if (!is_cli()) {
@session_start();
$this->setLanguage(Language::DEFAULT_LANGUAGE());
$this->parseCookies();
}
}
public function __destruct() {
if ($this->sql && $this->sql->isConnected()) {
$this->sql->close();
}
}
public function connectDB(): bool {
$databaseConf = $this->configuration->getDatabase();
if ($databaseConf) {
$this->sql = SQL::createConnection($databaseConf);
if ($this->sql->isConnected()) {
$settings = $this->configuration->getSettings();
$settings->loadFromDatabase($this);
return true;
}
} else {
$this->sql = null;
}
return false;
}
public function getId(): int {
return $this->uid;
}
public function isLoggedIn(): bool {
return $this->loggedIn;
}
public function getUsername(): string {
return $this->username;
}
public function getFullName(): string {
return $this->fullName;
}
public function getEmail(): ?string {
return $this->email;
}
public function getSQL(): ?SQL {
return $this->sql;
}
public function getLanguage(): Language {
return $this->language;
}
public function setLanguage(Language $language) {
$this->language = $language;
$language->load();
}
public function getSession(): ?Session {
return $this->session;
}
public function getConfiguration(): Configuration {
return $this->configuration;
}
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 {
$debugInfo = array(
'loggedIn' => $this->loggedIn,
'language' => $this->language->getName(),
);
if ($this->loggedIn) {
$debugInfo['uid'] = $this->uid;
$debugInfo['username'] = $this->username;
}
return $debugInfo;
}
public function jsonSerialize(): array {
if ($this->isLoggedIn()) {
return array(
'uid' => $this->uid,
'name' => $this->username,
'fullName' => $this->fullName,
'profilePicture' => $this->profilePicture,
'email' => $this->email,
'groups' => $this->groups,
'language' => $this->language->jsonSerialize(),
'session' => $this->session->jsonSerialize(),
"gpg" => ($this->gpgKey ? $this->gpgKey->jsonSerialize() : null),
"2fa" => ($this->twoFactorToken ? $this->twoFactorToken->jsonSerialize() : null),
);
} else {
return array(
'language' => $this->language->jsonSerialize(),
);
}
}
private function reset() {
$this->uid = 0;
$this->username = '';
$this->email = '';
$this->groups = [];
$this->loggedIn = false;
$this->session = null;
$this->profilePicture = null;
$this->gpgKey = null;
$this->twoFactorToken = null;
}
public function logout(): bool {
$success = true;
if ($this->loggedIn) {
$success = $this->session->destroy();
$this->reset();
}
return $success;
}
public function updateLanguage($lang): bool {
if ($this->sql) {
$request = new \Api\Language\Set($this);
return $request->execute(array("langCode" => $lang));
} else {
return false;
}
}
public function sendCookies() {
$baseUrl = $this->getConfiguration()->getSettings()->getBaseUrl();
$domain = parse_url($baseUrl, PHP_URL_HOST);
if ($this->loggedIn) {
$this->session->sendCookie($domain);
}
$this->language->sendCookie($domain);
session_write_close();
}
/**
* @param $userId user's id
* @param $sessionId session's id
* @param bool $sessionUpdate update session information, including session's lifetime and browser information
* @return bool true, if the data could be loaded
*/
public function loadSession($userId, $sessionId, bool $sessionUpdate = true): bool {
$userRow = $this->loadUser("Session", ["Session.data", "Session.stay_logged_in", "Session.csrf_token"], [
new Compare("User.uid", $userId),
new Compare("Session.uid", $sessionId),
new Compare("Session.active", true),
]);
if ($userRow !== false) {
$this->session = new Session($this, $sessionId, $userRow["csrf_token"]);
$this->session->setData(json_decode($userRow["data"] ?? '{}', true));
$this->session->stayLoggedIn($this->sql->parseBool($userRow["stay_logged_in"]));
if ($sessionUpdate) {
$this->session->update();
}
$this->loggedIn = true;
}
return $userRow !== false;
}
private 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->getJwtKey());
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
}
}
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']);
}
}
public function createSession(int $userId, bool $stayLoggedIn = false): bool {
$this->uid = $userId;
$this->session = Session::create($this, $stayLoggedIn);
if ($this->session) {
$this->loggedIn = true;
return true;
}
return false;
}
private function loadUser(string $table, array $columns, array $conditions) {
$userRow = $this->sql->select(
// User meta
"User.uid as userId", "User.name", "User.email", "User.fullName", "User.profilePicture", "User.confirmed",
// GPG
"User.gpg_id", "GpgKey.confirmed as gpg_confirmed", "GpgKey.fingerprint as gpg_fingerprint",
"GpgKey.expires as gpg_expires", "GpgKey.algorithm as gpg_algorithm",
// 2FA
"User.2fa_id", "2FA.confirmed as 2fa_confirmed", "2FA.data as 2fa_data", "2FA.type as 2fa_type",
// Language
"Language.uid as langId", "Language.code as langCode", "Language.name as langName",
// additional data
...$columns)
->from("User")
->innerJoin("$table", "$table.user_id", "User.uid")
->leftJoin("Language", "User.language_id", "Language.uid")
->leftJoin("GpgKey", "User.gpg_id", "GpgKey.uid")
->leftJoin("2FA", "User.2fa_id", "2FA.uid")
->where(new CondAnd(...$conditions))
->first()
->execute();
if ($userRow === null || $userRow === false) {
return false;
}
// Meta data
$userId = $userRow["userId"];
$this->uid = $userId;
$this->username = $userRow['name'];
$this->fullName = $userRow["fullName"];
$this->email = $userRow['email'];
$this->profilePicture = $userRow["profilePicture"];
// GPG
if (!empty($userRow["gpg_id"])) {
$this->gpgKey = new GpgKey($userRow["gpg_id"], $this->sql->parseBool($userRow["gpg_confirmed"]),
$userRow["gpg_fingerprint"], $userRow["gpg_algorithm"], $userRow["gpg_expires"]
);
}
// 2FA
if (!empty($userRow["2fa_id"])) {
$this->twoFactorToken = TwoFactorToken::newInstance($userRow["2fa_type"], $userRow["2fa_data"],
$userRow["2fa_id"], $this->sql->parseBool($userRow["2fa_confirmed"]));
}
// Language
if (!is_null($userRow['langId'])) {
$this->setLanguage(Language::newInstance($userRow['langId'], $userRow['langCode'], $userRow['langName']));
}
// select groups
$groupRows = $this->sql->select("Group.uid as groupId", "Group.name as groupName")
->from("UserGroup")
->where(new Compare("UserGroup.user_id", $userId))
->innerJoin("Group", "UserGroup.group_id", "Group.uid")
->execute();
if (is_array($groupRows)) {
foreach ($groupRows as $row) {
$this->groups[$row["groupId"]] = $row["groupName"];
}
}
return $userRow;
}
public function loadApiKey($apiKey): bool {
if ($this->loggedIn) {
return true;
}
$userRow = $this->loadUser("ApiKey", [], [
new Compare("ApiKey.api_key", $apiKey),
new Compare("valid_until", $this->sql->currentTimestamp(), ">"),
new Compare("ApiKey.active", 1),
]);
// User must be confirmed to use API-Keys
if ($userRow === false || !$this->sql->parseBool($userRow["confirmed"])) {
return false;
}
return true;
}
public function processVisit() {
if ($this->sql && $this->sql->isConnected() && isset($_COOKIE["PHPSESSID"]) && !empty($_COOKIE["PHPSESSID"])) {
if ($this->isBot()) {
return;
}
$cookie = $_COOKIE["PHPSESSID"];
$req = new \Api\Visitors\ProcessVisit($this);
$req->execute(array("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;
}
}

@ -10,7 +10,7 @@ if (is_file($autoLoad)) {
require_once $autoLoad; require_once $autoLoad;
} }
define("WEBBASE_VERSION", "1.5.2"); define("WEBBASE_VERSION", "2.0.0-alpha");
spl_autoload_extensions(".php"); spl_autoload_extensions(".php");
spl_autoload_register(function ($class) { spl_autoload_register(function ($class) {
@ -28,10 +28,6 @@ spl_autoload_register(function ($class) {
} }
}); });
function is_cli(): bool {
return php_sapi_name() === "cli";
}
function getProtocol(): string { function getProtocol(): string {
$isSecure = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') || $isSecure = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ||
(!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') || (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') ||
@ -246,7 +242,7 @@ function getClassName($class, bool $short = true): string {
} }
function createError($msg) { function createError($msg) {
return json_encode(array("success" => false, "msg" => $msg)); return ["success" => false, "msg" => $msg];
} }
function downloadFile($handle, $offset = 0, $length = null): bool { function downloadFile($handle, $offset = 0, $length = null): bool {
@ -278,3 +274,8 @@ function parseClass($class): string {
$parts = array_map('ucfirst', $parts); $parts = array_map('ucfirst', $parts);
return implode("\\", $parts); return implode("\\", $parts);
} }
function isClass(string $str): bool {
$path = getClassPath($str);
return is_file($path) && class_exists($str);
}

@ -1,5 +1,5 @@
FROM composer:latest AS composer FROM composer:latest AS composer
FROM php:7.4-fpm FROM php:8.0-fpm
WORKDIR "/application" WORKDIR "/application"
RUN mkdir -p /application/core/Configuration RUN mkdir -p /application/core/Configuration
RUN chown -R www-data:www-data /application RUN chown -R www-data:www-data /application

@ -20,12 +20,12 @@ if (!is_readable(getClassPath(Configuration::class))) {
die(json_encode([ "success" => false, "msg" => "Configuration class is not readable, check permissions before proceeding." ])); die(json_encode([ "success" => false, "msg" => "Configuration class is not readable, check permissions before proceeding." ]));
} }
$config = new Configuration(); $context = new \Objects\Context();
$user = new Objects\User($config); $sql = $context->initSQL();
$sql = $user->getSQL(); $settings = $context->getSettings();
$settings = $config->getSettings(); $context->parseCookies();
$installation = !$sql || ($sql->isConnected() && !$settings->isInstalled());
$installation = !$sql || ($sql->isConnected() && !$settings->isInstalled());
$requestedUri = $_GET["site"] ?? $_GET["api"] ?? $_SERVER["REQUEST_URI"]; $requestedUri = $_GET["site"] ?? $_GET["api"] ?? $_SERVER["REQUEST_URI"];
if ($installation) { if ($installation) {
@ -34,7 +34,7 @@ if ($installation) {
$response = "Redirecting to <a href=\"/\">/</a>"; $response = "Redirecting to <a href=\"/\">/</a>";
header("Location: /"); header("Location: /");
} else { } else {
$document = new Documents\Install(new Router($user)); $document = new Documents\Install(new Router($context));
$response = $document->getCode(); $response = $document->getCode();
} }
} else { } else {
@ -45,17 +45,17 @@ if ($installation) {
if (is_file($routerCachePath)) { if (is_file($routerCachePath)) {
@include_once $routerCachePath; @include_once $routerCachePath;
if (class_exists($routerCacheClass)) { if (class_exists($routerCacheClass)) {
$router = new $routerCacheClass($user); $router = new $routerCacheClass($context);
} }
} }
if ($router === null) { if ($router === null) {
$req = new \Api\Routes\GenerateCache($user); $req = new \Api\Routes\GenerateCache($context);
if ($req->execute()) { if ($req->execute()) {
$router = $req->getRouter(); $router = $req->getRouter();
} else { } else {
$message = "Unable to generate router cache: " . $req->getLastError(); $message = "Unable to generate router cache: " . $req->getLastError();
$response = (new Router($user))->returnStatusCode(500, [ "message" => $message ]); $response = (new Router($context))->returnStatusCode(500, [ "message" => $message ]);
} }
} }
@ -68,8 +68,8 @@ if ($installation) {
} }
} }
$user->processVisit(); $context->processVisit();
} }
$user->sendCookies(); $context->sendCookies();
die($response); die($response);

2
js/admin.min.js vendored

File diff suppressed because one or more lines are too long

@ -1,21 +1,33 @@
<?php <?php
use Configuration\Configuration;
use Driver\SQL\Query\CreateTable;
use Driver\SQL\SQL;
use Objects\Context;
use Objects\DatabaseEntity\DatabaseEntityHandler;
use Objects\DatabaseEntity\User;
class DatabaseEntityTest extends \PHPUnit\Framework\TestCase { class DatabaseEntityTest extends \PHPUnit\Framework\TestCase {
static \Objects\User $USER; static User $USER;
static \Driver\SQL\SQL $SQL; static SQL $SQL;
static \Objects\DatabaseEntity\DatabaseEntityHandler $HANDLER; static Context $CONTEXT;
static DatabaseEntityHandler $HANDLER;
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
parent::setUpBeforeClass(); parent::setUpBeforeClass();
self::$USER = new Objects\User(new \Configuration\Configuration()); self::$CONTEXT = new Context();
self::$SQL = self::$USER->getSQL(); if (!self::$CONTEXT->initSQL()) {
throw new Exception("Could not establish database connection");
}
self::$SQL = self::$CONTEXT->getSQL();
self::$HANDLER = TestEntity::getHandler(self::$SQL); self::$HANDLER = TestEntity::getHandler(self::$SQL);
self::$HANDLER->getLogger()->unitTestMode(); self::$HANDLER->getLogger()->unitTestMode();
} }
public function testCreateTable() { public function testCreateTable() {
$this->assertInstanceOf(\Driver\SQL\Query\CreateTable::class, self::$HANDLER->getTableQuery()); $this->assertInstanceOf(CreateTable::class, self::$HANDLER->getTableQuery());
$this->assertTrue(self::$HANDLER->createTable()); $this->assertTrue(self::$HANDLER->createTable());
} }
@ -60,7 +72,8 @@ class DatabaseEntityTest extends \PHPUnit\Framework\TestCase {
$allEntities = TestEntity::findAll(self::$SQL); $allEntities = TestEntity::findAll(self::$SQL);
$this->assertIsArray($allEntities); $this->assertIsArray($allEntities);
$this->assertCount(1, $allEntities); $this->assertCount(1, $allEntities);
$this->assertEquals($entityId, $allEntities[0]->getId()); $this->assertTrue(array_key_exists($entityId, $allEntities));
$this->assertEquals($entityId, $allEntities[$entityId]->getId());
// delete // delete
$this->assertTrue($entity->delete(self::$SQL)); $this->assertTrue($entity->delete(self::$SQL));
@ -94,4 +107,16 @@ class TestEntity extends \Objects\DatabaseEntity\DatabaseEntity {
public float $d; public float $d;
public \DateTime $e; public \DateTime $e;
public ?int $f; public ?int $f;
public function jsonSerialize(): array {
return [
"id" => $this->getId(),
"a" => $this->a,
"b" => $this->b,
"c" => $this->c,
"d" => $this->d,
"e" => $this->e,
"f" => $this->f,
];
}
} }

@ -2,7 +2,8 @@
use Api\Request; use Api\Request;
use Configuration\Configuration; use Configuration\Configuration;
use Objects\User; use Objects\Context;
use Objects\DatabaseEntity\User;
function __new_header_impl(string $line) { function __new_header_impl(string $line) {
if (preg_match("/^HTTP\/([0-9.]+) (\d+) (.*)$/", $line, $m)) { if (preg_match("/^HTTP\/([0-9.]+) (\d+) (.*)$/", $line, $m)) {
@ -34,6 +35,7 @@ class RequestTest extends \PHPUnit\Framework\TestCase {
const FUNCTION_OVERRIDES = ["header", "http_response_code"]; const FUNCTION_OVERRIDES = ["header", "http_response_code"];
static User $USER; static User $USER;
static User $USER_LOGGED_IN; static User $USER_LOGGED_IN;
static Context $CONTEXT;
static ?string $SENT_CONTENT; static ?string $SENT_CONTENT;
static array $SENT_HEADERS; static array $SENT_HEADERS;
@ -41,13 +43,9 @@ class RequestTest extends \PHPUnit\Framework\TestCase {
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
$config = new Configuration(); RequestTest::$CONTEXT = new Context();
RequestTest::$USER = new User($config); if (!RequestTest::$CONTEXT->initSQL()) {
RequestTest::$USER_LOGGED_IN = new User($config);
if (!RequestTest::$USER->getSQL() || !RequestTest::$USER->getSQL()->isConnected()) {
throw new Exception("Could not establish database connection"); throw new Exception("Could not establish database connection");
} else {
RequestTest::$USER->setLanguage(\Objects\Language::DEFAULT_LANGUAGE());
} }
if (!function_exists("runkit7_function_rename") || !function_exists("runkit7_function_remove")) { if (!function_exists("runkit7_function_rename") || !function_exists("runkit7_function_remove")) {
@ -65,7 +63,7 @@ class RequestTest extends \PHPUnit\Framework\TestCase {
} }
public static function tearDownAfterClass(): void { public static function tearDownAfterClass(): void {
RequestTest::$USER->getSQL()->close(); RequestTest::$CONTEXT->getSQL()?->close();
foreach (self::FUNCTION_OVERRIDES as $functionName) { foreach (self::FUNCTION_OVERRIDES as $functionName) {
runkit7_function_remove($functionName); runkit7_function_remove($functionName);
runkit7_function_rename("__orig_${functionName}_impl", $functionName); runkit7_function_rename("__orig_${functionName}_impl", $functionName);
@ -74,7 +72,7 @@ class RequestTest extends \PHPUnit\Framework\TestCase {
private function simulateRequest(Request $request, string $method, array $get = [], array $post = [], array $headers = []): bool { private function simulateRequest(Request $request, string $method, array $get = [], array $post = [], array $headers = []): bool {
if (!is_cli()) { if (!self::$CONTEXT->isCLI()) {
self::throwException(new \Exception("Cannot simulate request outside cli")); self::throwException(new \Exception("Cannot simulate request outside cli"));
} }
@ -97,7 +95,7 @@ class RequestTest extends \PHPUnit\Framework\TestCase {
public function testAllMethods() { public function testAllMethods() {
// all methods allowed // all methods allowed
$allMethodsAllowed = new RequestAllMethods(RequestTest::$USER, true); $allMethodsAllowed = new RequestAllMethods(RequestTest::$CONTEXT, true);
$this->assertTrue($this->simulateRequest($allMethodsAllowed, "GET"), $allMethodsAllowed->getLastError()); $this->assertTrue($this->simulateRequest($allMethodsAllowed, "GET"), $allMethodsAllowed->getLastError());
$this->assertTrue($this->simulateRequest($allMethodsAllowed, "POST"), $allMethodsAllowed->getLastError()); $this->assertTrue($this->simulateRequest($allMethodsAllowed, "POST"), $allMethodsAllowed->getLastError());
$this->assertFalse($this->simulateRequest($allMethodsAllowed, "PUT"), $allMethodsAllowed->getLastError()); $this->assertFalse($this->simulateRequest($allMethodsAllowed, "PUT"), $allMethodsAllowed->getLastError());
@ -109,7 +107,7 @@ class RequestTest extends \PHPUnit\Framework\TestCase {
public function testOnlyPost() { public function testOnlyPost() {
// only post allowed // only post allowed
$onlyPostAllowed = new RequestOnlyPost(RequestTest::$USER, true); $onlyPostAllowed = new RequestOnlyPost(RequestTest::$CONTEXT, true);
$this->assertFalse($this->simulateRequest($onlyPostAllowed, "GET")); $this->assertFalse($this->simulateRequest($onlyPostAllowed, "GET"));
$this->assertEquals("This method is not allowed", $onlyPostAllowed->getLastError(), $onlyPostAllowed->getLastError()); $this->assertEquals("This method is not allowed", $onlyPostAllowed->getLastError(), $onlyPostAllowed->getLastError());
$this->assertEquals(405, self::$SENT_STATUS_CODE); $this->assertEquals(405, self::$SENT_STATUS_CODE);
@ -121,25 +119,25 @@ class RequestTest extends \PHPUnit\Framework\TestCase {
public function testPrivate() { public function testPrivate() {
// private method // private method
$privateExternal = new RequestPrivate(RequestTest::$USER, true); $privateExternal = new RequestPrivate(RequestTest::$CONTEXT, true);
$this->assertFalse($this->simulateRequest($privateExternal, "GET")); $this->assertFalse($this->simulateRequest($privateExternal, "GET"));
$this->assertEquals("This function is private.", $privateExternal->getLastError()); $this->assertEquals("This function is private.", $privateExternal->getLastError());
$this->assertEquals(403, self::$SENT_STATUS_CODE); $this->assertEquals(403, self::$SENT_STATUS_CODE);
$privateInternal = new RequestPrivate(RequestTest::$USER, false); $privateInternal = new RequestPrivate(RequestTest::$CONTEXT, false);
$this->assertTrue($privateInternal->execute()); $this->assertTrue($privateInternal->execute());
} }
public function testDisabled() { public function testDisabled() {
// disabled method // disabled method
$disabledMethod = new RequestDisabled(RequestTest::$USER, true); $disabledMethod = new RequestDisabled(RequestTest::$CONTEXT, true);
$this->assertFalse($this->simulateRequest($disabledMethod, "GET")); $this->assertFalse($this->simulateRequest($disabledMethod, "GET"));
$this->assertEquals("This function is currently disabled.", $disabledMethod->getLastError(), $disabledMethod->getLastError()); $this->assertEquals("This function is currently disabled.", $disabledMethod->getLastError(), $disabledMethod->getLastError());
$this->assertEquals(503, self::$SENT_STATUS_CODE); $this->assertEquals(503, self::$SENT_STATUS_CODE);
} }
public function testLoginRequired() { public function testLoginRequired() {
$loginRequired = new RequestLoginRequired(RequestTest::$USER, true); $loginRequired = new RequestLoginRequired(RequestTest::$CONTEXT, true);
$this->assertFalse($this->simulateRequest($loginRequired, "GET")); $this->assertFalse($this->simulateRequest($loginRequired, "GET"));
$this->assertEquals("You are not logged in.", $loginRequired->getLastError(), $loginRequired->getLastError()); $this->assertEquals("You are not logged in.", $loginRequired->getLastError(), $loginRequired->getLastError());
$this->assertEquals(401, self::$SENT_STATUS_CODE); $this->assertEquals(401, self::$SENT_STATUS_CODE);
@ -147,8 +145,8 @@ class RequestTest extends \PHPUnit\Framework\TestCase {
} }
abstract class TestRequest extends Request { abstract class TestRequest extends Request {
public function __construct(User $user, bool $externalCall = false, $params = []) { public function __construct(Context $context, bool $externalCall = false, $params = []) {
parent::__construct($user, $externalCall, $params); parent::__construct($context, $externalCall, $params);
} }
protected function _die(string $data = ""): bool { protected function _die(string $data = ""): bool {
@ -162,35 +160,35 @@ abstract class TestRequest extends Request {
} }
class RequestAllMethods extends TestRequest { class RequestAllMethods extends TestRequest {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, []); parent::__construct($context, $externalCall, []);
} }
} }
class RequestOnlyPost extends TestRequest { class RequestOnlyPost extends TestRequest {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, []); parent::__construct($context, $externalCall, []);
$this->forbidMethod("GET"); $this->forbidMethod("GET");
} }
} }
class RequestPrivate extends TestRequest { class RequestPrivate extends TestRequest {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, []); parent::__construct($context, $externalCall, []);
$this->isPublic = false; $this->isPublic = false;
} }
} }
class RequestDisabled extends TestRequest { class RequestDisabled extends TestRequest {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, []); parent::__construct($context, $externalCall, []);
$this->isDisabled = true; $this->isDisabled = true;
} }
} }
class RequestLoginRequired extends TestRequest { class RequestLoginRequired extends TestRequest {
public function __construct(User $user, bool $externalCall = false) { public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($user, $externalCall, []); parent::__construct($context, $externalCall, []);
$this->loginRequired = true; $this->loginRequired = true;
} }
} }

@ -1,20 +1,17 @@
<?php <?php
use Configuration\Configuration; use Objects\Context;
use Objects\Router\EmptyRoute; use Objects\Router\EmptyRoute;
use Objects\Router\Router; use Objects\Router\Router;
use Objects\User;
class RouterTest extends \PHPUnit\Framework\TestCase { class RouterTest extends \PHPUnit\Framework\TestCase {
private static User $USER;
private static Router $ROUTER; private static Router $ROUTER;
private static Context $CONTEXT;
public static function setUpBeforeClass(): void { public static function setUpBeforeClass(): void {
RouterTest::$CONTEXT = new Context();
$config = new Configuration(); RouterTest::$ROUTER = new Router(RouterTest::$CONTEXT);
RouterTest::$USER = new User($config);
RouterTest::$ROUTER = new Router(RouterTest::$USER);
} }
public function testSimpleRoutes() { public function testSimpleRoutes() {

@ -3,7 +3,7 @@
use Base32\Base32; use Base32\Base32;
use Configuration\Configuration; use Configuration\Configuration;
use Objects\TwoFactor\TimeBasedTwoFactorToken; use Objects\TwoFactor\TimeBasedTwoFactorToken;
use Objects\User; use Objects\DatabaseEntity\User;
class TimeBasedTwoFactorTokenTest extends PHPUnit\Framework\TestCase { class TimeBasedTwoFactorTokenTest extends PHPUnit\Framework\TestCase {
@ -31,13 +31,14 @@ class TimeBasedTwoFactorTokenTest extends PHPUnit\Framework\TestCase {
public function testURL() { public function testURL() {
$secret = Base32::encode("12345678901234567890"); $secret = Base32::encode("12345678901234567890");
$configuration = new Configuration(); $context = new \Objects\Context();
$user = new User($configuration);
// $context->
$token = new TimeBasedTwoFactorToken($secret); $token = new TimeBasedTwoFactorToken($secret);
$siteName = $configuration->getSettings()->getSiteName(); $siteName = $context->getSettings()->getSiteName();
$username = $user->getUsername(); $username = $context->getUser()->getUsername();
$url = $token->getUrl($user); $url = $token->getUrl($context);
$this->assertEquals("otpauth://totp/$username?secret=$secret&issuer=$siteName", $url); $this->assertEquals("otpauth://totp/$username?secret=$secret&issuer=$siteName", $url);
} }
} }