|
@@ -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;
|
|
|
}
|
|
|
+ }
|
|
|
+
|
|
|
+ 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;
|
|
|
+ }
|
|
|
|
|
|
- private function insertToken(int $id, string $token) {
|
|
|
- $validUntil = (new DateTime())->modify("+1 hour");
|
|
|
+ $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;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|