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
namespace Api;
use Api\Parameter\Parameter;
use Objects\User;
use Objects\DatabaseEntity\User;
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:
```php
$req = new \Api\Mail\Send($user);
$req = new \Api\Mail\Send($context);
$success = $req->execute(array(
"to" => "mail@example.org",
"subject" => "Example Mail",

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

@ -25,10 +25,10 @@ export default function Header(props) {
let notificationItems = [];
for (let i = 0; i < parent.notifications.length; i++) {
const notification = parent.notifications[i];
const uid = notification.uid;
const id = notification.id;
const createdAt = getPeriodString(notification["created_at"]);
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}
<span className={"ml-2"}>{notification.title}</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]) {
let timeString = moment(event.timestamp).fromNow();
elements.push(
<div key={"time-entry-" + event.uid}>
<div key={"time-entry-" + event.id}>
<Icon icon={event.icon} className={"bg-" + color}/>
<div className="timeline-item">
<span className="time"><Icon icon={"clock"}/> {timeString}</span>

@ -161,12 +161,12 @@ export default class UserOverview extends React.Component {
createUserCard() {
let userRows = [];
for (let uid in this.state.users.data) {
if (!this.state.users.data.hasOwnProperty(uid)) {
for (let id in this.state.users.data) {
if (!this.state.users.data.hasOwnProperty(id)) {
continue;
}
let user = this.state.users.data[uid];
let user = this.state.users.data[id];
let confirmedIcon = <Icon icon={user["confirmed"] ? "check" : "times"}/>;
let groups = [];
@ -184,7 +184,7 @@ export default class UserOverview extends React.Component {
}
userRows.push(
<tr key={"user-" + uid}>
<tr key={"user-" + id}>
<td>{user.name}</td>
<td>{user.email}</td>
<td>{groups}</td>
@ -197,7 +197,7 @@ export default class UserOverview extends React.Component {
</td>
<td className={"text-center"}>{confirmedIcon}</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"}
data-tip={"Modify user details & group membership"}
data-type={"info"} data-place={"right"}/>
@ -291,15 +291,15 @@ export default class UserOverview extends React.Component {
createGroupCard() {
let groupRows = [];
for (let uid in this.state.groups.data) {
if (!this.state.groups.data.hasOwnProperty(uid)) {
for (let id in this.state.groups.data) {
if (!this.state.groups.data.hasOwnProperty(id)) {
continue;
}
let group = this.state.groups.data[uid];
let group = this.state.groups.data[id];
groupRows.push(
<tr key={"group-" + uid}>
<tr key={"group-" + id}>
<td>{group.name}</td>
<td className={"text-center"}>{group["memberCount"]}</td>
<td>
@ -309,7 +309,7 @@ export default class UserOverview extends React.Component {
</td>
<td>
<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-place={"bottom"}/>
</td>
@ -395,11 +395,11 @@ export default class UserOverview extends React.Component {
</div>;
}
onDeleteGroup(e, uid) {
onDeleteGroup(e, id) {
e.stopPropagation();
this.parent.showDialog("Are you really sure you want to delete this group?", "Delete Group?", ["Yes", "No"], (btn) => {
if (btn === "Yes") {
this.parent.api.deleteGroup(uid).then((res) => {
this.parent.api.deleteGroup(id).then((res) => {
if (!res.success) {
let errors = this.state.errors.slice();
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';
include_once 'core/constants.php';
use Configuration\Configuration;
use Configuration\DatabaseScript;
use Driver\SQL\Column\Column;
use Driver\SQL\Condition\Compare;
use Driver\SQL\Condition\CondIn;
use Driver\SQL\Expression\DateSub;
use Driver\SQL\SQL;
use Objects\ConnectionData;
use Objects\User;
function printLine(string $line = "") {
echo $line . PHP_EOL;
@ -24,10 +23,6 @@ function _exit(string $line = "") {
die();
}
if (!is_cli()) {
_exit("Can only be executed via CLI");
}
function getDatabaseConfig(): ConnectionData {
$configClass = "\\Configuration\\Database";
$file = getClassPath($configClass);
@ -39,8 +34,12 @@ function getDatabaseConfig(): ConnectionData {
return new $configClass();
}
$config = new Configuration();
$database = $config->getDatabase();
$context = new \Objects\Context();
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 (count($argv) < 3 || $argv[1] !== "db" || !in_array($argv[2], ["shell", "import", "export"])) {
$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;
$user = new User($config);
if (!$user->getSQL() || !$user->getSQL()->isConnected()) {
@ -58,6 +57,17 @@ function getUser(): ?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() {
@ -100,8 +110,7 @@ function handleDatabase(array $argv) {
_exit("Usage: cli.php db migrate <class name>");
}
$user = getUser() or die();
$sql = $user->getSQL();
$sql = connectSQL() or die();
applyPatch($sql, $class);
} else if (in_array($action, ["export", "import", "shell"])) {
@ -193,9 +202,7 @@ function handleDatabase(array $argv) {
proc_close($process);
}
} else if ($action === "clean") {
$user = getUser() or die();
$sql = $user->getSQL();
$sql = connectSQL() or die();
printLine("Deleting user related data older than 90 days...");
// 1st: Select all related tables and entities
@ -221,9 +228,9 @@ function handleDatabase(array $argv) {
}
// 2nd: delete!
foreach ($tables as $table => $uids) {
foreach ($tables as $table => $ids) {
$success = $sql->delete($table)
->where(new CondIn(new Column("uid"), $uids))
->where(new CondIn(new Column("id"), $ids))
->execute();
if (!$success) {
@ -336,9 +343,8 @@ function onMaintenance(array $argv) {
$newPatchFiles = array_diff($newPatchFiles, $oldPatchFiles);
if (count($newPatchFiles) > 0) {
printLine("Applying new database patches");
$user = getUser();
if ($user) {
$sql = $user->getSQL();
$sql = connectSQL();
if ($sql) {
foreach ($newPatchFiles as $patchFile) {
if (preg_match("/core\/Configuration\/(Patch\/.*)\.class\.php/", $patchFile, $match)) {
$patchName = $match[1];
@ -408,12 +414,13 @@ function printTable(array $head, array $body) {
}
function onSettings(array $argv) {
$user = getUser() or die();
global $context;
$sql = connectSQL() or die();
$action = $argv[2] ?? "list";
if ($action === "list" || $action === "get") {
$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]);
if (!$success) {
_exit("Error listings settings: " . $req->getLastError());
@ -430,7 +437,7 @@ function onSettings(array $argv) {
} else {
$key = $argv[3];
$value = $argv[4];
$req = new Api\Settings\Set($user);
$req = new Api\Settings\Set($context);
$success = $req->execute(["settings" => [$key => $value]]);
if (!$success) {
_exit("Error updating settings: " . $req->getLastError());
@ -441,7 +448,7 @@ function onSettings(array $argv) {
_exit("Usage: $argv[0] settings $argv[2] <key>");
} else {
$key = $argv[3];
$req = new Api\Settings\Set($user);
$req = new Api\Settings\Set($context);
$success = $req->execute(["settings" => [$key => null]]);
if (!$success) {
_exit("Error updating settings: " . $req->getLastError());
@ -453,18 +460,18 @@ function onSettings(array $argv) {
}
function onRoutes(array $argv) {
$user = getUser() or die();
global $context;
$sql = connectSQL() or die();
$action = $argv[2] ?? "list";
if ($action === "list") {
$req = new Api\Routes\Fetch($user);
$req = new Api\Routes\Fetch($context);
$success = $req->execute();
if (!$success) {
_exit("Error fetching routes: " . $req->getLastError());
} else {
$routes = $req->getResult()["routes"];
$head = ["uid", "request", "action", "target", "extra", "active", "exact"];
$head = ["id", "request", "action", "target", "extra", "active", "exact"];
// strict boolean
foreach ($routes as &$route) {
@ -486,7 +493,7 @@ function onRoutes(array $argv) {
"extra" => $argv[7] ?? "",
);
$req = new Api\Routes\Add($user);
$req = new Api\Routes\Add($context);
$success = $req->execute($params);
if (!$success) {
_exit($req->getLastError());
@ -497,13 +504,13 @@ function onRoutes(array $argv) {
$uid = $argv[3] ?? null;
if ($uid === null || ($action === "modify" && count($argv) < 7)) {
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 {
_exit("Usage: cli.php routes $action <uid>");
_exit("Usage: cli.php routes $action <id>");
}
}
$params = ["uid" => $uid];
$params = ["id" => $uid];
if ($action === "remove") {
$input = null;
do {
@ -513,13 +520,13 @@ function onRoutes(array $argv) {
echo "Remove route #$uid? (y|n): ";
} while(($input = trim(fgets(STDIN))) !== "y");
$req = new Api\Routes\Remove($user);
$req = new Api\Routes\Remove($context);
} else if ($action === "enable") {
$req = new Api\Routes\Enable($user);
$req = new Api\Routes\Enable($context);
} else if ($action === "disable") {
$req = new Api\Routes\Disable($user);
$req = new Api\Routes\Disable($context);
} else if ($action === "modify") {
$req = new Api\Routes\Update($user);
$req = new Api\Routes\Update($context);
$params["request"] = $argv[4];
$params["action"] = $argv[5];
$params["target"] = $argv[6];
@ -597,14 +604,15 @@ function onTest($argv) {
}
function onMail($argv) {
global $context;
$action = $argv[2] ?? null;
if ($action === "sync") {
$user = getUser() or die();
if (!$user->getConfiguration()->getSettings()->isMailEnabled()) {
$sql = connectSQL() or die();
if (!$context->getSettings()->isMailEnabled()) {
_exit("Mails are not configured yet.");
}
$req = new Api\Mail\Sync($user);
$req = new Api\Mail\Sync($context);
printLine("Syncing emails…");
if (!$req->execute()) {
_exit("Error syncing mails: " . $req->getLastError());
@ -612,8 +620,8 @@ function onMail($argv) {
_exit("Done.");
} else if ($action === "send_queue") {
$user = getUser() or die();
$req = new \Api\Mail\SendQueue($user);
$sql = connectSQL() or die();
$req = new \Api\Mail\SendQueue($context);
$debug = in_array("debug", $argv);
if (!$req->execute(["debug" => $debug])) {
_exit("Error processing mail queue: " . $req->getLastError());
@ -624,30 +632,31 @@ function onMail($argv) {
}
function onImpersonate($argv) {
global $context;
if (count($argv) < 3) {
_exit("Usage: cli.php impersonate <user_id|user_name>");
}
$user = getUser() or exit;
$sql = connectSQL() or die();
$userId = $argv[2];
if (!is_numeric($userId)) {
$sql = $user->getSQL();
$res = $sql->select("uid")
$res = $sql->select("id")
->from("User")
->where(new Compare("name", $userId))
->execute();
if ($res === false) {
_exit("SQL error: " . $sql->getLastError());
} else {
$userId = $res[0]["uid"];
$userId = $res[0]["id"];
}
}
$user->createSession(intval($userId));
$session = $user->getSession();
$user = new \Objects\DatabaseEntity\User($userId);
$session = new \Objects\DatabaseEntity\Session($context, $user);
$session->setData(["2faAuthenticated" => true]);
$session->update(false);
$session->update();
echo "session=" . $session->getCookie() . PHP_EOL;
}

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

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

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

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

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

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

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

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

@ -2,9 +2,17 @@
namespace Api {
use Objects\Context;
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 {
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.");
}
@ -24,12 +32,14 @@ namespace Api\Permission {
use Driver\SQL\Condition\CondLike;
use Driver\SQL\Condition\CondNot;
use Driver\SQL\Strategy\UpdateStrategy;
use Objects\User;
use Objects\Context;
use Objects\DatabaseEntity\Group;
use Objects\DatabaseEntity\User;
class Check extends PermissionAPI {
public function __construct(User $user, bool $externalCall = false) {
parent::__construct($user, $externalCall, array(
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, array(
'method' => new StringType('method', 323)
));
@ -39,7 +49,7 @@ namespace Api\Permission {
public function _execute(): bool {
$method = $this->getParam("method");
$sql = $this->user->getSQL();
$sql = $this->context->getSQL();
$res = $sql->select("groups")
->from("ApiPermission")
->where(new CondLike($method, new Column("method")))
@ -58,8 +68,9 @@ namespace Api\Permission {
return true;
}
$userGroups = $this->user->getGroups();
if (empty($userGroups) || empty(array_intersect($groups, array_keys($this->user->getGroups())))) {
$currentUser = $this->context->getUser();
$userGroups = $currentUser ? $currentUser->getGroups() : [];
if (empty($userGroups) || empty(array_intersect($groups, array_keys($userGroups)))) {
http_response_code(401);
return $this->createError("Permission denied.");
}
@ -71,33 +82,17 @@ namespace Api\Permission {
class Fetch extends PermissionAPI {
private array $groups;
private ?array $groups;
public function __construct(User $user, bool $externalCall = false) {
parent::__construct($user, $externalCall, array());
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, array());
}
private function fetchGroups() {
$sql = $this->user->getSQL();
$res = $sql->select("uid", "name", "color")
->from("Group")
->orderBy("uid")
->ascending()
->execute();
$this->success = ($res !== FALSE);
private function fetchGroups(): bool {
$sql = $this->context->getSQL();
$this->groups = Group::findAll($sql);
$this->success = ($this->groups !== FALSE);
$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;
}
@ -107,7 +102,7 @@ namespace Api\Permission {
return false;
}
$sql = $this->user->getSQL();
$sql = $this->context->getSQL();
$res = $sql->select("method", "groups", "description")
->from("ApiPermission")
->execute();
@ -137,8 +132,8 @@ namespace Api\Permission {
class Save extends PermissionAPI {
public function __construct(User $user, bool $externalCall = false) {
parent::__construct($user, $externalCall, array(
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, array(
'permissions' => new Parameter('permissions', Parameter::TYPE_ARRAY)
));
}
@ -150,27 +145,27 @@ namespace Api\Permission {
}
$permissions = $this->getParam("permissions");
$sql = $this->user->getSQL();
$sql = $this->context->getSQL();
$methodParam = new StringType('method', 32);
$groupsParam = new Parameter('groups', Parameter::TYPE_ARRAY);
$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();
foreach($permissions as $permission) {
foreach ($permissions as $permission) {
if (!is_array($permission)) {
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'");
} else if (!$methodParam->parseParam($permission["method"])) {
$expectedType = $methodParam->getTypeName();
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();
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.");
} else {
$method = $methodParam->value;

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

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

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

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

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

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

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

File diff suppressed because it is too large Load Diff

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

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

@ -5,90 +5,27 @@ namespace Configuration;
use Driver\SQL\SQL;
use \Driver\SQL\Strategy\SetNullStrategy;
use \Driver\SQL\Strategy\CascadeStrategy;
use Objects\DatabaseEntity\DatabaseEntity;
use PHPUnit\Util\Exception;
class CreateDatabase extends DatabaseScript {
public static function createQueries(SQL $sql): array {
$queries = array();
// Language
$queries[] = $sql->createTable("Language")
->addSerial("uid")
->addString("code", 5)
->addString("name", 32)
->primaryKey("uid")
->unique("code")
->unique("name");
self::loadEntities($queries, $sql);
$queries[] = $sql->insert("Language", array("code", "name"))
->addRow("en_US", 'American English')
->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")
->addInt("user_id")
->addString("token", 36)
->addEnum("token_type", array("password_reset", "email_confirm", "invite", "gpg_confirm"))
->addDateTime("valid_until")
->addBool("used", false)
->foreignKey("user_id", "User", "uid", new CascadeStrategy());
$queries[] = $sql->createTable("Group")
->addSerial("uid")
->addString("name", 32)
->addString("color", 10)
->primaryKey("uid")
->unique("name");
->foreignKey("user_id", "User", "id", new CascadeStrategy());
$queries[] = $sql->insert("Group", array("name", "color"))
->addRow(USER_GROUP_MODERATOR_NAME, "#007bff")
@ -99,42 +36,25 @@ class CreateDatabase extends DatabaseScript {
->addInt("user_id")
->addInt("group_id")
->unique("user_id", "group_id")
->foreignKey("user_id", "User", "uid", new CascadeStrategy())
->foreignKey("group_id", "Group", "uid", 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");
->foreignKey("user_id", "User", "id", new CascadeStrategy())
->foreignKey("group_id", "Group", "id", new CascadeStrategy());
$queries[] = $sql->createTable("UserNotification")
->addInt("user_id")
->addInt("notification_id")
->addBool("seen", false)
->foreignKey("user_id", "User", "uid")
->foreignKey("notification_id", "Notification", "uid")
->foreignKey("user_id", "User", "id")
->foreignKey("notification_id", "Notification", "id")
->unique("user_id", "notification_id");
$queries[] = $sql->createTable("GroupNotification")
->addInt("group_id")
->addInt("notification_id")
->addBool("seen", false)
->foreignKey("group_id", "Group", "uid")
->foreignKey("notification_id", "Notification", "uid")
->foreignKey("group_id", "Group", "id")
->foreignKey("notification_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")
->addInt("day")
->addInt("count", false, 1)
@ -142,14 +62,14 @@ class CreateDatabase extends DatabaseScript {
->unique("day", "cookie");
$queries[] = $sql->createTable("Route")
->addSerial("uid")
->addSerial("id")
->addString("request", 128)
->addEnum("action", array("redirect_temporary", "redirect_permanently", "static", "dynamic"))
->addString("target", 128)
->addString("extra", 64, true)
->addBool("active", true)
->addBool("exact", true)
->primaryKey("uid")
->primaryKey("id")
->unique("request");
$queries[] = $sql->insert("Route", ["request", "action", "target", "extra", "exact"])
@ -184,17 +104,17 @@ class CreateDatabase extends DatabaseScript {
$queries[] = $settingsQuery;
$queries[] = $sql->createTable("ContactRequest")
->addSerial("uid")
->addSerial("id")
->addString("from_name", 32)
->addString("from_email", 64)
->addString("message", 512)
->addString("messageId", 78, true) # null = don't sync with mails (usually if mail could not be sent)
->addDateTime("created_at", false, $sql->currentTimestamp())
->unique("messageId")
->primaryKey("uid");
->primaryKey("id");
$queries[] = $sql->createTable("ContactMessage")
->addSerial("uid")
->addSerial("id")
->addInt("request_id")
->addInt("user_id", true) # null = customer has sent this message
->addString("message", 512)
@ -202,9 +122,9 @@ class CreateDatabase extends DatabaseScript {
->addDateTime("created_at", false, $sql->currentTimestamp())
->addBool("read", false)
->unique("messageId")
->primaryKey("uid")
->foreignKey("request_id", "ContactRequest", "uid", new CascadeStrategy())
->foreignKey("user_id", "User", "uid", new SetNullStrategy());
->primaryKey("id")
->foreignKey("request_id", "ContactRequest", "id", new CascadeStrategy())
->foreignKey("user_id", "User", "id", new SetNullStrategy());
$queries[] = $sql->createTable("ApiPermission")
->addString("method", 32)
@ -213,7 +133,7 @@ class CreateDatabase extends DatabaseScript {
->primaryKey("method");
$queries[] = $sql->createTable("MailQueue")
->addSerial("uid")
->addSerial("id")
->addString("from", 64)
->addString("to", 64)
->addString("subject")
@ -225,18 +145,9 @@ class CreateDatabase extends DatabaseScript {
->addInt("retryCount", false, 5)
->addDateTime("nextTry", false, $sql->now())
->addString("errorMessage", NULL, true)
->primaryKey("uid");
->primaryKey("id");
$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"))
->addRow("ApiKey/create", array(), "Allows users to create API-Keys for themselves")
->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");
self::loadPatches($queries, $sql);
self::loadEntities($queries, $sql);
return $queries;
}
@ -293,18 +203,47 @@ class CreateDatabase extends DatabaseScript {
if (file_exists($entityDirectory) && is_dir($entityDirectory)) {
$scan_arr = scandir($entityDirectory);
$files_arr = array_diff($scan_arr, array('.', '..'));
$handlers = [];
foreach ($files_arr as $file) {
$suffix = ".class.php";
if (endsWith($file, $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";
$method = "$className::getHandler";
$handler = call_user_func($method, $sql);
$queries[] = $handler->getTableQuery();
$reflectionClass = new \ReflectionClass($className);
if ($reflectionClass->isSubclassOf(DatabaseEntity::class)) {
$method = "$className::getHandler";
$handler = call_user_func($method, $sql);
$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)
->exec(new CreateProcedure($sql, "InsertEntityLog"), [
"tableName" => new CurrentTable(),
"entityId" => new CurrentColumn("uid"),
"entityId" => new CurrentColumn("id"),
"lifetime" => $lifetime,
]),
@ -27,14 +27,14 @@ class EntityLog_2021_04_08 extends DatabaseScript {
->after()->update($table)
->exec(new CreateProcedure($sql, "UpdateEntityLog"), [
"tableName" => new CurrentTable(),
"entityId" => new CurrentColumn("uid"),
"entityId" => new CurrentColumn("id"),
]),
$sql->createTrigger("${table}_trg_delete")
->after()->delete($table)
->exec(new CreateProcedure($sql, "DeleteEntityLog"), [
"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")
->param(new CurrentTable())
->param(new IntColumn("uid"))
->param(new IntColumn("id"))
->param(new IntColumn("lifetime", false, 90))
->returns(new Trigger())
->exec(array(
$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")
->param(new CurrentTable())
->param(new IntColumn("uid"))
->param(new IntColumn("id"))
->returns(new Trigger())
->exec(array(
$sql->update("EntityLog")
->set("modified", $sql->now())
->where(new Compare("entityId", new CurrentColumn("uid")))
->where(new Compare("entityId", new CurrentColumn("id")))
->where(new Compare("tableName", new CurrentTable()))
));
$deleteProcedure = $sql->createProcedure("DeleteEntityLog")
->param(new CurrentTable())
->param(new IntColumn("uid"))
->param(new IntColumn("id"))
->returns(new Trigger())
->exec(array(
$sql->delete("EntityLog")
->where(new Compare("entityId", new CurrentColumn("uid")))
->where(new Compare("entityId", new CurrentColumn("id")))
->where(new Compare("tableName", new CurrentTable()))
));

@ -9,14 +9,6 @@ class SystemLog_2022_03_30 extends DatabaseScript {
public static function createQueries(SQL $sql): array {
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"])
->addRow("Logs/get", [USER_GROUP_ADMIN], "Allows users to fetch system logs")
];

@ -7,7 +7,7 @@
namespace Configuration;
use Driver\SQL\Query\Insert;
use Objects\User;
use Objects\Context;
class Settings {
@ -58,8 +58,8 @@ class Settings {
return $settings;
}
public function loadFromDatabase(User $user): bool {
$req = new \Api\Settings\Get($user);
public function loadFromDatabase(Context $context): bool {
$req = new \Api\Settings\Get($context);
$success = $req->execute();
if ($success) {
@ -78,7 +78,7 @@ class Settings {
$this->allowedExtensions = explode(",", $result["allowed_extensions"] ?? strtolower(implode(",", $this->allowedExtensions)));
if (!isset($result["jwt_secret"])) {
$req = new \Api\Settings\Set($user);
$req = new \Api\Settings\Set($context);
$req->execute(array("settings" => array(
"jwt_secret" => $this->jwtSecret
)));
@ -135,4 +135,8 @@ class Settings {
public function isExtensionAllowed(string $ext): bool {
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 (isset($_GET["token"]) && is_string($_GET["token"]) && !empty($_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"]));
if ($this->parameters["view"]["success"]) {
if (strcmp($req->getResult()["token"]["type"], "password_reset") !== 0) {
@ -35,18 +35,18 @@ class Account extends TemplateDocument {
}
} else if ($this->getTemplateName() === "account/register.twig") {
$settings = $this->getSettings();
if ($this->getUser()->isLoggedIn()) {
if ($this->getUser()) {
$this->createError("You are already logged in.");
} else if (!$settings->isRegistrationAllowed()) {
$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");
exit();
} else if ($this->getTemplateName() === "account/accept_invite.twig") {
if (isset($_GET["token"]) && is_string($_GET["token"]) && !empty($_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"]));
if ($this->parameters["view"]["success"]) {
if (strcmp($req->getResult()["token"]["type"], "invite") !== 0) {

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

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

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

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

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

@ -52,16 +52,19 @@ abstract class SQL {
protected ConnectionData $connectionData;
protected int $lastInsertId;
protected bool $logQueries;
public function __construct($connectionData) {
$this->connection = NULL;
$this->lastError = 'Unknown Error';
$this->connectionData = $connectionData;
$this->lastInsertId = 0;
$this->logger = new Logger(getClassName($this), $this);
$this->logQueries = false;
}
public function isConnected(): bool {
return !is_null($this->connection);
return !is_null($this->connection) && !is_bool($this->connection);
}
public function getLastError(): string {
@ -131,7 +134,7 @@ abstract class SQL {
$parameters = [];
$queryStr = $query->build($parameters);
if($query->dump) {
if ($query->dump) {
var_dump($queryStr);
var_dump($parameters);
}
@ -149,6 +152,31 @@ abstract class SQL {
$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;
}

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

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

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

@ -23,8 +23,7 @@ abstract class View extends StaticView {
public function isSearchable(): bool { return $this->searchable; }
public function getSiteName(): string {
// what a chain lol
return $this->getDocument()->getUser()->getConfiguration()->getSettings()->getSiteName();
return $this->getDocument()->getSettings()->getSiteName();
}
protected function load(string $viewClass) : string {
@ -43,7 +42,7 @@ abstract class View extends StaticView {
}
private function loadLanguageModules() {
$lang = $this->document->getUser()->getLanguage();
$lang = $this->document->getContext()->getLanguage();
foreach ($this->langModules as $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,20 +2,57 @@
namespace Objects\DatabaseEntity;
use Driver\SQL\Condition\Compare;
use Driver\SQL\Condition\Condition;
use Driver\SQL\SQL;
abstract class DatabaseEntity {
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);
return $handler->fetchOne($id);
return $handler->entityFromRow($row);
}
public static function newInstance(\ReflectionClass $reflectionClass, array $row) {
return $reflectionClass->newInstanceWithoutConstructor();
}
public static function find(SQL $sql, int $id, bool $fetchEntities = false, bool $fetchRecursive = false): static|bool|null {
$handler = self::getHandler($sql);
if ($fetchEntities) {
return DatabaseEntityQuery::fetchOne(self::getHandler($sql))
->where(new Compare($handler->getTableName() . ".id", $id))
->fetchEntities($fetchRecursive)
->execute();
} else {
return $handler->fetchOne($id);
}
}
public static function exists(SQL $sql, int $id): bool {
$handler = self::getHandler($sql);
$res = $sql->select($sql->count())
->from($handler->getTableName())
->where(new Compare($handler->getTableName() . ".id", $id))
->execute();
return $res !== false && $res[0]["count"] !== 0;
}
public static function findBuilder(SQL $sql): DatabaseEntityQuery {
return DatabaseEntityQuery::fetchOne(self::getHandler($sql));
}
public static function findAll(SQL $sql, ?Condition $condition = null): ?array {
@ -23,9 +60,13 @@ abstract class DatabaseEntity {
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);
$res = $handler->insertOrUpdate($this);
$res = $handler->insertOrUpdate($this, $columns);
if ($res === false) {
return false;
} else if ($this->id === null) {

@ -5,7 +5,9 @@ namespace Objects\DatabaseEntity;
use Driver\Logger\Logger;
use Driver\SQL\Column\BoolColumn;
use Driver\SQL\Column\DateTimeColumn;
use Driver\SQL\Column\EnumColumn;
use Driver\SQL\Column\IntColumn;
use Driver\SQL\Column\JsonColumn;
use Driver\SQL\Column\StringColumn;
use Driver\SQL\Condition\Compare;
use Driver\SQL\Condition\Condition;
@ -13,19 +15,27 @@ use Driver\SQL\Column\DoubleColumn;
use Driver\SQL\Column\FloatColumn;
use Driver\SQL\Constraint\ForeignKey;
use Driver\SQL\Query\CreateTable;
use Driver\SQL\Query\Select;
use Driver\SQL\SQL;
use Driver\SQL\Strategy\CascadeStrategy;
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;
class DatabaseEntityHandler {
private \ReflectionClass $entityClass;
private static \ReflectionProperty $ID_FIELD;
private string $tableName;
private array $columns;
private array $properties;
private array $relations;
private array $constraints;
private SQL $sql;
private Logger $logger;
@ -34,22 +44,23 @@ class DatabaseEntityHandler {
$className = $entityClass->getName();
$this->logger = new Logger($entityClass->getShortName(), $sql);
$this->entityClass = $entityClass;
if (!$this->entityClass->isSubclassOf(DatabaseEntity::class) ||
!$this->entityClass->isInstantiable()) {
if (!$this->entityClass->isSubclassOf(DatabaseEntity::class)) {
$this->raiseError("Cannot persist class '$className': Not an instance of DatabaseEntity or not instantiable.");
}
$this->tableName = $this->entityClass->getShortName();
$this->columns = [];
$this->properties = [];
$this->relations = [];
if (!isset(self::$ID_FIELD)) {
self::$ID_FIELD = (new \ReflectionClass(DatabaseEntity::class))->getProperty("id");
}
$this->columns = []; // property name => database column name
$this->properties = []; // property name => \ReflectionProperty
$this->relations = []; // property name => referenced table name
$this->constraints = []; // \Driver\SQL\Constraint\Constraint
foreach ($this->entityClass->getProperties() as $property) {
$propertyName = $property->getName();
if ($propertyName === "id") {
$this->properties[$propertyName] = $property;
continue;
}
$propertyType = $property->getType();
$columnName = self::getColumnName($propertyName);
if (!($propertyType instanceof \ReflectionNamedType)) {
@ -58,38 +69,83 @@ class DatabaseEntityHandler {
$nullable = $propertyType->allowsNull();
$propertyTypeName = $propertyType->getName();
if (!empty($property->getAttributes(Transient::class))) {
continue;
}
$defaultValue = (self::getAttribute($property, DefaultValue::class))?->getValue();
$isUnique = !empty($property->getAttributes(Unique::class));
if ($propertyTypeName === 'string') {
$this->columns[$propertyName] = new StringColumn($columnName, null, $nullable);
$enum = self::getAttribute($property, Enum::class);
if ($enum) {
$this->columns[$propertyName] = new EnumColumn($columnName, $enum->getValues(), $nullable, $defaultValue);
} else {
$maxLength = self::getAttribute($property, MaxLength::class);
$this->columns[$propertyName] = new StringColumn($columnName, $maxLength?->getValue(), $nullable, $defaultValue);
}
} else if ($propertyTypeName === 'int') {
$this->columns[$propertyName] = new IntColumn($columnName, $nullable);
$this->columns[$propertyName] = new IntColumn($columnName, $nullable, $defaultValue);
} else if ($propertyTypeName === 'float') {
$this->columns[$propertyName] = new FloatColumn($columnName, $nullable);
$this->columns[$propertyName] = new FloatColumn($columnName, $nullable, $defaultValue);
} else if ($propertyTypeName === 'double') {
$this->columns[$propertyName] = new DoubleColumn($columnName, $nullable);
$this->columns[$propertyName] = new DoubleColumn($columnName, $nullable, $defaultValue);
} else if ($propertyTypeName === 'bool') {
$this->columns[$propertyName] = new BoolColumn($columnName, $nullable);
$this->columns[$propertyName] = new BoolColumn($columnName, $defaultValue ?? false);
} else if ($propertyTypeName === 'DateTime') {
$this->columns[$propertyName] = new DateTimeColumn($columnName, $nullable);
} else {
$this->columns[$propertyName] = new DateTimeColumn($columnName, $nullable, $defaultValue);
/*} else if ($propertyName === 'array') {
$many = self::getAttribute($property, Many::class);
if ($many) {
$requestedType = $many->getValue();
if (isClass($requestedType)) {
$requestedClass = new \ReflectionClass($requestedType);
} else {
$this->raiseError("Cannot persist class '$className': Property '$propertyName' has non persist-able type: $requestedType");
}
} else {
$this->raiseError("Cannot persist class '$className': Property '$propertyName' has non persist-able type: $propertyTypeName");
}*/
} else if ($propertyTypeName !== "mixed") {
try {
$requestedClass = new \ReflectionClass($propertyTypeName);
if ($requestedClass->isSubclassOf(DatabaseEntity::class)) {
$columnName .= "_id";
$requestedHandler = ($requestedClass->getName() === $this->entityClass->getName()) ?
$this : DatabaseEntity::getHandler($this->sql, $requestedClass);
$strategy = $nullable ? new SetNullStrategy() : new CascadeStrategy();
$this->columns[$propertyName] = new IntColumn($columnName, $nullable);
$this->relations[$propertyName] = new ForeignKey($columnName, $requestedHandler->tableName, "id", $strategy);
$this->columns[$propertyName] = new IntColumn($columnName, $nullable, $defaultValue);
$this->constraints[] = new ForeignKey($columnName, $requestedHandler->tableName, "id", $strategy);
$this->relations[$propertyName] = $requestedHandler;
} else {
$this->raiseError("Cannot persist class '$className': Property '$propertyName' has non persist-able type: $propertyTypeName");
}
} catch (\Exception $ex) {
$this->raiseError("Cannot persist class '$className' property '$propertyTypeName': " . $ex->getMessage());
}
} else {
if (!empty($property->getAttributes(Json::class))) {
$this->columns[$propertyName] = new JsonColumn($columnName, $nullable, $defaultValue);
} else {
$this->raiseError("Cannot persist class '$className': Property '$propertyName' has non persist-able type: $propertyTypeName");
}
}
$this->properties[$propertyName] = $property;
if ($isUnique) {
$this->constraints[] = new \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
return strtolower(preg_replace_callback("/([a-z])([A-Z]+)/", function ($m) {
return $m[1] . "_" . strtolower($m[2]);
@ -108,46 +164,111 @@ class DatabaseEntityHandler {
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 {
$entity = $this->entityClass->newInstanceWithoutConstructor();
foreach ($this->columns as $propertyName => $column) {
$value = $row[$column->getName()];
$property = $this->properties[$propertyName];
if ($property->getType()->getName() === "DateTime") {
$value = new \DateTime($value);
}
$property->setValue($entity, $value);
$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;
}
self::$ID_FIELD->setAccessible(true);
self::$ID_FIELD->setValue($entity, $row["id"]);
foreach ($this->columns as $propertyName => $column) {
$columnName = $column->getName();
if (array_key_exists($columnName, $row)) {
$value = $row[$columnName];
$property = $this->properties[$propertyName];
if ($column instanceof DateTimeColumn) {
$value = new \DateTime($value);
} else if ($column instanceof JsonColumn) {
$value = json_decode($value);
} else if (isset($this->relations[$propertyName])) {
$relColumnPrefix = self::getColumnName($propertyName) . "_";
if (array_key_exists($relColumnPrefix . "id", $row)) {
$relId = $row[$relColumnPrefix . "id"];
if ($relId !== null) {
$relationHandler = $this->relations[$propertyName];
$value = $relationHandler->entityFromRow(self::getPrefixedRow($row, $relColumnPrefix));
} else if (!$column->notNull()) {
$value = null;
} else {
continue;
}
} else {
continue;
}
}
$property->setAccessible(true);
$property->setValue($entity, $value);
}
}
$this->properties["id"]->setAccessible(true);
$this->properties["id"]->setValue($entity, $row["id"]);
$entity->postFetch($this->sql, $row);
return $entity;
} catch (\Exception $exception) {
$this->logger->error("Error creating entity from database row: " . $exception->getMessage());
throw $exception;
return null;
}
}
public function fetchOne(int $id): ?DatabaseEntity {
$res = $this->sql->select("id", ...array_keys($this->columns))
->from($this->tableName)
->where(new Compare("id", $id))
public function getSelectQuery(): Select {
return $this->sql->select(...$this->getColumnNames())
->from($this->tableName);
}
public function fetchOne(int $id): DatabaseEntity|bool|null {
$res = $this->getSelectQuery()
->where(new Compare($this->tableName . ".id", $id))
->first()
->execute();
if (empty($res)) {
return null;
if ($res === false || $res === null) {
return $res;
} else {
return $this->entityFromRow($res);
}
}
public function fetchMultiple(?Condition $condition = null): ?array {
$query = $this->sql->select("id", ...array_keys($this->columns))
->from($this->tableName);
$query = $this->getSelectQuery();
if ($condition) {
$query->where($condition);
@ -159,7 +280,10 @@ class DatabaseEntityHandler {
} else {
$entities = [];
foreach ($res as $row) {
$entities[] = $this->entityFromRow($row);
$entity = $this->entityFromRow($row);
if ($entity) {
$entities[$entity->getId()] = $entity;
}
}
return $entities;
}
@ -175,7 +299,7 @@ class DatabaseEntityHandler {
$query->addColumn($column);
}
foreach ($this->relations as $constraint) {
foreach ($this->constraints as $constraint) {
$query->addConstraint($constraint);
}
@ -187,29 +311,43 @@ class DatabaseEntityHandler {
return $query->execute();
}
public function insertOrUpdate(DatabaseEntity $entity) {
public function insertOrUpdate(DatabaseEntity $entity, ?array $columns = null) {
$id = $entity->getId();
if ($id === null) {
$columns = [];
$row = [];
$action = $id === null ? "insert" : "update";
foreach ($this->columns as $propertyName => $column) {
$columns[] = $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 insert entity: property '$propertyName' was not initialized yet.");
return false;
}
$row[] = $value;
$row = [];
foreach ($this->columns as $propertyName => $column) {
if ($columns && !in_array($column->getName(), $columns)) {
continue;
}
$res = $this->sql->insert($this->tableName, $columns)
->addRow(...$row)
$property = $this->properties[$propertyName];
$property->setAccessible(true);
if ($property->isInitialized($entity)) {
$value = $property->getValue($entity);
if (isset($this->relations[$propertyName])) {
$value = $value->getId();
}
} else if (!$this->columns[$propertyName]->notNull()) {
$value = null;
} else {
if ($action !== "update") {
$this->logger->error("Cannot $action entity: property '$propertyName' was not initialized yet.");
return false;
} else {
continue;
}
}
$row[$column->getName()] = $value;
}
$entity->preInsert($row);
if ($id === null) {
$res = $this->sql->insert($this->tableName, array_keys($row))
->addRow(...array_values($row))
->returning("id")
->execute();
@ -220,20 +358,9 @@ class DatabaseEntityHandler {
}
} else {
$query = $this->sql->update($this->tableName)
->where(new Compare("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;
}
->where(new Compare($this->tableName . ".id", $id));
foreach ($row as $columnName => $value) {
$query->set($columnName, $value);
}
@ -242,15 +369,14 @@ class DatabaseEntityHandler {
}
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) {
$this->logger->error($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
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";
private int $id;
private bool $confirmed;
private string $fingerprint;
private string $algorithm;
#[MaxLength(64)] private string $fingerprint;
#[MaxLength(64)] private string $algorithm;
private \DateTime $expires;
#[DefaultValue(CurrentTimeStamp::class)] private \DateTime $added;
public function __construct(int $id, bool $confirmed, string $fingerprint, string $algorithm, string $expires) {
$this->id = $id;
parent::__construct($id);
$this->confirmed = $confirmed;
$this->fingerprint = $fingerprint;
$this->algorithm = $algorithm;
@ -25,26 +29,17 @@ class GpgKey extends ApiObject {
$cmd = self::GPG2 . " --encrypt --output - --recipient $gpgFingerprint --trust-model always --batch --armor";
list($out, $err) = self::proc_exec($cmd, $body, true);
if ($out === null) {
return self::createError("Error while communicating with GPG agent");
return createError("Error while communicating with GPG agent");
} else if ($err) {
return self::createError($err);
return createError($err);
} else {
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 {
$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)) {
return null;
}
@ -62,29 +57,25 @@ class GpgKey extends ApiObject {
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 {
list($out, $err) = self::proc_exec(self::GPG2 . " --show-key", $key);
if ($out === null) {
return self::createError("Error while communicating with GPG agent");
return createError("Error while communicating with GPG agent");
}
if ($err) {
return self::createError($err);
return createError($err);
}
$lines = explode("\n", $out);
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)) {
return self::createError("Error parsing GPG output");
return createError("Error parsing GPG output");
}
$keyType = $matches[1];
$keyAlg = $matches[2];
$keyAlg = $matches[2];
$expires = \DateTime::createFromFormat("Y-m-d", $matches[3]);
$fingerprint = trim($lines[1]);
$keyData = ["type" => $keyType, "algorithm" => $keyAlg, "expires" => $expires, "fingerprint" => $fingerprint];
@ -94,17 +85,17 @@ class GpgKey extends ApiObject {
public static function importKey(string $key): array {
list($out, $err) = self::proc_exec(self::GPG2 . " --import", $key);
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("/.*\s+unchanged:\s+(\d+)/", $err, $matches) && intval($matches[1]) > 0) ||
(preg_match("/.*\s+imported:\s+(\d+)/", $err, $matches) && intval($matches[1]) > 0)) {
(preg_match("/.*\s+imported:\s+(\d+)/", $err, $matches) && intval($matches[1]) > 0)) {
return ["success" => true];
}
}
return self::createError($err);
return createError($err);
}
public static function export($gpgFingerprint, bool $armored): array {
@ -115,7 +106,7 @@ class GpgKey extends ApiObject {
$cmd .= escapeshellarg($gpgFingerprint);
list($out, $err) = self::proc_exec($cmd);
if ($err) {
return self::createError($err);
return createError($err);
}
return ["success" => true, "data" => $out];
@ -125,12 +116,18 @@ class GpgKey extends ApiObject {
return $this->confirmed;
}
public function getId(): int {
return $this->id;
}
public function getFingerprint(): string {
return $this->fingerprint;
}
public function jsonSerialize(): array {
return [
"id" => $this->getId(),
"fingerprint" => $this->fingerprint,
"algorithm" => $this->algorithm,
"expires" => $this->expires->getTimestamp(),
"added" => $this->added->getTimestamp(),
"confirmed" => $this->confirmed
];
}
}

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

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

@ -37,9 +37,9 @@ class StaticFileRoute extends AbstractRoute {
}
$pathInfo = pathinfo($path);
if ($router !== null && ($user = $router->getUser()) !== null) {
if ($router !== null) {
$ext = $pathInfo["extension"] ?? "";
if (!$user->getConfiguration()->getSettings()->isExtensionAllowed($ext)) {
if (!$router->getContext()->getSettings()->isExtensionAllowed($ext)) {
http_response_code(406);
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;
use Cose\Algorithm\Signature\ECDSA\ECSignature;
use Objects\DatabaseEntity\Attribute\Transient;
use Objects\DatabaseEntity\TwoFactorToken;
class KeyBasedTwoFactorToken extends TwoFactorToken {
const TYPE = "fido";
private ?string $challenge;
private ?string $credentialId;
private ?PublicKey $publicKey;
#[Transient] private ?string $challenge;
#[Transient] private ?string $credentialId;
#[Transient] private ?PublicKey $publicKey;
public function __construct(string $data, ?int $id = null, bool $confirmed = false) {
parent::__construct(self::TYPE, $id, $confirmed);
if (!$confirmed) {
protected function readData(string $data) {
if ($this->isConfirmed()) {
$this->challenge = base64_decode($data);
$this->credentialId = null;
$this->publicKey = null;
@ -34,7 +35,7 @@ class KeyBasedTwoFactorToken extends TwoFactorToken {
return $this->publicKey;
}
public function getCredentialId() {
public function getCredentialId(): ?string {
return $this->credentialId;
}

@ -5,22 +5,29 @@ namespace Objects\TwoFactor;
use Base32\Base32;
use chillerlan\QRCode\QRCode;
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 {
const TYPE = "totp";
private string $secret;
#[Transient] private string $secret;
public function __construct(string $secret, ?int $id = null, bool $confirmed = false) {
parent::__construct(self::TYPE, $id, $confirmed);
public function __construct(string $secret) {
parent::__construct(self::TYPE);
$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;
$name = rawurlencode($user->getUsername());
$settings = $user->getConfiguration()->getSettings();
$name = rawurlencode($context->getUser()->getUsername());
$settings = $context->getSettings();
$urlArgs = [
"secret" => $this->secret,
"issuer" => $settings->getSiteName(),
@ -30,10 +37,10 @@ class TimeBasedTwoFactorToken extends TwoFactorToken {
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]);
$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 {

@ -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;
}
define("WEBBASE_VERSION", "1.5.2");
define("WEBBASE_VERSION", "2.0.0-alpha");
spl_autoload_extensions(".php");
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 {
$isSecure = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ||
(!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) {
return json_encode(array("success" => false, "msg" => $msg));
return ["success" => false, "msg" => $msg];
}
function downloadFile($handle, $offset = 0, $length = null): bool {
@ -278,3 +274,8 @@ function parseClass($class): string {
$parts = array_map('ucfirst', $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 php:7.4-fpm
FROM php:8.0-fpm
WORKDIR "/application"
RUN mkdir -p /application/core/Configuration
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." ]));
}
$config = new Configuration();
$user = new Objects\User($config);
$sql = $user->getSQL();
$settings = $config->getSettings();
$installation = !$sql || ($sql->isConnected() && !$settings->isInstalled());
$context = new \Objects\Context();
$sql = $context->initSQL();
$settings = $context->getSettings();
$context->parseCookies();
$installation = !$sql || ($sql->isConnected() && !$settings->isInstalled());
$requestedUri = $_GET["site"] ?? $_GET["api"] ?? $_SERVER["REQUEST_URI"];
if ($installation) {
@ -34,7 +34,7 @@ if ($installation) {
$response = "Redirecting to <a href=\"/\">/</a>";
header("Location: /");
} else {
$document = new Documents\Install(new Router($user));
$document = new Documents\Install(new Router($context));
$response = $document->getCode();
}
} else {
@ -45,17 +45,17 @@ if ($installation) {
if (is_file($routerCachePath)) {
@include_once $routerCachePath;
if (class_exists($routerCacheClass)) {
$router = new $routerCacheClass($user);
$router = new $routerCacheClass($context);
}
}
if ($router === null) {
$req = new \Api\Routes\GenerateCache($user);
$req = new \Api\Routes\GenerateCache($context);
if ($req->execute()) {
$router = $req->getRouter();
} else {
$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);

2
js/admin.min.js vendored

File diff suppressed because one or more lines are too long

@ -1,21 +1,33 @@
<?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 {
static \Objects\User $USER;
static \Driver\SQL\SQL $SQL;
static \Objects\DatabaseEntity\DatabaseEntityHandler $HANDLER;
static User $USER;
static SQL $SQL;
static Context $CONTEXT;
static DatabaseEntityHandler $HANDLER;
public static function setUpBeforeClass(): void {
parent::setUpBeforeClass();
self::$USER = new Objects\User(new \Configuration\Configuration());
self::$SQL = self::$USER->getSQL();
self::$CONTEXT = new Context();
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->getLogger()->unitTestMode();
}
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());
}
@ -60,7 +72,8 @@ class DatabaseEntityTest extends \PHPUnit\Framework\TestCase {
$allEntities = TestEntity::findAll(self::$SQL);
$this->assertIsArray($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
$this->assertTrue($entity->delete(self::$SQL));
@ -94,4 +107,16 @@ class TestEntity extends \Objects\DatabaseEntity\DatabaseEntity {
public float $d;
public \DateTime $e;
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 Configuration\Configuration;
use Objects\User;
use Objects\Context;
use Objects\DatabaseEntity\User;
function __new_header_impl(string $line) {
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"];
static User $USER;
static User $USER_LOGGED_IN;
static Context $CONTEXT;
static ?string $SENT_CONTENT;
static array $SENT_HEADERS;
@ -41,13 +43,9 @@ class RequestTest extends \PHPUnit\Framework\TestCase {
public static function setUpBeforeClass(): void {
$config = new Configuration();
RequestTest::$USER = new User($config);
RequestTest::$USER_LOGGED_IN = new User($config);
if (!RequestTest::$USER->getSQL() || !RequestTest::$USER->getSQL()->isConnected()) {
RequestTest::$CONTEXT = new Context();
if (!RequestTest::$CONTEXT->initSQL()) {
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")) {
@ -65,7 +63,7 @@ class RequestTest extends \PHPUnit\Framework\TestCase {
}
public static function tearDownAfterClass(): void {
RequestTest::$USER->getSQL()->close();
RequestTest::$CONTEXT->getSQL()?->close();
foreach (self::FUNCTION_OVERRIDES as $functionName) {
runkit7_function_remove($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 {
if (!is_cli()) {
if (!self::$CONTEXT->isCLI()) {
self::throwException(new \Exception("Cannot simulate request outside cli"));
}
@ -97,7 +95,7 @@ class RequestTest extends \PHPUnit\Framework\TestCase {
public function testAllMethods() {
// 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, "POST"), $allMethodsAllowed->getLastError());
$this->assertFalse($this->simulateRequest($allMethodsAllowed, "PUT"), $allMethodsAllowed->getLastError());
@ -109,7 +107,7 @@ class RequestTest extends \PHPUnit\Framework\TestCase {
public function testOnlyPost() {
// only post allowed
$onlyPostAllowed = new RequestOnlyPost(RequestTest::$USER, true);
$onlyPostAllowed = new RequestOnlyPost(RequestTest::$CONTEXT, true);
$this->assertFalse($this->simulateRequest($onlyPostAllowed, "GET"));
$this->assertEquals("This method is not allowed", $onlyPostAllowed->getLastError(), $onlyPostAllowed->getLastError());
$this->assertEquals(405, self::$SENT_STATUS_CODE);
@ -121,25 +119,25 @@ class RequestTest extends \PHPUnit\Framework\TestCase {
public function testPrivate() {
// private method
$privateExternal = new RequestPrivate(RequestTest::$USER, true);
$privateExternal = new RequestPrivate(RequestTest::$CONTEXT, true);
$this->assertFalse($this->simulateRequest($privateExternal, "GET"));
$this->assertEquals("This function is private.", $privateExternal->getLastError());
$this->assertEquals(403, self::$SENT_STATUS_CODE);
$privateInternal = new RequestPrivate(RequestTest::$USER, false);
$privateInternal = new RequestPrivate(RequestTest::$CONTEXT, false);
$this->assertTrue($privateInternal->execute());
}
public function testDisabled() {
// disabled method
$disabledMethod = new RequestDisabled(RequestTest::$USER, true);
$disabledMethod = new RequestDisabled(RequestTest::$CONTEXT, true);
$this->assertFalse($this->simulateRequest($disabledMethod, "GET"));
$this->assertEquals("This function is currently disabled.", $disabledMethod->getLastError(), $disabledMethod->getLastError());
$this->assertEquals(503, self::$SENT_STATUS_CODE);
}
public function testLoginRequired() {
$loginRequired = new RequestLoginRequired(RequestTest::$USER, true);
$loginRequired = new RequestLoginRequired(RequestTest::$CONTEXT, true);
$this->assertFalse($this->simulateRequest($loginRequired, "GET"));
$this->assertEquals("You are not logged in.", $loginRequired->getLastError(), $loginRequired->getLastError());
$this->assertEquals(401, self::$SENT_STATUS_CODE);
@ -147,8 +145,8 @@ class RequestTest extends \PHPUnit\Framework\TestCase {
}
abstract class TestRequest extends Request {
public function __construct(User $user, bool $externalCall = false, $params = []) {
parent::__construct($user, $externalCall, $params);
public function __construct(Context $context, bool $externalCall = false, $params = []) {
parent::__construct($context, $externalCall, $params);
}
protected function _die(string $data = ""): bool {
@ -162,35 +160,35 @@ abstract class TestRequest extends Request {
}
class RequestAllMethods extends TestRequest {
public function __construct(User $user, bool $externalCall = false) {
parent::__construct($user, $externalCall, []);
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, []);
}
}
class RequestOnlyPost extends TestRequest {
public function __construct(User $user, bool $externalCall = false) {
parent::__construct($user, $externalCall, []);
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, []);
$this->forbidMethod("GET");
}
}
class RequestPrivate extends TestRequest {
public function __construct(User $user, bool $externalCall = false) {
parent::__construct($user, $externalCall, []);
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, []);
$this->isPublic = false;
}
}
class RequestDisabled extends TestRequest {
public function __construct(User $user, bool $externalCall = false) {
parent::__construct($user, $externalCall, []);
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, []);
$this->isDisabled = true;
}
}
class RequestLoginRequired extends TestRequest {
public function __construct(User $user, bool $externalCall = false) {
parent::__construct($user, $externalCall, []);
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, []);
$this->loginRequired = true;
}
}

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

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