diff --git a/.htaccess b/.htaccess index c7e4846..bd3f44c 100644 --- a/.htaccess +++ b/.htaccess @@ -3,6 +3,7 @@ Options -Indexes DirectorySlash Off +SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 RewriteEngine On RewriteRule ^api(/.*)?$ /index.php?api=$1 [L,QSA] diff --git a/cli.php b/cli.php index 1159071..e3878d1 100644 --- a/cli.php +++ b/cli.php @@ -1,5 +1,7 @@ user->getSQL(); $res = $sql->select($sql->count()) ->from("ApiKey") diff --git a/core/Api/PermissionAPI.class.php b/core/Api/PermissionAPI.class.php index dd91f75..0cdc1d4 100644 --- a/core/Api/PermissionAPI.class.php +++ b/core/Api/PermissionAPI.class.php @@ -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; } diff --git a/core/Api/Request.class.php b/core/Api/Request.class.php index 4e42e6c..41d2c36 100644 --- a/core/Api/Request.class.php +++ b/core/Api/Request.class.php @@ -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(); + } } \ No newline at end of file diff --git a/core/Api/RoutesAPI.class.php b/core/Api/RoutesAPI.class.php index da96438..8086ab2 100644 --- a/core/Api/RoutesAPI.class.php +++ b/core/Api/RoutesAPI.class.php @@ -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(); diff --git a/core/Api/UserAPI.class.php b/core/Api/UserAPI.class.php index 8083657..9dcdf5b 100644 --- a/core/Api/UserAPI.class.php +++ b/core/Api/UserAPI.class.php @@ -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; + } + } } \ No newline at end of file diff --git a/core/Api/VerifyCaptcha.class.php b/core/Api/VerifyCaptcha.class.php index 51ae427..07f2a44 100644 --- a/core/Api/VerifyCaptcha.class.php +++ b/core/Api/VerifyCaptcha.class.php @@ -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"]; diff --git a/core/Configuration/CreateDatabase.class.php b/core/Configuration/CreateDatabase.class.php index 4a9ea26..33cc5fb 100644 --- a/core/Configuration/CreateDatabase.class.php +++ b/core/Configuration/CreateDatabase.class.php @@ -131,10 +131,11 @@ class CreateDatabase extends DatabaseScript { $queries[] = $sql->insert("Route", array("request", "action", "target", "extra")) ->addRow("^/admin(/.*)?$", "dynamic", "\\Documents\\Admin", NULL) - ->addRow("^/register(/)?$", "dynamic", "\\Documents\\Account", "\\Views\\Account\\Register") - ->addRow("^/confirmEmail(/)?$", "dynamic", "\\Documents\\Account", "\\Views\\Account\\ConfirmEmail") - ->addRow("^/acceptInvite(/)?$", "dynamic", "\\Documents\\Account", "\\Views\\Account\\AcceptInvite") - ->addRow("^/resetPassword(/)?$", "dynamic", "\\Documents\\Account", "\\Views\\Account\\ResetPassword") + ->addRow("^/register/?$", "dynamic", "\\Documents\\Account", "\\Views\\Account\\Register") + ->addRow("^/confirmEmail/?$", "dynamic", "\\Documents\\Account", "\\Views\\Account\\ConfirmEmail") + ->addRow("^/acceptInvite/?$", "dynamic", "\\Documents\\Account", "\\Views\\Account\\AcceptInvite") + ->addRow("^/resetPassword/?$", "dynamic", "\\Documents\\Account", "\\Views\\Account\\ResetPassword") + ->addRow("^/resendConfirmEmail/?$", "dynamic", "\\Documents\\Account", "\\Views\\Account\\ResendConfirmEmail") ->addRow("^/$", "static", "/static/welcome.html", NULL); $queries[] = $sql->createTable("Settings") diff --git a/core/Documents/Account.class.php b/core/Documents/Account.class.php index 27c6843..b8dc6d6 100644 --- a/core/Documents/Account.class.php +++ b/core/Documents/Account.class.php @@ -17,6 +17,7 @@ namespace Documents { namespace Documents\Account { use Elements\Head; + use Elements\Link; use Elements\Script; use Elements\SimpleBody; @@ -32,6 +33,7 @@ namespace Documents\Account { $this->addJS(Script::ACCOUNT); $this->loadBootstrap(); $this->loadFontawesome(); + $this->addCSS(Link::CORE); } protected function initMetas(): array { diff --git a/core/Driver/SQL/Condition/Exists.class.php b/core/Driver/SQL/Condition/Exists.class.php new file mode 100644 index 0000000..ab45af9 --- /dev/null +++ b/core/Driver/SQL/Condition/Exists.class.php @@ -0,0 +1,22 @@ +subQuery = $subQuery; + } + + public function getSubQuery(): Select + { + return $this->subQuery; + } +} \ No newline at end of file diff --git a/core/Driver/SQL/PostgreSQL.class.php b/core/Driver/SQL/PostgreSQL.class.php index 7a8a40b..4fa67be 100644 --- a/core/Driver/SQL/PostgreSQL.class.php +++ b/core/Driver/SQL/PostgreSQL.class.php @@ -340,7 +340,7 @@ class PostgreSQL extends SQL { return ($statusTexts[$status] ?? "Unknown") . " (v$version)"; } - protected function buildCondition($condition, &$params) { + public function buildCondition($condition, &$params) { if($condition instanceof CondRegex) { $left = $condition->getLeftExp(); $right = $condition->getRightExp(); diff --git a/core/Driver/SQL/Query/AlterTable.class.php b/core/Driver/SQL/Query/AlterTable.class.php index 2af16a5..15e4841 100644 --- a/core/Driver/SQL/Query/AlterTable.class.php +++ b/core/Driver/SQL/Query/AlterTable.class.php @@ -54,6 +54,11 @@ class AlterTable extends Query { return $this; } + public function resetAutoIncrement(): AlterTable { + $this->action = "RESET_AUTO_INCREMENT"; + return $this; + } + public function getAction(): string { return $this->action; } public function getColumn(): ?Column { return $this->column; } public function getConstraint(): ?Constraint { return $this->constraint; } @@ -65,6 +70,10 @@ class AlterTable extends Query { $column = $this->getColumn(); $constraint = $this->getConstraint(); + if ($action === "RESET_AUTO_INCREMENT") { + return "ALTER TABLE $tableName AUTO_INCREMENT=1"; + } + $query = "ALTER TABLE $tableName $action "; if ($column) { diff --git a/core/Driver/SQL/Query/Select.class.php b/core/Driver/SQL/Query/Select.class.php index bf51178..d3e83e4 100644 --- a/core/Driver/SQL/Query/Select.class.php +++ b/core/Driver/SQL/Query/Select.class.php @@ -23,6 +23,7 @@ class Select extends Query { $this->selectValues = (!empty($selectValues) && is_array($selectValues[0])) ? $selectValues[0] : $selectValues; $this->tables = array(); $this->conditions = array(); + $this->havings = array(); $this->joins = array(); $this->orderColumns = array(); $this->groupColumns = array(); @@ -41,6 +42,11 @@ class Select extends Query { return $this; } + public function having(...$conditions): Select { + $this->havings[] = (count($conditions) === 1 ? $conditions : new CondOr($conditions)); + return $this; + } + public function innerJoin(string $table, string $columnA, string $columnB, ?string $tableAlias = null): Select { $this->joins[] = new Join("INNER", $table, $columnA, $columnB, $tableAlias); return $this; @@ -94,6 +100,7 @@ class Select extends Query { public function getLimit(): int { return $this->limit; } public function getOffset(): int { return $this->offset; } public function getGroupBy(): array { return $this->groupColumns; } + public function getHavings(): array { return $this->havings; } public function build(array &$params): ?string { @@ -101,6 +108,17 @@ class Select extends Query { foreach ($this->selectValues as $value) { if (is_string($value)) { $selectValues[] = $this->sql->columnName($value); + } else if ($value instanceof Select) { + $subSelect = $value->build($params); + if (count($value->getSelectValues()) !== 1) { + $selectValues[] = "($subSelect)"; + } else { + $columnName = $value->getSelectValues()[0]; + if(($index = stripos($columnName, " as ")) !== FALSE) { + $columnName = substr($columnName, $index + 4); + } + $selectValues[] = "($subSelect) as $columnName"; + } } else { $selectValues[] = $this->sql->addValue($value, $params); } @@ -115,6 +133,10 @@ class Select extends Query { $tables = $this->sql->tableName($tables); $where = $this->sql->getWhereClause($this->getConditions(), $params); + $havingClause = ""; + if (count($this->havings) > 0) { + $havingClause = " HAVING " . $this->sql->buildCondition($this->getHavings(), $params); + } $joinStr = ""; $joins = $this->getJoins(); @@ -145,6 +167,6 @@ class Select extends Query { $limit = ($this->getLimit() > 0 ? (" LIMIT " . $this->getLimit()) : ""); $offset = ($this->getOffset() > 0 ? (" OFFSET " . $this->getOffset()) : ""); - return "SELECT $selectValues FROM $tables$joinStr$where$groupBy$orderBy$limit$offset"; + return "SELECT $selectValues FROM $tables$joinStr$where$groupBy$havingClause$orderBy$limit$offset"; } } \ No newline at end of file diff --git a/core/Driver/SQL/SQL.class.php b/core/Driver/SQL/SQL.class.php index 21cdcf5..508a35e 100644 --- a/core/Driver/SQL/SQL.class.php +++ b/core/Driver/SQL/SQL.class.php @@ -4,6 +4,7 @@ namespace Driver\SQL; use Driver\SQL\Column\Column; use Driver\SQL\Condition\Compare; +use Driver\SQL\Condition\CondAnd; use Driver\SQL\Condition\CondBool; use Driver\SQL\Condition\CondIn; use Driver\SQL\Condition\Condition; @@ -11,6 +12,7 @@ use Driver\SQL\Condition\CondKeyword; use Driver\SQL\Condition\CondNot; use Driver\Sql\Condition\CondNull; use Driver\SQL\Condition\CondOr; +use Driver\SQL\Condition\Exists; use Driver\SQL\Constraint\Constraint; use \Driver\SQL\Constraint\Unique; use \Driver\SQL\Constraint\PrimaryKey; @@ -234,7 +236,7 @@ abstract class SQL { // Statements protected abstract function execute($query, $values=NULL, $returnValues=false); - protected function buildCondition($condition, &$params) { + public function buildCondition($condition, &$params) { if ($condition instanceof CondOr) { $conditions = array(); @@ -242,6 +244,12 @@ abstract class SQL { $conditions[] = $this->buildCondition($cond, $params); } return "(" . implode(" OR ", $conditions) . ")"; + } else if ($condition instanceof CondAnd) { + $conditions = array(); + foreach($condition->getConditions() as $cond) { + $conditions[] = $this->buildCondition($cond, $params); + } + return "(" . implode(" AND ", $conditions) . ")"; } else if ($condition instanceof Compare) { $column = $this->columnName($condition->getColumn()); $value = $condition->getValue(); @@ -302,8 +310,10 @@ abstract class SQL { } return "NOT $expression"; - } else if($condition instanceof CondNull) { - return $this->columnName($condition->getColumn()) . " IS NULL"; + } else if ($condition instanceof CondNull) { + return $this->columnName($condition->getColumn()) . " IS NULL"; + } else if ($condition instanceof Exists) { + return "EXISTS(" .$condition->getSubQuery()->build($params) . ")"; } else { $this->lastError = "Unsupported condition type: " . get_class($condition); return null; diff --git a/core/Elements/Script.class.php b/core/Elements/Script.class.php index 21dd4d4..74a6953 100644 --- a/core/Elements/Script.class.php +++ b/core/Elements/Script.class.php @@ -11,7 +11,8 @@ class Script extends StaticView { const INSTALL = "/js/install.js"; const BOOTSTRAP = "/js/bootstrap.bundle.min.js"; const ACCOUNT = "/js/account.js"; - const FILES = "/js/files.min.js"; + const SECLAB = "/js/seclab.min.js"; + const FONTAWESOME = "/js/fontawesome-all.min.js"; private string $type; private string $content; diff --git a/core/External/ZipStream/File.php b/core/External/ZipStream/File.php index 3aa22a6..117a73b 100644 --- a/core/External/ZipStream/File.php +++ b/core/External/ZipStream/File.php @@ -68,6 +68,13 @@ namespace External\ZipStream { $this->fileHandle = fopen($filename, 'rb'); } + public function loadFromBuffer($buf) { + $this->crc32 = hash('crc32b', $buf, true); + $this->sha256 = hash('sha256', $buf); + $this->fileSize = strlen($buf); + $this->content = $buf; + } + public function name() { return $this->name; } diff --git a/core/Objects/AesStream.class.php b/core/Objects/AesStream.class.php new file mode 100644 index 0000000..247b717 --- /dev/null +++ b/core/Objects/AesStream.class.php @@ -0,0 +1,126 @@ +key = $key; + $this->iv = $iv; + $this->inputFile = null; + $this->outputFile = null; + $this->callback = null; + + if (!in_array(strlen($key), [16, 24, 32])) { + throw new \Exception("Invalid Key Size"); + } else if (strlen($iv) !== 16) { + throw new \Exception("Invalid IV Size"); + } + } + + public function setInput($file) { + $this->inputFile = $file; + } + + public function setOutput($callback) { + $this->callback = $callback; + } + + public function setOutputFile(string $file) { + $this->outputFile = $file; + } + + private function add(string $a, int $b): string { + // counter $b is n = PHP_INT_SIZE bytes large + $b_arr = pack('I', $b); + $b_size = strlen($b_arr); + $a_size = strlen($a); + + $prefix = ""; + if ($a_size > $b_size) { + $prefix = substr($a, 0, $a_size - $b_size); + } + + // xor last n bytes of $a with $b + $xor = substr($a, strlen($prefix), $b_size); + if (strlen($xor) !== strlen($b_arr)) { + var_dump($xor); + var_dump($b_arr); + die(); + } + $xor = $this->xor($xor, $b_arr); + return $prefix . $xor; + } + + private function xor(string $a, string $b): string { + $arr_a = str_split($a); + $arr_b = str_split($b); + if (strlen($a) !== strlen($b)) { + var_dump($a); + var_dump($b); + var_dump(range(0, strlen($a) - 1)); + die(); + } + + return implode("", array_map(function($i) use ($arr_a, $arr_b) { + return chr(ord($arr_a[$i]) ^ ord($arr_b[$i])); + }, range(0, strlen($a) - 1))); + } + + public function start(): bool { + if (!$this->inputFile) { + return false; + } + + $blockSize = 16; + $bitStrength = strlen($this->key) * 8; + $aesMode = "AES-$bitStrength-ECB"; + + $outputHandle = null; + $inputHandle = fopen($this->inputFile, "rb"); + if (!$inputHandle) { + return false; + } + + if ($this->outputFile !== null) { + $outputHandle = fopen($this->outputFile, "wb"); + if (!$outputHandle) { + return false; + } + } + + $counter = 0; + while (!feof($inputHandle)) { + $chunk = fread($inputHandle, 4096); + $chunkSize = strlen($chunk); + for ($offset = 0; $offset < $chunkSize; $offset += $blockSize) { + $block = substr($chunk, $offset, $blockSize); + if (strlen($block) !== $blockSize) { + $padding = ($blockSize - strlen($block)); + $block .= str_repeat(chr($padding), $padding); + } + + $ivCounter = $this->add($this->iv, $counter + 1); + $encrypted = substr(openssl_encrypt($ivCounter, $aesMode, $this->key, OPENSSL_RAW_DATA), 0, $blockSize); + $encrypted = $this->xor($encrypted, $block); + if (is_callable($this->callback)) { + call_user_func($this->callback, $encrypted); + } + + if ($outputHandle !== null) { + fwrite($outputHandle, $encrypted); + } + } + } + + fclose($inputHandle); + if ($outputHandle) fclose($outputHandle); + return true; + } +} \ No newline at end of file diff --git a/core/Objects/Session.class.php b/core/Objects/Session.class.php index 7053787..eeaa193 100644 --- a/core/Objects/Session.class.php +++ b/core/Objects/Session.class.php @@ -66,7 +66,7 @@ class Session extends ApiObject { $token = array('userId' => $this->user->getId(), 'sessionId' => $this->sessionId); $sessionCookie = JWT::encode($token, $settings->getJwtSecret()); $secure = strcmp(getProtocol(), "https") === 0; - setcookie('session', $sessionCookie, $this->getExpiresTime(), "/", "", $secure); + setcookie('session', $sessionCookie, $this->getExpiresTime(), "/", "", $secure, true); } public function getExpiresTime(): int { diff --git a/core/Objects/User.class.php b/core/Objects/User.class.php index cd48215..19a7081 100644 --- a/core/Objects/User.class.php +++ b/core/Objects/User.class.php @@ -27,7 +27,7 @@ class User extends ApiObject { $this->connectDb(); if (!is_cli()) { - session_start(); + @session_start(); $this->setLanguage(Language::DEFAULT_LANGUAGE()); $this->parseCookies(); } @@ -227,9 +227,12 @@ class User extends ApiObject { } $res = $this->sql->select("ApiKey.user_id as uid", "User.name", "User.email", "User.confirmed", - "Language.uid as langId", "Language.code as langCode", "Language.name as langName") + "Language.uid as langId", "Language.code as langCode", "Language.name as langName", + "Group.uid as groupId", "Group.name as groupName") ->from("ApiKey") ->innerJoin("User", "ApiKey.user_id", "User.uid") + ->leftJoin("UserGroup", "UserGroup.user_id", "User.uid") + ->leftJoin("Group", "UserGroup.group_id", "Group.uid") ->leftJoin("Language", "User.language_id", "Language.uid") ->where(new Compare("ApiKey.api_key", $apiKey)) ->where(new Compare("valid_until", $this->sql->currentTimestamp(), ">")) @@ -253,6 +256,10 @@ class User extends ApiObject { if(!is_null($row['langId'])) { $this->setLanguage(Language::newInstance($row['langId'], $row['langCode'], $row['langName'])); } + + foreach($res as $row) { + $this->groups[$row["groupId"]] = $row["groupName"]; + } } } diff --git a/core/Views/Account/AcceptInvite.class.php b/core/Views/Account/AcceptInvite.class.php index d1c7c4b..8dc47c9 100644 --- a/core/Views/Account/AcceptInvite.class.php +++ b/core/Views/Account/AcceptInvite.class.php @@ -73,13 +73,13 @@ class AcceptInvite extends AccountView {
$this->description
Enter your E-Mail address, to receive a new e-mail to confirm your registration.
+