diff --git a/Core/API/MailAPI.class.php b/Core/API/MailAPI.class.php
index 79d3476..19365f3 100644
--- a/Core/API/MailAPI.class.php
+++ b/Core/API/MailAPI.class.php
@@ -32,6 +32,7 @@ namespace Core\API {
$connectionData->setProperty("from", $settings["mail_from"] ?? "");
$connectionData->setProperty("last_sync", $settings["mail_last_sync"] ?? "");
$connectionData->setProperty("mail_footer", $settings["mail_footer"] ?? "");
+ $connectionData->setProperty("mail_async", $settings["mail_async"] ?? false);
return $connectionData;
}
@@ -89,7 +90,7 @@ namespace Core\API\Mail {
'replyTo' => new Parameter('replyTo', Parameter::TYPE_EMAIL, true, null),
'replyName' => new StringType('replyName', 32, true, ""),
'gpgFingerprint' => new StringType("gpgFingerprint", 64, true, null),
- 'async' => new Parameter("async", Parameter::TYPE_BOOLEAN, true, true)
+ 'async' => new Parameter("async", Parameter::TYPE_BOOLEAN, true, null)
));
$this->isPublic = false;
}
@@ -110,7 +111,13 @@ namespace Core\API\Mail {
$body = $this->getParam('body');
$gpgFingerprint = $this->getParam("gpgFingerprint");
- if ($this->getParam("async")) {
+ $mailAsync = $this->getParam("async");
+ if ($mailAsync === null) {
+ // not set? grab from settings
+ $mailAsync = $mailConfig->getProperty("mail_async", false);
+ }
+
+ if ($mailAsync) {
$sql = $this->context->getSQL();
$this->success = $sql->insert("MailQueue", ["from", "to", "subject", "body",
"replyTo", "replyName", "gpgFingerprint"])
diff --git a/Core/API/Request.class.php b/Core/API/Request.class.php
index 3b6294d..59a5f83 100644
--- a/Core/API/Request.class.php
+++ b/Core/API/Request.class.php
@@ -223,7 +223,7 @@ abstract class Request {
}
// Check for permission
- if (!($this instanceof \API\Permission\Save)) {
+ if (!($this instanceof \Core\API\Permission\Save)) {
$req = new \Core\API\Permission\Check($this->context);
$this->success = $req->execute(array("method" => $this->getMethod()));
$this->lastError = $req->getLastError();
@@ -242,8 +242,8 @@ abstract class Request {
}
$sql = $this->context->getSQL();
- if (!$sql->isConnected()) {
- $this->lastError = $sql->getLastError();
+ if ($sql === null || !$sql->isConnected()) {
+ $this->lastError = $sql ? $sql->getLastError() : "Database not connected yet.";
return false;
}
@@ -265,8 +265,8 @@ abstract class Request {
return false;
}
- protected function getParam($name, $obj = NULL) {
- // i don't know why phpstorm
+ protected function getParam($name, $obj = NULL): mixed {
+ // I don't know why phpstorm
if ($obj === NULL) {
$obj = $this->params;
}
diff --git a/Core/API/TemplateAPI.class.php b/Core/API/TemplateAPI.class.php
index 39a7edc..cebc25b 100644
--- a/Core/API/TemplateAPI.class.php
+++ b/Core/API/TemplateAPI.class.php
@@ -45,16 +45,23 @@ namespace Core\API\Template {
return $this->createError("Invalid template file extension. Allowed: " . implode(",", $allowedExtensions));
}
- $templateDir = WEBROOT . "/Core/Templates/";
$templateCache = WEBROOT . "/Core/Cache/Templates/";
- $path = realpath($templateDir . $templateFile);
- if (!startsWith($path, realpath($templateDir))) {
- return $this->createError("Template file not in template directory");
- } else if (!is_file($path)) {
- return $this->createError("Template file not found");
+ $baseDirs = ["Site", "Core"];
+ $valid = false;
+
+ foreach ($baseDirs as $baseDir) {
+ $path = realpath(implode("/", [WEBROOT, $baseDir, "Templates", $templateFile]));
+ if ($path && is_file($path)) {
+ $valid = true;
+ break;
+ }
}
- $twigLoader = new FilesystemLoader($templateDir);
+ if (!$valid) {
+ return $this->createError("Template file not found or not inside template directory");
+ }
+
+ $twigLoader = new FilesystemLoader(dirname($path));
$twigEnvironment = new Environment($twigLoader, [
'cache' => $templateCache,
'auto_reload' => true
diff --git a/Core/API/TfaAPI.class.php b/Core/API/TfaAPI.class.php
index a78b335..5909e46 100644
--- a/Core/API/TfaAPI.class.php
+++ b/Core/API/TfaAPI.class.php
@@ -123,10 +123,11 @@ namespace Core\API\TFA {
if ($this->success) {
$body = $req->getResult()["html"];
$gpg = $currentUser->getGPG();
+ $siteName = $settings->getSiteName();
$req = new \Core\API\Mail\Send($this->context);
$this->success = $req->execute([
"to" => $currentUser->getEmail(),
- "subject" => "[Security Lab] 2FA-Authentication removed",
+ "subject" => "[$siteName] 2FA-Authentication removed",
"body" => $body,
"gpgFingerprint" => $gpg?->getFingerprint()
]);
diff --git a/Core/API/UserAPI.class.php b/Core/API/UserAPI.class.php
index 5f62faa..4051414 100644
--- a/Core/API/UserAPI.class.php
+++ b/Core/API/UserAPI.class.php
@@ -2,10 +2,11 @@
namespace Core\API {
- use Cassandra\Date;
use Core\Driver\SQL\Condition\Compare;
use Core\Objects\Context;
use Core\Objects\DatabaseEntity\Language;
+ use Core\Objects\DatabaseEntity\User;
+ use Core\Objects\DatabaseEntity\UserToken;
abstract class UserAPI extends Request {
@@ -74,10 +75,10 @@ namespace Core\API {
$this->checkPasswordRequirements($password, $confirmPassword);
}
- protected function insertUser($username, $email, $password, $confirmed, $fullName = "") {
+ protected function insertUser($username, $email, $password, $confirmed, $fullName = ""): bool|User {
$sql = $this->context->getSQL();
- $user = new \Core\Objects\DatabaseEntity\User();
+ $user = new User();
$user->language = Language::DEFAULT_LANGUAGE(false);
$user->registeredAt = new \DateTime();
$user->password = $this->hashPassword($password);
@@ -89,57 +90,13 @@ namespace Core\API {
$this->success = ($user->save($sql) !== FALSE);
$this->lastError = $sql->getLastError();
- if ($this->success) {
- return $user->getId();
- }
-
- return $this->success;
+ return $this->success ? $user : false;
}
protected function hashPassword($password): string {
return password_hash($password, PASSWORD_BCRYPT);
}
- protected function getUser(int $id) {
- $sql = $this->context->getSQL();
- $res = $sql->select("User.id as userId", "User.name", "User.full_name", "User.email",
- "User.registered_at", "User.confirmed", "User.last_online", "User.profile_picture",
- "User.gpg_id", "GpgKey.confirmed as gpg_confirmed", "GpgKey.fingerprint as gpg_fingerprint",
- "GpgKey.expires as gpg_expires", "GpgKey.algorithm as gpg_algorithm",
- "Group.id as groupId", "Group.name as groupName", "Group.color as groupColor")
- ->from("User")
- ->leftJoin("UserGroup", "User.id", "UserGroup.user_id")
- ->leftJoin("Group", "Group.id", "UserGroup.group_id")
- ->leftJoin("GpgKey", "GpgKey.id", "User.gpg_id")
- ->where(new Compare("User.id", $id))
- ->execute();
-
- $this->success = ($res !== FALSE);
- $this->lastError = $sql->getLastError();
-
- return ($this->success && !empty($res) ? $res : array());
- }
-
- protected function invalidateToken($token) {
- $this->context->getSQL()
- ->update("UserToken")
- ->set("used", true)
- ->where(new Compare("token", $token))
- ->execute();
- }
-
- protected function insertToken(int $userId, string $token, string $tokenType, int $duration): bool {
- $validUntil = (new \DateTime())->modify("+$duration hour");
- $sql = $this->context->getSQL();
- $res = $sql->insert("UserToken", array("user_id", "token", "token_type", "valid_until"))
- ->addRow($userId, $token, $tokenType, $validUntil)
- ->execute();
-
- $this->success = ($res !== FALSE);
- $this->lastError = $sql->getLastError();
- return $this->success;
- }
-
protected function formatDuration(int $count, string $string): string {
if ($count === 1) {
return $string;
@@ -147,6 +104,24 @@ namespace Core\API {
return "the next $count ${string}s";
}
}
+
+ protected function checkToken(string $token) : UserToken|bool {
+ $sql = $this->context->getSQL();
+ $userToken = UserToken::findBuilder($sql)
+ ->where(new Compare("UserToken.token", $token))
+ ->where(new Compare("UserToken.valid_until", $sql->now(), ">"))
+ ->where(new Compare("UserToken.used", 0))
+ ->fetchEntities()
+ ->execute();
+
+ if ($userToken === false) {
+ return $this->createError("Error verifying token: " . $sql->getLastError());
+ } else if ($userToken === null) {
+ return $this->createError("This token does not exist or is no longer valid");
+ } else {
+ return $userToken;
+ }
+ }
}
}
@@ -158,18 +133,14 @@ namespace Core\API\User {
use Core\API\Template\Render;
use Core\API\UserAPI;
use Core\API\VerifyCaptcha;
- use DateTime;
+ use Core\Objects\DatabaseEntity\UserToken;
use Core\Driver\SQL\Column\Column;
use Core\Driver\SQL\Condition\Compare;
- use Core\Driver\SQL\Condition\CondBool;
use Core\Driver\SQL\Condition\CondIn;
- use Core\Driver\SQL\Condition\CondNot;
use Core\Driver\SQL\Expression\JsonArrayAgg;
use ImagickException;
use Core\Objects\Context;
- use Core\Objects\DatabaseEntity\DatabaseEntityHandler;
use Core\Objects\DatabaseEntity\GpgKey;
- use Core\Objects\DatabaseEntity\TwoFactorToken;
use Core\Objects\TwoFactor\KeyBasedTwoFactorToken;
use Core\Objects\DatabaseEntity\User;
@@ -192,7 +163,6 @@ namespace Core\API\User {
$email = $this->getParam('email');
$password = $this->getParam('password');
$confirmPassword = $this->getParam('confirmPassword');
-
if (!$this->checkRequirements($username, $password, $confirmPassword)) {
return false;
}
@@ -203,10 +173,9 @@ namespace Core\API\User {
// prevent duplicate keys
$email = (!is_null($email) && empty($email)) ? null : $email;
-
- $id = $this->insertUser($username, $email, $password, true);
- if ($id !== false) {
- $this->result["userId"] = $id;
+ $user = $this->insertUser($username, $email, $password, true);
+ if ($user !== false) {
+ $this->result["userId"] = $user->getId();
}
return $this->success;
@@ -357,80 +326,60 @@ namespace Core\API\User {
$sql = $this->context->getSQL();
$userId = $this->getParam("id");
- $user = $this->getUser($userId);
- if ($this->success) {
- if (empty($user)) {
- return $this->createError("User not found");
- } else {
+ $user = User::find($sql, $userId, true);
+ if ($user === false) {
+ return $this->createError("Error querying user: " . $sql->getLastError());
+ } else if ($user === null) {
+ return $this->createError("User not found");
+ } else {
- $gpgFingerprint = null;
- if ($user[0]["gpg_id"] && $sql->parseBool($user[0]["gpg_confirmed"])) {
- $gpgFingerprint = $user[0]["gpg_fingerprint"];
+ $queriedUser = $user->jsonSerialize();
+
+ // either we are querying own info or we are support / admin
+ $currentUser = $this->context->getUser();
+ $canView = ($userId === $currentUser->getId() ||
+ $currentUser->hasGroup(USER_GROUP_ADMIN) ||
+ $currentUser->hasGroup(USER_GROUP_SUPPORT));
+
+ // full info only when we have administrative privileges, or we are querying ourselves
+ $fullInfo = ($userId === $currentUser->getId() ||
+ $currentUser->hasGroup(USER_GROUP_ADMIN) ||
+ $currentUser->hasGroup(USER_GROUP_SUPPORT));
+
+ if (!$canView) {
+
+ // check if user posted something publicly
+ $res = $sql->select(new JsonArrayAgg(new Column("publishedBy"), "publisherIds"))
+ ->from("News")
+ ->execute();
+ $this->success = ($res !== false);
+ $this->lastError = $sql->getLastError();
+ if (!$this->success) {
+ return false;
+ } else {
+ $canView = in_array($userId, json_decode($res[0]["publisherIds"], true));
}
+ }
- $queriedUser = array(
- "id" => $userId,
- "name" => $user[0]["name"],
- "fullName" => $user[0]["full_name"],
- "email" => $user[0]["email"],
- "registered_at" => $user[0]["registered_at"],
- "last_online" => $user[0]["last_online"],
- "profilePicture" => $user[0]["profile_picture"],
- "confirmed" => $sql->parseBool($user["0"]["confirmed"]),
- "groups" => array(),
- "gpgFingerprint" => $gpgFingerprint,
- );
+ if (!$canView) {
+ return $this->createError("No permissions to access this user");
+ }
- foreach ($user as $row) {
- if (!is_null($row["groupId"])) {
- $queriedUser["groups"][$row["groupId"]] = array(
- "name" => $row["groupName"],
- "color" => $row["groupColor"],
- );
- }
- }
-
- // either we are querying own info or we are support / admin
- $currentUser = $this->context->getUser();
- $canView = ($userId === $currentUser->getId() ||
- $currentUser->hasGroup(USER_GROUP_ADMIN) ||
- $currentUser->hasGroup(USER_GROUP_SUPPORT));
-
- // full info only when we have administrative privileges, or we are querying ourselves
- $fullInfo = ($userId === $currentUser->getId() ||
- $currentUser->hasGroup(USER_GROUP_ADMIN) ||
- $currentUser->hasGroup(USER_GROUP_SUPPORT));
-
- if (!$canView) {
-
- // check if user posted something publicly
- $res = $sql->select(new JsonArrayAgg(new Column("publishedBy"), "publisherIds"))
- ->from("News")
- ->execute();
- $this->success = ($res !== false);
- $this->lastError = $sql->getLastError();
- if (!$this->success) {
- return false;
- } else {
- $canView = in_array($userId, json_decode($res[0]["publisherIds"], true));
- }
- }
-
- if (!$canView) {
+ if (!$fullInfo) {
+ if (!$queriedUser["confirmed"]) {
return $this->createError("No permissions to access this user");
}
- if (!$fullInfo) {
- if (!$queriedUser["confirmed"]) {
- return $this->createError("No permissions to access this user");
+ $publicAttributes = ["id", "name", "fullName", "profilePicture", "email", "groups"];
+ foreach (array_keys($queriedUser) as $attr) {
+ if (!in_array($attr, $publicAttributes)) {
+ unset($queriedUser[$attr]);
}
- unset($queriedUser["registered_at"]);
- unset($queriedUser["confirmed"]);
- unset($queriedUser["last_online"]);
}
-
- $this->result["user"] = $queriedUser;
}
+
+ unset($queriedUser["session"]); // strip session information
+ $this->result["user"] = $queriedUser;
}
return $this->success;
@@ -469,7 +418,6 @@ namespace Core\API\User {
$this->result["permissions"] = $permissions;
$this->result["user"] = $currentUser->jsonSerialize();
- $this->result["user"]["session"] = $this->context->getSession()?->jsonSerialize();
}
return $this->success;
@@ -496,25 +444,19 @@ namespace Core\API\User {
}
// Create user
- $id = $this->insertUser($username, $email, "", false);
- if (!$this->success) {
+ $user = $this->insertUser($username, $email, "", false);
+ if ($user === false) {
return false;
}
// Create Token
$token = generateRandomString(36);
$validDays = 7;
- $valid_until = (new DateTime())->modify("+$validDays day");
$sql = $this->context->getSQL();
- $res = $sql->insert("UserToken", array("user_id", "token", "token_type", "valid_until"))
- ->addRow($id, $token, "invite", $valid_until)
- ->execute();
- $this->success = ($res !== FALSE);
- $this->lastError = $sql->getLastError();
-
- //send validation mail
- if ($this->success) {
+ $userToken = new UserToken($user, $token, UserToken::TYPE_INVITE, $validDays * 24);
+ if ($userToken->save($sql)) {
+ //send validation mail
$settings = $this->context->getSettings();
$baseUrl = $settings->getBaseUrl();
$siteName = $settings->getSiteName();
@@ -551,7 +493,7 @@ namespace Core\API\User {
}
}
- $this->logger->info("Created new user with id=$id");
+ $this->logger->info("Created new user with id=" . $user->getId());
return $this->success;
}
}
@@ -566,56 +508,37 @@ namespace Core\API\User {
$this->csrfTokenRequired = false;
}
- private function updateUser($uid, $password): bool {
- $sql = $this->context->getSQL();
- $res = $sql->update("User")
- ->set("password", $this->hashPassword($password))
- ->set("confirmed", true)
- ->where(new Compare("id", $uid))
- ->execute();
-
- $this->success = ($res !== FALSE);
- $this->lastError = $sql->getLastError();
- return $this->success;
- }
-
public function _execute(): bool {
if ($this->context->getUser()) {
return $this->createError("You are already logged in.");
}
+ $sql = $this->context->getSQL();
$token = $this->getParam("token");
$password = $this->getParam("password");
$confirmPassword = $this->getParam("confirmPassword");
-
- $req = new CheckToken($this->context);
- $this->success = $req->execute(array("token" => $token));
- $this->lastError = $req->getLastError();
-
- if (!$this->success) {
+ $userToken = $this->checkToken($token);
+ if ($userToken === false) {
return false;
+ } else if ($userToken->getType() !== UserToken::TYPE_INVITE) {
+ return $this->createError("Invalid token type");
}
- $result = $req->getResult();
- if (strcasecmp($result["token"]["type"], "invite") !== 0) {
- return $this->createError("Invalid token type");
- } else if ($result["user"]["confirmed"]) {
+ $user = $userToken->getUser();
+ if ($user->confirmed) {
return $this->createError("Your email address is already confirmed.");
} else if (!$this->checkPasswordRequirements($password, $confirmPassword)) {
return false;
- } else if (!$this->updateUser($result["user"]["id"], $password)) {
- return false;
} else {
-
- // Invalidate token
- $this->context->getSQL()
- ->update("UserToken")
- ->set("used", true)
- ->where(new Compare("token", $token))
- ->execute();
-
- return true;
+ $user->password = $this->hashPassword($password);
+ $user->confirmed = true;
+ if ($user->save($sql)) {
+ $userToken->invalidate($sql);
+ return true;
+ } else {
+ return $this->createError("Unable to update user details: " . $sql->getLastError());
+ }
}
}
}
@@ -629,44 +552,33 @@ namespace Core\API\User {
$this->csrfTokenRequired = false;
}
- private function updateUser($uid): bool {
- $sql = $this->context->getSQL();
- $res = $sql->update("User")
- ->set("confirmed", true)
- ->where(new Compare("id", $uid))
- ->execute();
-
- $this->success = ($res !== FALSE);
- $this->lastError = $sql->getLastError();
- return $this->success;
- }
-
public function _execute(): bool {
if ($this->context->getUser()) {
return $this->createError("You are already logged in.");
}
+ $sql = $this->context->getSQL();
$token = $this->getParam("token");
- $req = new CheckToken($this->context);
- $this->success = $req->execute(array("token" => $token));
- $this->lastError = $req->getLastError();
-
- if ($this->success) {
- $result = $req->getResult();
- if (strcasecmp($result["token"]["type"], "email_confirm") !== 0) {
- return $this->createError("Invalid token type");
- } else if ($result["user"]["confirmed"]) {
- return $this->createError("Your email address is already confirmed.");
- } else if (!$this->updateUser($result["user"]["id"])) {
- return false;
- } else {
- $this->invalidateToken($token);
- return true;
- }
+ $userToken = $this->checkToken($token);
+ if ($userToken === false) {
+ return false;
+ } else if ($userToken->getType() !== UserToken::TYPE_EMAIL_CONFIRM) {
+ return $this->createError("Invalid token type");
}
- return $this->success;
+ $user = $userToken->getUser();
+ if ($user->confirmed) {
+ return $this->createError("Your email address is already confirmed.");
+ } else {
+ $user->confirmed = true;
+ if ($user->save($sql)) {
+ $userToken->invalidate($sql);
+ return true;
+ } else {
+ return $this->createError("Unable to update user details: " . $sql->getLastError());
+ }
+ }
}
}
@@ -705,39 +617,29 @@ namespace Core\API\User {
$stayLoggedIn = $this->getParam('stayLoggedIn');
$sql = $this->context->getSQL();
- $res = $sql->select("User.id", "User.password", "User.confirmed",
- "TwoFactorToken.id as 2fa_id", "TwoFactorToken.type as 2fa_type",
- "TwoFactorToken.confirmed as 2fa_confirmed", "TwoFactorToken.data as 2fa_data")
- ->from("User")
+ $user = User::findBuilder($sql)
->where(new Compare("User.name", $username), new Compare("User.email", $username))
- ->leftJoin("TwoFactorToken", "TwoFactorToken.id", "User.two_factor_token_id")
- ->first()
+ ->fetchEntities()
->execute();
- $session = null;
- $this->success = ($res !== FALSE);
- $this->lastError = $sql->getLastError();
-
- if ($this->success) {
- if ($res === null) {
+ if ($user !== false) {
+ if ($user === null) {
return $this->wrongCredentials();
} else {
- $userId = $res['id'];
- $confirmed = $sql->parseBool($res["confirmed"]);
- $token = $res["2fa_id"] ? TwoFactorToken::fromRow($sql, DatabaseEntityHandler::getPrefixedRow($res, "2fa_")) : null;
- if (password_verify($password, $res['password'])) {
- if (!$confirmed) {
+ if (password_verify($password, $user->password)) {
+ if (!$user->confirmed) {
$this->result["emailConfirmed"] = false;
return $this->createError("Your email address has not been confirmed yet.");
- } else if (!($session = $this->context->createSession($userId, $stayLoggedIn))) {
+ } else if (!($session = $this->context->createSession($user, $stayLoggedIn))) {
return $this->createError("Error creating Session: " . $sql->getLastError());
} else {
+ $tfaToken = $user->getTwoFactorToken();
$this->result["loggedIn"] = true;
$this->result["logoutIn"] = $session->getExpiresSeconds();
$this->result["csrf_token"] = $session->getCsrfToken();
- if ($token && $token->isConfirmed()) {
- $this->result["2fa"] = ["type" => $token->getType()];
- if ($token instanceof KeyBasedTwoFactorToken) {
+ if ($tfaToken && $tfaToken->isConfirmed()) {
+ $this->result["2fa"] = ["type" => $tfaToken->getType()];
+ if ($tfaToken instanceof KeyBasedTwoFactorToken) {
$challenge = base64_encode(generateRandomString(32, "raw"));
$this->result["2fa"]["challenge"] = $challenge;
$_SESSION["challenge"] = $challenge;
@@ -749,6 +651,8 @@ namespace Core\API\User {
return $this->wrongCredentials();
}
}
+ } else {
+ return $this->createError("Error fetching user details: " . $sql->getLastError());
}
return $this->success;
@@ -779,9 +683,6 @@ namespace Core\API\User {
class Register extends UserAPI {
- private ?int $userId;
- private string $token;
-
public function __construct(Context $context, bool $externalCall = false) {
$parameters = array(
"username" => new StringType("username", 32),
@@ -837,14 +738,17 @@ namespace Core\API\User {
}, explode(".", $fullName))
);
- $this->userId = $this->insertUser($username, $email, $password, false, $fullName);
- if (!$this->success) {
+ $sql = $this->context->getSQL();
+ $user = $this->insertUser($username, $email, $password, false, $fullName);
+ if ($user === false) {
return false;
}
$validHours = 48;
- $this->token = generateRandomString(36);
- if ($this->insertToken($this->userId, $this->token, "email_confirm", $validHours)) {
+ $token = generateRandomString(36);
+ $userToken = new UserToken($user, $token, UserToken::TYPE_EMAIL_CONFIRM, $validHours);
+
+ if ($userToken->save($sql)) {
$baseUrl = $settings->getBaseUrl();
$siteName = $settings->getSiteName();
@@ -852,7 +756,7 @@ namespace Core\API\User {
$this->success = $req->execute([
"file" => "mail/confirm_email.twig",
"parameters" => [
- "link" => "$baseUrl/confirmEmail?token=$this->token",
+ "link" => "$baseUrl/confirmEmail?token=$token",
"site_name" => $siteName,
"base_url" => $baseUrl,
"username" => $username,
@@ -868,10 +772,12 @@ namespace Core\API\User {
"to" => $email,
"subject" => "[$siteName] E-Mail Confirmation",
"body" => $messageBody,
- "async" => true,
));
$this->lastError = $request->getLastError();
}
+ } else {
+ $this->lastError = "Could create user token: " . $sql->getLastError();
+ $this->success = false;
}
if (!$this->success) {
@@ -880,59 +786,7 @@ namespace Core\API\User {
"Please contact the server administration. This issue has been automatically logged. Reason: " . $this->lastError;
}
- $this->logger->info("Registered new user with id=" . $this->userId);
- return $this->success;
- }
- }
-
- class CheckToken extends UserAPI {
-
- public function __construct(Context $context, $externalCall = false) {
- parent::__construct($context, $externalCall, array(
- 'token' => new StringType('token', 36),
- ));
- }
-
- private function checkToken($token) {
- $sql = $this->context->getSQL();
- $res = $sql->select("UserToken.token_type", "User.id", "User.name", "User.email", "User.confirmed")
- ->from("UserToken")
- ->innerJoin("User", "UserToken.user_id", "User.id")
- ->where(new Compare("UserToken.token", $token))
- ->where(new Compare("UserToken.valid_until", $sql->now(), ">"))
- ->where(new Compare("UserToken.used", 0))
- ->execute();
- $this->lastError = $sql->getLastError();
- $this->success = ($res !== FALSE);
-
- if ($this->success && !empty($res)) {
- return $res[0];
- }
-
- return array();
- }
-
- public function _execute(): bool {
-
- $token = $this->getParam('token');
- $tokenEntry = $this->checkToken($token);
-
- if ($this->success) {
- if (!empty($tokenEntry)) {
- $this->result["token"] = array(
- "type" => $tokenEntry["token_type"]
- );
-
- $this->result["user"] = array(
- "name" => $tokenEntry["name"],
- "email" => $tokenEntry["email"],
- "id" => $tokenEntry["id"],
- "confirmed" => $tokenEntry["confirmed"]
- );
- } else {
- return $this->createError("This token does not exist or is no longer valid");
- }
- }
+ $this->logger->info("Registered new user with id=" . $user->getId());
return $this->success;
}
}
@@ -956,11 +810,13 @@ namespace Core\API\User {
public function _execute(): bool {
+ $sql = $this->context->getSQL();
+ $currentUser = $this->context->getUser();
$id = $this->getParam("id");
- $user = $this->getUser($id);
+ $user = User::find($sql, $id, true);
- if ($this->success) {
- if (empty($user)) {
+ if ($user !== false) {
+ if ($user === null) {
return $this->createError("User not found");
}
@@ -986,45 +842,35 @@ namespace Core\API\User {
$groupIds[] = $param->value;
}
- if ($id === $this->context->getUser()->getId() && !in_array(USER_GROUP_ADMIN, $groupIds)) {
+ if ($id === $currentUser->getId() && !in_array(USER_GROUP_ADMIN, $groupIds)) {
return $this->createError("Cannot remove Administrator group from own user.");
}
}
// Check for duplicate username, email
- $usernameChanged = !is_null($username) && strcasecmp($username, $user[0]["name"]) !== 0;
- $fullNameChanged = !is_null($fullName) && strcasecmp($fullName, $user[0]["full_name"]) !== 0;
- $emailChanged = !is_null($email) && strcasecmp($email, $user[0]["email"]) !== 0;
+ $usernameChanged = !is_null($username) && strcasecmp($username, $user->name) !== 0;
+ $fullNameChanged = !is_null($fullName) && strcasecmp($fullName, $user->fullName) !== 0;
+ $emailChanged = !is_null($email) && strcasecmp($email, $user->email) !== 0;
if ($usernameChanged || $emailChanged) {
if (!$this->checkUserExists($usernameChanged ? $username : NULL, $emailChanged ? $email : NULL)) {
return false;
}
}
- $sql = $this->context->getSQL();
- $query = $sql->update("User");
-
- if ($usernameChanged) $query->set("name", $username);
- if ($fullNameChanged) $query->set("full_name", $fullName);
- if ($emailChanged) $query->set("email", $email);
- if (!is_null($password)) $query->set("password", $this->hashPassword($password));
+ if ($usernameChanged) $user->name = $username;
+ if ($fullNameChanged) $user->fullName = $fullName;
+ if ($emailChanged) $user->email = $email;
+ if (!is_null($password)) $user->password = $this->hashPassword($password);
if (!is_null($confirmed)) {
- if ($id === $this->context->getUser()->getId() && $confirmed === false) {
+ if ($id === $currentUser->getId() && $confirmed === false) {
return $this->createError("Cannot make own account unconfirmed.");
} else {
- $query->set("confirmed", $confirmed);
+ $user->confirmed = $confirmed;
}
}
- if (!empty($query->getValues())) {
- $query->where(new Compare("User.id", $id));
- $res = $query->execute();
- $this->lastError = $sql->getLastError();
- $this->success = ($res !== FALSE);
- }
-
- if ($this->success) {
+ if ($user->save($sql)) {
$deleteQuery = $sql->delete("UserGroup")->where(new Compare("user_id", $id));
$insertQuery = $sql->insert("UserGroup", array("user_id", "group_id"));
@@ -1036,6 +882,8 @@ namespace Core\API\User {
$this->success = ($deleteQuery->execute() !== FALSE) && (empty($groupIds) || $insertQuery->execute() !== FALSE);
$this->lastError = $sql->getLastError();
}
+ } else {
+ return $this->createError("Error fetching user details: " . $sql->getLastError());
}
return $this->success;
@@ -1054,19 +902,18 @@ namespace Core\API\User {
public function _execute(): bool {
+ $currentUser = $this->context->getUser();
$id = $this->getParam("id");
- if ($id === $this->context->getUser()->getId()) {
+ if ($id === $currentUser->getId()) {
return $this->createError("You cannot delete your own user.");
}
- $user = $this->getUser($id);
- if ($this->success) {
- if (empty($user)) {
+ $sql = $this->context->getSQL();
+ $user = User::find($sql, $id);
+ if ($user !== false) {
+ if ($user === null) {
return $this->createError("User not found");
} else {
-
- $sql = $this->context->getSQL();
- $user = new User($id);
$this->success = ($user->delete($sql) !== FALSE);
$this->lastError = $sql->getLastError();
}
@@ -1109,17 +956,20 @@ namespace Core\API\User {
}
}
+ $sql = $this->context->getSQL();
$email = $this->getParam("email");
- $user = $this->findUser($email);
- if ($this->success === false) {
- return false;
- }
-
- if ($user !== null) {
+ $user = User::findBuilder($sql)
+ ->where(new Compare("email", $email))
+ ->fetchEntities()
+ ->execute();
+ if ($user === false) {
+ return $this->createError("Could not fetch user details: " . $sql->getLastError());
+ } else if ($user !== null) {
$validHours = 1;
$token = generateRandomString(36);
- if (!$this->insertToken($user["id"], $token, "password_reset", $validHours)) {
- return false;
+ $userToken = new UserToken($user, $token, UserToken::TYPE_PASSWORD_RESET, $validHours);
+ if (!$userToken->save($sql)) {
+ return $this->createError("Could not create user token: " . $sql->getLastError());
}
$baseUrl = $settings->getBaseUrl();
@@ -1132,7 +982,7 @@ namespace Core\API\User {
"link" => "$baseUrl/resetPassword?token=$token",
"site_name" => $siteName,
"base_url" => $baseUrl,
- "username" => $user["name"],
+ "username" => $user->name,
"valid_time" => $this->formatDuration($validHours, "hour")
]
]);
@@ -1141,11 +991,8 @@ namespace Core\API\User {
if ($this->success) {
$messageBody = $req->getResult()["html"];
- $gpgFingerprint = null;
- if ($user["gpg_id"] && $user["gpg_confirmed"]) {
- $gpgFingerprint = $user["gpg_fingerprint"];
- }
-
+ $gpgKey = $user->getGPG();
+ $gpgFingerprint = ($gpgKey && $gpgKey->isConfirmed()) ? $gpgKey->getFingerprint() : null;
$request = new \Core\API\Mail\Send($this->context);
$this->success = $request->execute(array(
"to" => $email,
@@ -1154,33 +1001,12 @@ namespace Core\API\User {
"gpgFingerprint" => $gpgFingerprint
));
$this->lastError = $request->getLastError();
- $this->logger->info("Requested password reset for user id=" . $user["id"] . " by ip_address=" . $_SERVER["REMOTE_ADDR"]);
+ $this->logger->info("Requested password reset for user id=" . $user->getId() . " by ip_address=" . $_SERVER["REMOTE_ADDR"]);
}
}
return $this->success;
}
-
- private function findUser($email): ?array {
- $sql = $this->context->getSQL();
- $res = $sql->select("User.id", "User.name",
- "User.gpg_id", "GpgKey.confirmed as gpg_confirmed", "GpgKey.fingerprint as gpg_fingerprint")
- ->from("User")
- ->leftJoin("GpgKey", "GpgKey.id", "User.gpg_id")
- ->where(new Compare("User.email", $email))
- ->where(new CondBool("User.confirmed"))
- ->execute();
-
- $this->success = ($res !== FALSE);
- $this->lastError = $sql->getLastError();
- if ($this->success) {
- if (!empty($res)) {
- return $res[0];
- }
- }
-
- return null;
- }
}
class ResendConfirmEmail extends UserAPI {
@@ -1214,46 +1040,39 @@ namespace Core\API\User {
$email = $this->getParam("email");
$sql = $this->context->getSQL();
- $res = $sql->select("User.id", "User.name", "UserToken.token", "UserToken.token_type", "UserToken.used")
- ->from("User")
- ->leftJoin("UserToken", "User.id", "UserToken.user_id")
+ $user = User::findBuilder($sql)
->where(new Compare("User.email", $email))
->where(new Compare("User.confirmed", false))
->execute();
- $this->success = ($res !== FALSE);
- $this->lastError = $sql->getLastError();
- if (!$this->success) {
- return $this->createError($sql->getLastError());
- } else if (!is_array($res) || empty($res)) {
- // user does not exist
+ if ($user === false) {
+ return $this->createError("Error retrieving user details: " . $sql->getLastError());
+ } else if ($user === null) {
+ // token does not exist: ignore!
return true;
}
- $userId = $res[0]["id"];
- $token = current(
- array_map(function ($row) {
- return $row["token"];
- }, array_filter($res, function ($row) use ($sql) {
- return !$sql->parseBool($row["used"]) && $row["token_type"] === "email_confirm";
- }))
- );
+ $userToken = UserToken::findBuilder($sql)
+ ->where(new Compare("used", false))
+ ->where(new Compare("tokenType", UserToken::TYPE_EMAIL_CONFIRM))
+ ->where(new Compare("user_id", $user->getId()))
+ ->execute();
$validHours = 48;
- if (!$token) {
+ if ($userToken === false) {
+ return $this->createError("Error retrieving token details: " . $sql->getLastError());
+ } else if ($userToken === null) {
// no token generated yet, let's generate one
$token = generateRandomString(36);
- if (!$this->insertToken($userId, $token, "email_confirm", $validHours)) {
- return false;
+ $userToken = new UserToken($user, $token, UserToken::TYPE_EMAIL_CONFIRM, $validHours);
+ if (!$userToken->save($sql)) {
+ return $this->createError("Error generating new token: " . $sql->getLastError());
}
} else {
- $sql->update("UserToken")
- ->set("valid_until", (new DateTime())->modify("+$validHours hour"))
- ->where(new Compare("token", $token))
- ->execute();
+ $userToken->updateDurability($sql, $validHours);
}
- $username = $res[0]["name"];
+ $username = $user->name;
$baseUrl = $settings->getBaseUrl();
$siteName = $settings->getSiteName();
@@ -1261,7 +1080,7 @@ namespace Core\API\User {
$this->success = $req->execute([
"file" => "mail/confirm_email.twig",
"parameters" => [
- "link" => "$baseUrl/confirmEmail?token=$token",
+ "link" => "$baseUrl/confirmEmail?token=" . $userToken->getToken(),
"site_name" => $siteName,
"base_url" => $baseUrl,
"username" => $username,
@@ -1298,46 +1117,35 @@ namespace Core\API\User {
$this->csrfTokenRequired = false;
}
- private function updateUser($uid, $password): bool {
- $sql = $this->context->getSQL();
- $res = $sql->update("User")
- ->set("password", $this->hashPassword($password))
- ->where(new Compare("id", $uid))
- ->execute();
-
- $this->success = ($res !== FALSE);
- $this->lastError = $sql->getLastError();
- return $this->success;
- }
-
public function _execute(): bool {
if ($this->context->getUser()) {
return $this->createError("You are already logged in.");
}
+ $sql = $this->context->getSQL();
$token = $this->getParam("token");
$password = $this->getParam("password");
$confirmPassword = $this->getParam("confirmPassword");
-
- $req = new CheckToken($this->context);
- $this->success = $req->execute(array("token" => $token));
- $this->lastError = $req->getLastError();
- if (!$this->success) {
+ $userToken = $this->checkToken($token);
+ if ($userToken === false) {
return false;
+ } else if ($userToken->getType() !== UserToken::TYPE_PASSWORD_RESET) {
+ return $this->createError("Invalid token type");
}
- $result = $req->getResult();
- if (strcasecmp($result["token"]["type"], "password_reset") !== 0) {
- return $this->createError("Invalid token type");
- } else if (!$this->checkPasswordRequirements($password, $confirmPassword)) {
- return false;
- } else if (!$this->updateUser($result["user"]["id"], $password)) {
+ $user = $token->getUser();
+ if (!$this->checkPasswordRequirements($password, $confirmPassword)) {
return false;
} else {
- $this->logger->info("Issued password reset for user id=" . $result["user"]["id"]);
- $this->invalidateToken($token);
- return true;
+ $user->password = $this->hashPassword($password);
+ if ($user->save($sql)) {
+ $this->logger->info("Issued password reset for user id=" . $user->getId());
+ $userToken->invalidate($sql);
+ return true;
+ } else {
+ return $this->createError("Error updating user details: " . $sql->getLastError());
+ }
}
}
}
@@ -1370,43 +1178,33 @@ namespace Core\API\User {
}
$sql = $this->context->getSQL();
- $query = $sql->update("User")->where(new Compare("id", $this->context->getUser()->getId()));
+
+ $currentUser = $this->context->getUser();
if ($newUsername !== null) {
if (!$this->checkUsernameRequirements($newUsername) || !$this->checkUserExists($newUsername)) {
return false;
} else {
- $query->set("name", $newUsername);
+ $currentUser->name = $newUsername;
}
}
if ($newFullName !== null) {
- $query->set("full_name", $newFullName);
+ $currentUser->fullName = $newFullName;
}
if ($newPassword !== null || $newPasswordConfirm !== null) {
if (!$this->checkPasswordRequirements($newPassword, $newPasswordConfirm)) {
return false;
} else {
- $res = $sql->select("password")
- ->from("User")
- ->where(new Compare("id", $this->context->getUser()->getId()))
- ->execute();
-
- $this->success = ($res !== false);
- $this->lastError = $sql->getLastError();
- if (!$this->success) {
- return false;
- }
-
- if (!password_verify($oldPassword, $res[0]["password"])) {
+ if (!password_verify($oldPassword, $currentUser->password)) {
return $this->createError("Wrong password");
}
- $query->set("password", $this->hashPassword($newPassword));
+ $currentUser->password = $this->hashPassword($newPassword);
}
}
- $this->success = $query->execute();
+ $this->success = $currentUser->save($sql) !== false;
$this->lastError = $sql->getLastError();
return $this->success;
}
@@ -1425,7 +1223,7 @@ namespace Core\API\User {
private function testKey(string $keyString) {
$res = GpgKey::getKeyInfo($keyString);
if (!$res["success"]) {
- return $this->createError($res["error"]);
+ return $this->createError($res["error"] ?? $res["msg"]);
}
$keyData = $res["data"];
@@ -1466,38 +1264,15 @@ namespace Core\API\User {
}
$sql = $this->context->getSQL();
- $res = $sql->insert("GpgKey", ["fingerprint", "algorithm", "expires"])
- ->addRow($keyData["fingerprint"], $keyData["algorithm"], $keyData["expires"])
- ->returning("id")
- ->execute();
-
- $this->success = ($res !== false);
- $this->lastError = $sql->getLastError();
- if (!$this->success) {
- return false;
- }
-
- $gpgKeyId = $sql->getLastInsertId();
- $res = $sql->update("User")
- ->set("gpg_id", $gpgKeyId)
- ->where(new Compare("id", $currentUser->getId()))
- ->execute();
-
- $this->success = ($res !== false);
- $this->lastError = $sql->getLastError();
- if (!$this->success) {
- return false;
+ $gpgKey = new GpgKey($keyData["fingerprint"], $keyData["algorithm"], $keyData["expires"]);
+ if (!$gpgKey->save($sql)) {
+ return $this->createError("Error creating gpg key: " . $sql->getLastError());
}
$token = generateRandomString(36);
- $res = $sql->insert("UserToken", ["user_id", "token", "token_type", "valid_until"])
- ->addRow($currentUser->getId(), $token, "gpg_confirm", (new DateTime())->modify("+1 hour"))
- ->execute();
-
- $this->success = ($res !== false);
- $this->lastError = $sql->getLastError();
- if (!$this->success) {
- return false;
+ $userToken = new UserToken($currentUser, $token, UserToken::TYPE_GPG_CONFIRM, 1);
+ if (!$userToken->save($sql)) {
+ return $this->createError("Error saving user token: " . $sql->getLastError());
}
$name = htmlspecialchars($currentUser->getFullName());
@@ -1508,32 +1283,32 @@ namespace Core\API\User {
$settings = $this->context->getSettings();
$baseUrl = htmlspecialchars($settings->getBaseUrl());
$token = htmlspecialchars(urlencode($token));
- $url = "$baseUrl/settings?confirmGPG&token=$token";
+ $url = "$baseUrl/settings?confirmGPG&token=$token"; // TODO: fix this url
$mailBody = "Hello $name,
" .
"you imported a GPG public key for end-to-end encrypted mail communication. " .
"To confirm the key and verify, you own the corresponding private key, please click on the following link. " .
"The link is active for one hour.
" .
"$url
- Best Regards
- ilum:e Security Lab";
+ Best Regards
" .
+ $settings->getSiteName() . " Administration";
$sendMail = new \Core\API\Mail\Send($this->context);
$this->success = $sendMail->execute(array(
"to" => $currentUser->getEmail(),
- "subject" => "Security Lab - Confirm GPG-Key",
+ "subject" => $settings->getSiteName() . " - Confirm GPG-Key",
"body" => $mailBody,
- "gpgFingerprint" => $keyData["fingerprint"]
+ "gpgFingerprint" => $gpgKey->getFingerprint()
));
$this->lastError = $sendMail->getLastError();
if ($this->success) {
- $this->result["gpg"] = array(
- "fingerprint" => $keyData["fingerprint"],
- "confirmed" => false,
- "algorithm" => $keyData["algorithm"],
- "expires" => $keyData["expires"]->getTimestamp()
- );
+ $currentUser->gpgKey = $gpgKey;
+ if ($currentUser->save($sql)) {
+ $this->result["gpg"] = $gpgKey->jsonSerialize();
+ } else {
+ return $this->createError("Error updating user details: " . $sql->getLastError());
+ }
}
return $this->success;
@@ -1558,29 +1333,11 @@ namespace Core\API\User {
}
$sql = $this->context->getSQL();
- $res = $sql->select("password")
- ->from("User")
- ->where(new Compare("User.id", $currentUser->getId()))
- ->execute();
-
- $this->success = ($res !== false);
- $this->lastError = $sql->getLastError();
-
- if ($this->success && is_array($res)) {
- $hash = $res[0]["password"];
- $password = $this->getParam("password");
- if (!password_verify($password, $hash)) {
- return $this->createError("Incorrect password.");
- } else {
- $res = $sql->delete("GpgKey")
- ->where(new Compare("id",
- $sql->select("User.gpg_id")
- ->from("User")
- ->where(new Compare("User.id", $currentUser->getId()))
- ))->execute();
- $this->success = ($res !== false);
- $this->lastError = $sql->getLastError();
- }
+ $password = $this->getParam("password");
+ if (!password_verify($password, $currentUser->password)) {
+ return $this->createError("Incorrect password.");
+ } else if (!$gpgKey->delete($sql)) {
+ return $this->createError("Error deleting gpg key: " . $sql->getLastError());
}
return $this->success;
@@ -1608,41 +1365,26 @@ namespace Core\API\User {
$token = $this->getParam("token");
$sql = $this->context->getSQL();
- $res = $sql->select($sql->count())
- ->from("UserToken")
+
+ $userToken = UserToken::findBuilder($sql)
->where(new Compare("token", $token))
->where(new Compare("valid_until", $sql->now(), ">="))
->where(new Compare("user_id", $currentUser->getId()))
- ->where(new Compare("token_type", "gpg_confirm"))
- ->where(new CondNot(new CondBool("used")))
+ ->where(new Compare("token_type", UserToken::TYPE_GPG_CONFIRM))
->execute();
- $this->success = ($res !== false);
- $this->lastError = $sql->getLastError();
-
- if ($this->success && is_array($res)) {
- if ($res[0]["count"] === 0) {
+ if ($userToken !== false) {
+ if ($userToken === null) {
return $this->createError("Invalid token");
} else {
- $res = $sql->update("GpgKey")
- ->set("confirmed", 1)
- ->where(new Compare("id", $gpgKey->getId()))
- ->execute();
-
- $this->success = ($res !== false);
- $this->lastError = $sql->getLastError();
- if (!$this->success) {
- return false;
+ if (!$gpgKey->confirm($sql)) {
+ return $this->createError("Error updating gpg key: " . $sql->getLastError());
}
- $res = $sql->update("UserToken")
- ->set("used", 1)
- ->where(new Compare("token", $token))
- ->execute();
-
- $this->success = ($res !== false);
- $this->lastError = $sql->getLastError();
+ $userToken->invalidate($sql);
}
+ } else {
+ return $this->createError("Error validating token: " . $sql->getLastError());
}
return $this->success;
@@ -1676,24 +1418,23 @@ namespace Core\API\User {
}
$email = $currentUser->getEmail();
- $gpgFingerprint = $gpgKey->getFingerprint();
} else {
- $req = new Get($this->context);
- $this->success = $req->execute(["id" => $userId]);
- $this->lastError = $req->getLastError();
- if (!$this->success) {
- return false;
+ $sql = $this->context->getSQL();
+ $user = User::find($sql, $userId, true);
+ if ($user === false) {
+ return $this->createError("Error fetching user details: " . $sql->getLastError());
+ } else if ($user === null) {
+ return $this->createError("User not found");
}
- $res = $req->getResult()["user"];
- $email = $res["email"];
- $gpgFingerprint = $res["gpgFingerprint"];
- if (!$gpgFingerprint) {
- return $this->createError("This user has not added a gpg key yet");
+ $email = $user->getEmail();
+ $gpgKey = $user->getGPG();
+ if (!$gpgKey || !$gpgKey->isConfirmed()) {
+ return $this->createError("This user has not added a gpg key yet or has not confirmed it yet.");
}
}
- $res = GpgKey::export($gpgFingerprint, $format !== "gpg");
+ $res = GpgKey::export($gpgKey->getFingerprint(), $format !== "gpg");
if (!$res["success"]) {
return $this->createError($res["error"]);
}
@@ -1817,14 +1558,11 @@ namespace Core\API\User {
}
$sql = $this->context->getSQL();
- $this->success = $sql->update("User")
- ->set("profile_picture", $fileName)
- ->where(new Compare("id", $userId))
- ->execute();
-
- $this->lastError = $sql->getLastError();
- if ($this->success) {
+ $currentUser->profilePicture = $fileName;
+ if ($currentUser->save($sql)) {
$this->result["profilePicture"] = $fileName;
+ } else {
+ return $this->createError("Error updating user details: " . $sql->getLastError());
}
return $this->success;
@@ -1839,25 +1577,22 @@ namespace Core\API\User {
public function _execute(): bool {
+ $sql = $this->context->getSQL();
$currentUser = $this->context->getUser();
+ $userId = $currentUser->getId();
$pfp = $currentUser->getProfilePicture();
if (!$pfp) {
return $this->createError("You did not upload a profile picture yet");
}
- $userId = $currentUser->getId();
- $sql = $this->context->getSQL();
- $this->success = $sql->update("User")
- ->set("profile_picture", NULL)
- ->where(new Compare("id", $userId))
- ->execute();
- $this->lastError = $sql->getLastError();
+ $currentUser->profilePicture = null;
+ if (!$currentUser->save($sql)) {
+ return $this->createError("Error updating user details: " . $sql->getLastError());
+ }
- if ($this->success) {
- $path = WEBROOT . "/img/uploads/user/$userId/$pfp";
- if (is_file($path)) {
- @unlink($path);
- }
+ $path = WEBROOT . "/img/uploads/user/$userId/$pfp";
+ if (is_file($path)) {
+ @unlink($path);
}
return $this->success;
diff --git a/Core/Configuration/CreateDatabase.class.php b/Core/Configuration/CreateDatabase.class.php
index 5e8ff85..d91390b 100644
--- a/Core/Configuration/CreateDatabase.class.php
+++ b/Core/Configuration/CreateDatabase.class.php
@@ -19,14 +19,6 @@ class CreateDatabase extends DatabaseScript {
->addRow("en_US", 'American English')
->addRow("de_DE", 'Deutsch Standard');
- $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", "id", new CascadeStrategy());
-
$queries[] = $sql->insert("Group", array("name", "color"))
->addRow(USER_GROUP_MODERATOR_NAME, "#007bff")
->addRow(USER_GROUP_SUPPORT_NAME, "#28a745")
diff --git a/Core/Configuration/Settings.class.php b/Core/Configuration/Settings.class.php
index ec3b42c..7d42e53 100644
--- a/Core/Configuration/Settings.class.php
+++ b/Core/Configuration/Settings.class.php
@@ -15,20 +15,28 @@ class Settings {
//
private bool $installationComplete;
- // settings
+ // general settings
private string $siteName;
private string $baseUrl;
+ private bool $registrationAllowed;
+ private array $allowedExtensions;
+ private string $timeZone;
+
+ // jwt
private ?string $jwtPublicKey;
private ?string $jwtSecretKey;
private string $jwtAlgorithm;
- private bool $registrationAllowed;
+
+ // recaptcha
private bool $recaptchaEnabled;
- private bool $mailEnabled;
private string $recaptchaPublicKey;
private string $recaptchaPrivateKey;
+
+ // mail
+ private bool $mailEnabled;
private string $mailSender;
private string $mailFooter;
- private array $allowedExtensions;
+ private bool $mailAsync;
//
private Logger $logger;
@@ -55,7 +63,11 @@ class Settings {
}
public static function loadDefaults(): Settings {
- $hostname = $_SERVER["SERVER_NAME"] ?? "localhost";
+ $hostname = $_SERVER["SERVER_NAME"];
+ if (empty($hostname)) {
+ $hostname = "localhost";
+ }
+
$protocol = getProtocol();
$settings = new Settings();
@@ -65,6 +77,7 @@ class Settings {
$settings->allowedExtensions = ['png', 'jpg', 'jpeg', 'gif', 'htm', 'html'];
$settings->installationComplete = false;
$settings->registrationAllowed = false;
+ $settings->timeZone = date_default_timezone_get();
// JWT
$settings->jwtSecretKey = null;
@@ -80,7 +93,7 @@ class Settings {
$settings->mailEnabled = false;
$settings->mailSender = "webmaster@localhost";
$settings->mailFooter = "";
-
+ $settings->mailAsync = false;
return $settings;
}
@@ -118,7 +131,7 @@ class Settings {
return in_array(strtoupper($algorithm), ["HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "EDDSA"]);
}
- public function saveJwtKey(Context $context) {
+ public function saveJwtKey(Context $context): \Core\API\Settings\Set {
$req = new \Core\API\Settings\Set($context);
$req->execute(array("settings" => array(
"jwt_secret_key" => $this->jwtSecretKey,
@@ -140,6 +153,7 @@ class Settings {
$this->baseUrl = $result["base_url"] ?? $this->baseUrl;
$this->registrationAllowed = $result["user_registration_enabled"] ?? $this->registrationAllowed;
$this->installationComplete = $result["installation_completed"] ?? $this->installationComplete;
+ $this->timeZone = $result["time_zone"] ?? $this->timeZone;
$this->jwtSecretKey = $result["jwt_secret_key"] ?? $this->jwtSecretKey;
$this->jwtPublicKey = $result["jwt_public_key"] ?? $this->jwtPublicKey;
$this->jwtAlgorithm = $result["jwt_algorithm"] ?? $this->jwtAlgorithm;
@@ -149,6 +163,7 @@ class Settings {
$this->mailEnabled = $result["mail_enabled"] ?? $this->mailEnabled;
$this->mailSender = $result["mail_from"] ?? $this->mailSender;
$this->mailFooter = $result["mail_footer"] ?? $this->mailFooter;
+ $this->mailAsync = $result["mail_async"] ?? $this->mailAsync;
$this->allowedExtensions = explode(",", $result["allowed_extensions"] ?? strtolower(implode(",", $this->allowedExtensions)));
if (!isset($result["jwt_secret_key"])) {
@@ -156,16 +171,19 @@ class Settings {
$this->saveJwtKey($context);
}
}
+
+ date_default_timezone_set($this->timeZone);
}
return false;
}
- public function addRows(Insert $query) {
+ public function addRows(Insert $query): void {
$query->addRow("site_name", $this->siteName, false, false)
->addRow("base_url", $this->baseUrl, false, false)
->addRow("user_registration_enabled", $this->registrationAllowed ? "1" : "0", false, false)
->addRow("installation_completed", $this->installationComplete ? "1" : "0", true, true)
+ ->addRow("time_zone", $this->timeZone, false, false)
->addRow("jwt_secret_key", $this->jwtSecretKey, true, false)
->addRow("jwt_public_key", $this->jwtPublicKey, false, false)
->addRow("jwt_algorithm", $this->jwtAlgorithm, false, false)
@@ -179,6 +197,14 @@ class Settings {
return $this->siteName;
}
+ public function getTimeZone(): string {
+ return $this->timeZone;
+ }
+
+ public function setTimeZone(string $tz) {
+ $this->timeZone = $tz;
+ }
+
public function getBaseUrl(): string {
return $this->baseUrl;
}
@@ -203,6 +229,10 @@ class Settings {
return $this->mailEnabled;
}
+ public function isMailAsync(): bool {
+ return $this->mailAsync;
+ }
+
public function getMailSender(): string {
return $this->mailSender;
}
diff --git a/Core/Documents/Install.class.php b/Core/Documents/Install.class.php
index 7d0b76b..47925c9 100644
--- a/Core/Documents/Install.class.php
+++ b/Core/Documents/Install.class.php
@@ -235,7 +235,7 @@ namespace Documents\Install {
$username = posix_getpwuid($userId)['name'];
$failedRequirements[] = sprintf("%s is not owned by current user: $username ($userId). " .
"Try running chown -R $userId %s or give the required directories write permissions: " .
- "core/Configuration, core/Cache, core/External",
+ "Site/Configuration, Core/Cache, Core/External",
WEBROOT, WEBROOT);
$success = false;
}
diff --git a/Core/Driver/Logger/Logger.class.php b/Core/Driver/Logger/Logger.class.php
index 2062a46..239cb65 100644
--- a/Core/Driver/Logger/Logger.class.php
+++ b/Core/Driver/Logger/Logger.class.php
@@ -72,7 +72,7 @@ class Logger {
$module = preg_replace("/[^a-zA-Z0-9-]/", "-", $this->module);
$date = (\DateTime::createFromFormat('U.u', microtime(true)))->format(self::LOG_FILE_DATE_FORMAT);
$logFile = implode("_", [$module, $severity, $date]) . ".log";
- $logPath = implode(DIRECTORY_SEPARATOR, [WEBROOT, "core", "Logs", $logFile]);
+ $logPath = implode(DIRECTORY_SEPARATOR, [WEBROOT, "Core", "Logs", $logFile]);
@file_put_contents($logPath, $message);
}
diff --git a/Core/Elements/TemplateDocument.class.php b/Core/Elements/TemplateDocument.class.php
index e7ae1d5..fc40c20 100644
--- a/Core/Elements/TemplateDocument.class.php
+++ b/Core/Elements/TemplateDocument.class.php
@@ -31,7 +31,7 @@ class TemplateDocument extends Document {
$this->parameters = $params;
$this->twigLoader = new FilesystemLoader(self::TEMPLATE_PATH);
$this->twigEnvironment = new Environment($this->twigLoader, [
- 'cache' => WEBROOT . '/core/Cache/Templates/',
+ 'cache' => WEBROOT . '/Core/Cache/Templates/',
'auto_reload' => true
]);
$this->twigEnvironment->addExtension(new CustomTwigFunctions());
diff --git a/Core/Objects/Context.class.php b/Core/Objects/Context.class.php
index 4a38e5c..a3c415d 100644
--- a/Core/Objects/Context.class.php
+++ b/Core/Objects/Context.class.php
@@ -7,6 +7,7 @@ use Core\Configuration\Settings;
use Core\Driver\SQL\Condition\Compare;
use Core\Driver\SQL\Condition\CondLike;
use Core\Driver\SQL\Condition\CondOr;
+use Core\Driver\SQL\Join;
use Core\Driver\SQL\SQL;
use Firebase\JWT\JWT;
use Core\Objects\DatabaseEntity\Language;
@@ -92,6 +93,9 @@ class Context {
private function loadSession(int $userId, int $sessionId) {
$this->session = Session::init($this, $userId, $sessionId);
$this->user = $this->session?->getUser();
+ if ($this->user) {
+ $this->user->session = $this->session;
+ }
}
public function parseCookies() {
@@ -173,7 +177,7 @@ class Context {
public function loadApiKey(string $apiKey): bool {
$this->user = User::findBuilder($this->sql)
- ->addJoin(new \Driver\SQL\Join("INNER","ApiKey", "ApiKey.user_id", "User.id"))
+ ->addJoin(new 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))
@@ -184,19 +188,18 @@ class Context {
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;
- }
+ public function createSession(User $user, bool $stayLoggedIn): ?Session {
+ $this->user = $user;
+ $this->session = new Session($this, $this->user);
+ $this->session->stayLoggedIn = $stayLoggedIn;
+ if ($this->session->update()) {
+ $user->session = $this->session;
+ return $this->session;
+ } else {
+ $this->user = null;
+ $this->session = null;
+ return null;
}
-
- $this->user = null;
- $this->session = null;
- return null;
}
public function getLanguage(): Language {
diff --git a/Core/Objects/DatabaseEntity/Attribute/EnumArr.class.php b/Core/Objects/DatabaseEntity/Attribute/EnumArr.class.php
new file mode 100644
index 0000000..4fd1fb4
--- /dev/null
+++ b/Core/Objects/DatabaseEntity/Attribute/EnumArr.class.php
@@ -0,0 +1,12 @@
+isInitialized($entity)) {
$value = $property->getValue($entity);
if (isset($this->relations[$propertyName])) {
- $value = $value->getId();
+ $value = $value?->getId();
}
} else if (!$this->columns[$propertyName]->notNull()) {
$value = null;
@@ -411,4 +411,8 @@ class DatabaseEntityHandler {
$this->logger->error($message);
throw new Exception($message);
}
+
+ public function getSQL(): SQL {
+ return $this->sql;
+ }
}
\ No newline at end of file
diff --git a/Core/Objects/DatabaseEntity/DatabaseEntityQuery.class.php b/Core/Objects/DatabaseEntity/DatabaseEntityQuery.class.php
index 53c5178..842372c 100644
--- a/Core/Objects/DatabaseEntity/DatabaseEntityQuery.class.php
+++ b/Core/Objects/DatabaseEntity/DatabaseEntityQuery.class.php
@@ -2,6 +2,7 @@
namespace Core\Objects\DatabaseEntity;
+use Core\Driver\Logger\Logger;
use Core\Driver\SQL\Condition\Condition;
use Core\Driver\SQL\Join;
use Core\Driver\SQL\Query\Select;
@@ -13,20 +14,29 @@ use Core\Driver\SQL\SQL;
*/
class DatabaseEntityQuery {
+ private Logger $logger;
private DatabaseEntityHandler $handler;
private Select $selectQuery;
private int $resultType;
+ private bool $logVerbose;
private function __construct(DatabaseEntityHandler $handler, int $resultType) {
$this->handler = $handler;
$this->selectQuery = $handler->getSelectQuery();
+ $this->logger = new Logger("DB-EntityQuery", $handler->getSQL());
$this->resultType = $resultType;
+ $this->logVerbose = false;
if ($this->resultType === SQL::FETCH_ONE) {
$this->selectQuery->first();
}
}
+ public function debug(): DatabaseEntityQuery {
+ $this->logVerbose = true;
+ return $this;
+ }
+
public static function fetchAll(DatabaseEntityHandler $handler): DatabaseEntityQuery {
return new DatabaseEntityQuery($handler, SQL::FETCH_ALL);
}
@@ -106,6 +116,13 @@ class DatabaseEntityQuery {
}
public function execute(): DatabaseEntity|array|null {
+
+ if ($this->logVerbose) {
+ $params = [];
+ $query = $this->selectQuery->build($params);
+ $this->logger->debug("QUERY: $query\nARGS: " . print_r($params, true));
+ }
+
$res = $this->selectQuery->execute();
if ($res === null || $res === false) {
return null;
diff --git a/Core/Objects/DatabaseEntity/GpgKey.class.php b/Core/Objects/DatabaseEntity/GpgKey.class.php
index c32139a..6a41b85 100644
--- a/Core/Objects/DatabaseEntity/GpgKey.class.php
+++ b/Core/Objects/DatabaseEntity/GpgKey.class.php
@@ -3,6 +3,7 @@
namespace Core\Objects\DatabaseEntity;
use Core\Driver\SQL\Expression\CurrentTimeStamp;
+use Core\Driver\SQL\SQL;
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
use Core\Objects\DatabaseEntity\Attribute\DefaultValue;
@@ -16,12 +17,13 @@ class GpgKey extends DatabaseEntity {
private \DateTime $expires;
#[DefaultValue(CurrentTimeStamp::class)] private \DateTime $added;
- public function __construct(int $id, bool $confirmed, string $fingerprint, string $algorithm, string $expires) {
- parent::__construct($id);
- $this->confirmed = $confirmed;
+ public function __construct(string $fingerprint, string $algorithm, \DateTime $expires) {
+ parent::__construct();
+ $this->confirmed = false;
$this->fingerprint = $fingerprint;
$this->algorithm = $algorithm;
- $this->expires = new \DateTime($expires);
+ $this->expires = $expires;
+ $this->added = new \DateTime();
}
public static function encrypt(string $body, string $gpgFingerprint): array {
@@ -130,4 +132,9 @@ class GpgKey extends DatabaseEntity {
"confirmed" => $this->confirmed
];
}
+
+ public function confirm(SQL $sql): bool {
+ $this->confirmed = true;
+ return $this->save($sql);
+ }
}
\ No newline at end of file
diff --git a/Core/Objects/DatabaseEntity/Session.class.php b/Core/Objects/DatabaseEntity/Session.class.php
index 43534b9..4fae092 100644
--- a/Core/Objects/DatabaseEntity/Session.class.php
+++ b/Core/Objects/DatabaseEntity/Session.class.php
@@ -96,7 +96,7 @@ class Session extends DatabaseEntity {
return array(
'id' => $this->getId(),
'active' => $this->active,
- 'expires' => $this->expires,
+ 'expires' => $this->expires->getTimestamp(),
'ipAddress' => $this->ipAddress,
'os' => $this->os,
'browser' => $this->browser,
diff --git a/Core/Objects/DatabaseEntity/User.class.php b/Core/Objects/DatabaseEntity/User.class.php
index 2b4c3ec..4b21130 100644
--- a/Core/Objects/DatabaseEntity/User.class.php
+++ b/Core/Objects/DatabaseEntity/User.class.php
@@ -17,12 +17,12 @@ class User extends DatabaseEntity {
#[MaxLength(128)] public string $password;
#[MaxLength(64)] public string $fullName;
#[MaxLength(64)] #[Unique] public ?string $email;
- #[MaxLength(64)] private ?string $profilePicture;
+ #[MaxLength(64)] public ?string $profilePicture;
private ?\DateTime $lastOnline;
#[DefaultValue(CurrentTimeStamp::class)] public \DateTime $registeredAt;
public bool $confirmed;
#[DefaultValue(1)] public Language $language;
- private ?GpgKey $gpgKey;
+ public ?GpgKey $gpgKey;
private ?TwoFactorToken $twoFactorToken;
#[Transient] private array $groups;
@@ -37,7 +37,6 @@ class User extends DatabaseEntity {
$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();
@@ -99,6 +98,9 @@ class User extends DatabaseEntity {
'session' => (isset($this->session) ? $this->session->jsonSerialize() : null),
"gpg" => (isset($this->gpgKey) ? $this->gpgKey->jsonSerialize() : null),
"2fa" => (isset($this->twoFactorToken) ? $this->twoFactorToken->jsonSerialize() : null),
+ "reqisteredAt" => $this->registeredAt->getTimestamp(),
+ "lastOnline" => $this->lastOnline->getTimestamp(),
+ "confirmed" => $this->confirmed
];
}
diff --git a/Core/Objects/DatabaseEntity/UserToken.class.php b/Core/Objects/DatabaseEntity/UserToken.class.php
new file mode 100644
index 0000000..0c41fff
--- /dev/null
+++ b/Core/Objects/DatabaseEntity/UserToken.class.php
@@ -0,0 +1,72 @@
+user = $user;
+ $this->token = $token;
+ $this->tokenType = $type;
+ $this->validUntil = (new \DateTime())->modify("+$validHours HOUR");
+ $this->used = false;
+ }
+
+ public function jsonSerialize(): array {
+ return [
+ "id" => $this->getId(),
+ "token" => $this->token,
+ "tokenType" => $this->tokenType
+ ];
+ }
+
+ public function getType(): string {
+ return $this->tokenType;
+ }
+
+ public function invalidate(SQL $sql): bool {
+ $this->used = true;
+ return $this->save($sql);
+ }
+
+ public function getUser(): User {
+ return $this->user;
+ }
+
+ public function updateDurability(SQL $sql, int $validHours): bool {
+ $this->validUntil = (new \DateTime())->modify("+$validHours HOURS");
+ return $this->save($sql);
+ }
+
+ public function getToken(): string {
+ return $this->token;
+ }
+}
\ No newline at end of file
diff --git a/Site/Templates/.gitkeep b/Site/Templates/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/cli.php b/cli.php
index d8ca069..fb5cc70 100644
--- a/cli.php
+++ b/cli.php
@@ -48,17 +48,6 @@ if ($database !== null && $database->getProperty("isDocker", false) && !is_file(
}
}
-/*function getUser(): ?User {
- global $config;
- $user = new User($config);
- if (!$user->getSQL() || !$user->getSQL()->isConnected()) {
- printLine("Could not establish database connection");
- return null;
- }
-
- return $user;
-}*/
-
function connectSQL(): ?SQL {
global $context;
$sql = $context->initSQL();
@@ -76,7 +65,7 @@ function printHelp() {
function applyPatch(\Core\Driver\SQL\SQL $sql, string $patchName): bool {
$class = str_replace('/', '\\', $patchName);
- $className = "\\Configuration\\$class";
+ $className = "\\Core\\Configuration\\$class";
$classPath = getClassPath($className);
if (!file_exists($classPath) || !is_readable($classPath)) {
printLine("Database script file does not exist or is not readable");
@@ -282,7 +271,7 @@ function onMaintenance(array $argv) {
_exit("Maintenance disabled");
} else if ($action === "update") {
- $oldPatchFiles = glob('core/Configuration/Patch/*.php');
+ $oldPatchFiles = glob('Core/Configuration/Patch/*.php');
printLine("$ git remote -v");
exec("git remote -v", $gitRemote, $ret);
if ($ret !== 0) {
@@ -339,14 +328,15 @@ function onMaintenance(array $argv) {
die();
}
- $newPatchFiles = glob('core/Configuration/Patch/*.php');
+ // TODO: also collect patches from Site/Configuration/Patch ... and what about database entities?
+ $newPatchFiles = glob('Core/Configuration/Patch/*.php');
$newPatchFiles = array_diff($newPatchFiles, $oldPatchFiles);
if (count($newPatchFiles) > 0) {
printLine("Applying new database patches");
$sql = connectSQL();
if ($sql) {
foreach ($newPatchFiles as $patchFile) {
- if (preg_match("/core\/Configuration\/(Patch\/.*)\.class\.php/", $patchFile, $match)) {
+ if (preg_match("/Core\/Configuration\/(Patch\/.*)\.class\.php/", $patchFile, $match)) {
$patchName = $match[1];
applyPatch($sql, $patchName);
}
@@ -415,7 +405,7 @@ function printTable(array $head, array $body) {
function onSettings(array $argv) {
global $context;
- $sql = connectSQL() or die();
+ connectSQL() or die();
$action = $argv[2] ?? "list";
if ($action === "list" || $action === "get") {
@@ -461,7 +451,7 @@ function onSettings(array $argv) {
function onRoutes(array $argv) {
global $context;
- $sql = connectSQL() or die();
+ connectSQL() or die();
$action = $argv[2] ?? "list";
if ($action === "list") {
@@ -607,6 +597,7 @@ function onMail($argv) {
global $context;
$action = $argv[2] ?? null;
if ($action === "send_queue") {
+ connectSQL() or die();
$req = new \Core\API\Mail\SendQueue($context);
$debug = in_array("debug", $argv);
if (!$req->execute(["debug" => $debug])) {
diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile
index e4385ad..4782d5b 100644
--- a/docker/php/Dockerfile
+++ b/docker/php/Dockerfile
@@ -1,11 +1,14 @@
FROM composer:latest AS composer
FROM php:8.0-fpm
WORKDIR "/application"
-RUN mkdir -p /application/core/Configuration
-RUN chown -R www-data:www-data /application
+RUN mkdir -p /application/core/Configuration /var/www/.gnupg && \
+ chown -R www-data:www-data /application /var/www/ && \
+ chmod 700 /var/www/.gnupg
# YAML + dev dependencies
-RUN apt-get update -y && apt-get install libyaml-dev libzip-dev libgmp-dev -y && apt-get clean && \
+RUN apt-get update -y && \
+ apt-get install -y libyaml-dev libzip-dev libgmp-dev gnupg2 && \
+ apt-get clean && \
pecl install yaml && docker-php-ext-enable yaml
# Runkit (no stable release available)