This commit is contained in:
2021-11-11 14:25:26 +01:00
parent 1737a2f592
commit 25d47f7528
32 changed files with 633 additions and 121 deletions

View File

@@ -6,7 +6,7 @@ namespace Api {
abstract class ApiKeyAPI extends Request {
protected function apiKeyExists($id) {
protected function apiKeyExists($id): bool {
$sql = $this->user->getSQL();
$res = $sql->select($sql->count())
->from("ApiKey")

View File

@@ -3,7 +3,7 @@
namespace Api {
abstract class PermissionAPI extends Request {
protected function checkStaticPermission() {
protected function checkStaticPermission(): bool {
if (!$this->user->isLoggedIn() || !$this->user->hasGroup(USER_GROUP_ADMIN)) {
return $this->createError("Permission denied.");
}
@@ -21,6 +21,7 @@ namespace Api\Permission {
use Driver\SQL\Column\Column;
use Driver\SQL\Condition\Compare;
use Driver\SQL\Condition\CondIn;
use Driver\SQL\Condition\CondLike;
use Driver\SQL\Condition\CondNot;
use Driver\SQL\Strategy\UpdateStrategy;
use Objects\User;
@@ -44,14 +45,14 @@ namespace Api\Permission {
$sql = $this->user->getSQL();
$res = $sql->select("groups")
->from("ApiPermission")
->where(new Compare("method", $method))
->where(new CondLike($method, new Column("method")))
->execute();
$this->success = ($res !== FALSE);
$this->lastError = $sql->getLastError();
if ($this->success) {
if (empty($res)) {
if (empty($res) || !is_array($res)) {
return true;
}

View File

@@ -45,9 +45,13 @@ class Request {
}
}
public function parseParams($values): bool {
public function parseParams($values, $structure = NULL): bool {
foreach ($this->params as $name => $param) {
if ($structure === NULL) {
$structure = $this->params;
}
foreach ($structure as $name => $param) {
$value = $values[$name] ?? NULL;
$isEmpty = (is_string($value) && strlen($value) === 0) || (is_array($value) && empty($value));
@@ -90,7 +94,7 @@ class Request {
$values = $_REQUEST;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_SERVER["CONTENT_TYPE"]) && in_array("application/json", explode(";", $_SERVER["CONTENT_TYPE"]))) {
$jsonData = json_decode(file_get_contents('php://input'), true);
if ($jsonData) {
if ($jsonData !== null) {
$values = array_merge($values, $jsonData);
} else {
$this->lastError = 'Invalid request body.';
@@ -124,9 +128,12 @@ class Request {
// Logged in or api key authorized?
if ($this->loginRequired) {
if (isset($values['api_key']) && $this->apiKeyAllowed) {
$apiKey = $values['api_key'];
$apiKeyAuthorized = $this->user->authorize($apiKey);
if (isset($_SERVER["HTTP_AUTHORIZATION"]) && $this->apiKeyAllowed) {
$authHeader = $_SERVER["HTTP_AUTHORIZATION"];
if (startsWith($authHeader, "Bearer ")) {
$apiKey = substr($authHeader, strlen("Bearer "));
$apiKeyAuthorized = $this->user->authorize($apiKey);
}
}
if (!$this->user->isLoggedIn() && !$apiKeyAuthorized) {
@@ -182,9 +189,13 @@ class Request {
return false;
}
protected function getParam($name) {
protected function getParam($name, $obj = NULL) {
// i don't know why phpstorm
return (isset($this->params[$name]) ? $this->params[$name]->value : NULL);
if ($obj === NULL) {
$obj = $this->params;
}
return (isset($obj[$name]) ? $obj[$name]->value : NULL);
}
public function isPublic(): bool {
@@ -222,4 +233,16 @@ class Request {
$this->result['msg'] = $this->lastError;
return json_encode($this->result);
}
protected function disableOutputBuffer() {
header('X-Accel-Buffering: no');
header("Cache-Control: no-transform, no-store, max-age=0");
ob_implicit_flush(true);
$levels = ob_get_level();
for ( $i = 0; $i < $levels; $i ++ ) {
ob_end_flush();
}
flush();
}
}

View File

@@ -138,6 +138,7 @@ namespace Api\Routes {
->from("Route")
->where(new CondBool("active"))
->where(new CondRegex($request, new Column("request")))
->orderBy("uid")->ascending()
->limit(1)
->execute();

View File

@@ -6,7 +6,7 @@ namespace Api {
abstract class UserAPI extends Request {
protected function userExists(?string $username, ?string $email) {
protected function userExists(?string $username, ?string $email = null) {
$conditions = array();
if ($username) {
@@ -52,12 +52,19 @@ namespace Api {
return true;
}
protected function checkRequirements($username, $password, $confirmPassword) {
if(strlen($username) < 5 || strlen($username) > 32) {
protected function checkUsernameRequirements($username): bool {
if (strlen($username) < 5 || strlen($username) > 32) {
return $this->createError("The username should be between 5 and 32 characters long");
} else if (!preg_match("/[a-zA-Z0-9_\-]+/", $username)) {
return $this->createError("The username should only contain the following characters: a-z A-Z 0-9 _ -");
}
return $this->checkPasswordRequirements($password, $confirmPassword);
return true;
}
protected function checkRequirements($username, $password, $confirmPassword): bool {
return $this->checkUsernameRequirements($username) &&
$this->checkPasswordRequirements($password, $confirmPassword);
}
protected function insertUser($username, $email, $password, $confirmed) {
@@ -123,6 +130,18 @@ namespace Api {
->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->user->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;
}
}
}
@@ -193,7 +212,7 @@ namespace Api\User {
));
}
private function getUserCount() {
private function getUserCount(): bool {
$sql = $this->user->getSQL();
$res = $sql->select($sql->count())->from("User")->execute();
@@ -359,6 +378,23 @@ namespace Api\User {
$this->result["loggedIn"] = false;
} else {
$this->result["loggedIn"] = true;
$userGroups = array_keys($this->user->getGroups());
$sql = $this->user->getSQL();
$res = $sql->select("method", "groups")
->from("ApiPermission")
->execute();
$permissions = [];
if (is_array($res)) {
foreach ($res as $row) {
$requiredGroups = json_decode($row["groups"], true);
if (empty($requiredGroups) || !empty(array_intersect($requiredGroups, $userGroups))) {
$permissions[] = $row["method"];
}
}
}
$this->result["permissions"] = $permissions;
}
$this->result["user"] = $this->user->jsonSerialize();
@@ -456,7 +492,7 @@ namespace Api\User {
$this->csrfTokenRequired = false;
}
private function updateUser($uid, $password) {
private function updateUser($uid, $password): bool {
$sql = $this->user->getSQL();
$res = $sql->update("User")
->set("password", $this->hashPassword($password))
@@ -500,7 +536,6 @@ namespace Api\User {
} else if (!$this->updateUser($result["user"]["uid"], $password)) {
return false;
} else {
// Invalidate token
$this->user->getSQL()
->update("UserToken")
@@ -519,9 +554,10 @@ namespace Api\User {
parent::__construct($user, $externalCall, array(
'token' => new StringType('token', 36)
));
$this->csrfTokenRequired = false;
}
private function updateUser($uid) {
private function updateUser($uid): bool {
$sql = $this->user->getSQL();
$res = $sql->update("User")
->set("confirmed", true)
@@ -543,7 +579,6 @@ namespace Api\User {
}
$token = $this->getParam("token");
$req = new CheckToken($this->user);
$this->success = $req->execute(array("token" => $token));
$this->lastError = $req->getLastError();
@@ -579,7 +614,7 @@ namespace Api\User {
$this->forbidMethod("GET");
}
private function wrongCredentials() {
private function wrongCredentials(): bool {
$runtime = microtime(true) - $this->startedAt;
$sleepTime = round(3e6 - $runtime);
if ($sleepTime > 0) usleep($sleepTime);
@@ -613,7 +648,7 @@ namespace Api\User {
$this->lastError = $sql->getLastError();
if ($this->success) {
if (count($res) === 0) {
if (!is_array($res) || count($res) === 0) {
return $this->wrongCredentials();
} else {
$row = $res[0];
@@ -621,6 +656,7 @@ namespace Api\User {
$confirmed = $sql->parseBool($row["confirmed"]);
if (password_verify($password, $row['password'])) {
if (!$confirmed) {
$this->result["emailConfirmed"] = false;
return $this->createError("Your email address has not been confirmed yet.");
} else if (!($this->success = $this->user->createSession($uid, $stayLoggedIn))) {
return $this->createError("Error creating Session: " . $sql->getLastError());
@@ -681,18 +717,6 @@ namespace Api\User {
$this->csrfTokenRequired = false;
}
private function insertToken() {
$validUntil = (new DateTime())->modify("+48 hour");
$sql = $this->user->getSQL();
$res = $sql->insert("UserToken", array("user_id", "token", "token_type", "valid_until"))
->addRow($this->userId, $this->token, "email_confirm", $validUntil)
->execute();
$this->success = ($res !== FALSE);
$this->lastError = $sql->getLastError();
return $this->success;
}
public function execute($values = array()): bool {
if (!parent::execute($values)) {
return false;
@@ -720,6 +744,7 @@ namespace Api\User {
$email = $this->getParam('email');
$password = $this->getParam("password");
$confirmPassword = $this->getParam("confirmPassword");
if (!$this->userExists($username, $email)) {
return false;
}
@@ -733,14 +758,13 @@ namespace Api\User {
return false;
}
$id = $this->insertUser($username, $email, $password, false);
if ($id === FALSE) {
$this->userId = $this->insertUser($username, $email, $password, false);
if (!$this->success) {
return false;
}
$this->userId = $id;
$this->token = generateRandomString(36);
if ($this->insertToken()) {
if ($this->insertToken($this->userId, $this->token, "email_confirm", 48)) {
$settings = $this->user->getConfiguration()->getSettings();
$baseUrl = htmlspecialchars($settings->getBaseUrl());
$siteName = htmlspecialchars($settings->getSiteName());
@@ -845,6 +869,7 @@ namespace Api\User {
));
$this->loginRequired = true;
$this->forbidMethod("GET");
}
public function execute($values = array()): bool {
@@ -887,8 +912,8 @@ namespace Api\User {
}
// Check for duplicate username, email
$usernameChanged = !is_null($username) ? strcasecmp($username, $user[0]["name"]) !== 0 : false;
$emailChanged = !is_null($email) ? strcasecmp($email, $user[0]["email"]) !== 0 : false;
$usernameChanged = !is_null($username) && strcasecmp($username, $user[0]["name"]) !== 0;
$emailChanged = !is_null($email) && strcasecmp($email, $user[0]["email"]) !== 0;
if($usernameChanged || $emailChanged) {
if (!$this->userExists($usernameChanged ? $username : NULL, $emailChanged ? $email : NULL)) {
return false;
@@ -917,7 +942,7 @@ namespace Api\User {
$this->success = ($res !== FALSE);
}
if ($this->success && !empty($groupIds)) {
if ($this->success) {
$deleteQuery = $sql->delete("UserGroup")->where(new Compare("user_id", $id));
$insertQuery = $sql->insert("UserGroup", array("user_id", "group_id"));
@@ -926,7 +951,7 @@ namespace Api\User {
$insertQuery->addRow($id, $groupId);
}
$this->success = ($deleteQuery->execute() !== FALSE) && ($insertQuery->execute() !== FALSE);
$this->success = ($deleteQuery->execute() !== FALSE) && (empty($groupIds) || $insertQuery->execute() !== FALSE);
$this->lastError = $sql->getLastError();
}
}
@@ -983,7 +1008,6 @@ namespace Api\User {
}
parent::__construct($user, $externalCall, $parameters);
$this->csrfTokenRequired = false;
}
public function execute($values = array()): bool {
@@ -1017,7 +1041,7 @@ namespace Api\User {
if ($user !== null) {
$token = generateRandomString(36);
if (!$this->insertToken($user["uid"], $token)) {
if (!$this->insertToken($user["uid"], $token, "password_reset", 1)) {
return false;
}
@@ -1067,16 +1091,102 @@ namespace Api\User {
return $this->success;
}
}
private function insertToken(int $id, string $token) {
$validUntil = (new DateTime())->modify("+1 hour");
class ResendConfirmEmail extends UserAPI {
public function __construct(User $user, $externalCall = false) {
$parameters = array(
'email' => new Parameter('email', Parameter::TYPE_EMAIL),
);
$settings = $user->getConfiguration()->getSettings();
if ($settings->isRecaptchaEnabled()) {
$parameters["captcha"] = new StringType("captcha");
}
parent::__construct($user, $externalCall, $parameters);
}
public function execute($values = array()): bool {
if (!parent::execute($values)) {
return false;
}
if ($this->user->isLoggedIn()) {
return $this->createError("You already logged in.");
}
$settings = $this->user->getConfiguration()->getSettings();
if ($settings->isRecaptchaEnabled()) {
$captcha = $this->getParam("captcha");
$req = new VerifyCaptcha($this->user);
if (!$req->execute(array("captcha" => $captcha, "action" => "resendConfirmation"))) {
return $this->createError($req->getLastError());
}
}
$messageBody = $this->getMessageTemplate("message_confirm_email");
if ($messageBody === false) {
return false;
}
$email = $this->getParam("email");
$sql = $this->user->getSQL();
$res = $sql->insert("UserToken", array("user_id", "token", "token_type", "valid_until"))
->addRow($id, $token, "password_reset", $validUntil)
$res = $sql->select("User.uid", "User.name", "UserToken.token", "UserToken.token_type", "UserToken.used")
->from("User")
->leftJoin("UserToken", "User.uid", "UserToken.user_id")
->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
return true;
}
$userId = $res[0]["uid"];
$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";
}))
);
if (!$token) {
// no token generated yet, let's generate one
$token = generateRandomString(36);
if (!$this->insertToken($userId, $token, "email_confirm", 48)) {
return false;
}
}
$username = $res[0]["name"];
$baseUrl = htmlspecialchars($settings->getBaseUrl());
$siteName = htmlspecialchars($settings->getSiteName());
$replacements = array(
"link" => "$baseUrl/confirmEmail?token=$token",
"site_name" => $siteName,
"base_url" => $baseUrl,
"username" => htmlspecialchars($username)
);
foreach($replacements as $key => $value) {
$messageBody = str_replace("{{{$key}}}", $value, $messageBody);
}
$request = new \Api\Mail\Send($this->user);
$this->success = $request->execute(array(
"to" => $email,
"subject" => "[$siteName] E-Mail Confirmation",
"body" => $messageBody
));
$this->lastError = $request->getLastError();
return $this->success;
}
}
@@ -1138,4 +1248,52 @@ namespace Api\User {
}
}
}
class UpdateProfile extends UserAPI {
public function __construct(User $user, bool $externalCall = false) {
parent::__construct($user, $externalCall, array(
'username' => new StringType('username', 32, true, NULL),
'password' => new StringType('password', -1, true, NULL),
));
$this->loginRequired = true;
$this->csrfTokenRequired = true;
$this->forbidMethod("GET");
}
public function execute($values = array()): bool {
if (!parent::execute($values)) {
return false;
}
$newUsername = $this->getParam("username");
$newPassword = $this->getParam("password");
if ($newUsername === null && $newPassword === null) {
return $this->createError("You must either provide an updated username or password");
}
$sql = $this->user->getSQL();
$query = $sql->update("User")->where(new Compare("id", $this->user->getId()));
if ($newUsername !== null) {
if (!$this->checkUsernameRequirements($newUsername) || $this->userExists($newUsername)) {
return false;
} else {
$query->set("name", $newUsername);
}
}
if ($newPassword !== null) { // TODO: confirm password?
if (!$this->checkPasswordRequirements($newPassword, $newPassword)) {
return false;
} else {
$query->set("password", $this->hashPassword($newPassword));
}
}
$this->success = $query->execute();
$this->lastError = $sql->getLastError();
return $this->success;
}
}
}

View File

@@ -47,9 +47,9 @@ class VerifyCaptcha extends Request {
$this->success = false;
$this->lastError = "Could not verify captcha: No response from google received.";
if($response) {
if ($response) {
$this->success = $response["success"];
if(!$this->success) {
if (!$this->success) {
$this->lastError = "Could not verify captcha: " . implode(";", $response["error-codes"]);
} else {
$score = $response["score"];