From 6a62ae58fc7e339d09ce4bd8b7cc4f6be302a405 Mon Sep 17 00:00:00 2001 From: Kilian Hofmann Date: Mon, 29 Jul 2024 22:06:57 +0200 Subject: [PATCH] Docs --- exam/api/Login/Login.php | 15 +++ exam/api/Logout/Logout.php | 12 +++ exam/api/Posts/Posts.php | 43 +++++++- exam/api/Refresh/Refresh.php | 14 +++ exam/api/Register/Register.php | 25 +++++ exam/api/Users/Image/Image.php | 25 +++++ exam/api/Users/Posts/Posts.php | 12 +++ exam/api/Users/Users.php | 58 +++++++++- exam/classes/Api/Api.php | 5 + exam/classes/ApiError/ApiError.php | 39 +++++++ exam/classes/Auth/AdminAuth.php | 14 +++ exam/classes/Auth/Auth.php | 13 +++ exam/classes/Auth/OptAuth.php | 15 ++- exam/classes/Config/Config.php | 42 ++++++++ exam/classes/Database/Database.php | 18 ++++ exam/classes/GUID/GUID.php | 13 +++ exam/classes/Input/Input.php | 30 ++++++ exam/classes/Models/Post/Post.php | 65 ++++++++++- exam/classes/Models/User/User.php | 166 ++++++++++++++++++++++++++++- exam/classes/Request/Request.php | 19 ++++ exam/classes/Response/Response.php | 28 +++++ exam/utils/helpers.php | 6 ++ 22 files changed, 670 insertions(+), 7 deletions(-) diff --git a/exam/api/Login/Login.php b/exam/api/Login/Login.php index ed85fcd..286534a 100644 --- a/exam/api/Login/Login.php +++ b/exam/api/Login/Login.php @@ -9,8 +9,23 @@ use Khofmann\Input\Input; use Khofmann\Response\Response; use Khofmann\Models\User\User; +/** + * Login route handlers + */ class Login extends Api { + /** + * Login POST handler + * + * Log in a user. Required inputs are `email` and `password`. + * + * Returns user and tokens + * + * @throws 400 Missing field + * @throws 401 Invalid credentials (login fails) + * @throws 404 User not found + * @throws 500 Failed to log in user + */ public function post(): void { // Fetch all required inputs. diff --git a/exam/api/Logout/Logout.php b/exam/api/Logout/Logout.php index 6b0c459..79ecef2 100644 --- a/exam/api/Logout/Logout.php +++ b/exam/api/Logout/Logout.php @@ -7,8 +7,20 @@ use Khofmann\Models\User\User; use Khofmann\Request\Request; use Khofmann\Response\Response; +/** + * Logout route handlers + */ class Logout extends Api { + /** + * Logout POST handler + * + * Logout a user. User is retrieved using the authentication `token`. + * + * Returns user. + * + * @throws 404 User not found + */ public function post(): void { // Get user auth token. diff --git a/exam/api/Posts/Posts.php b/exam/api/Posts/Posts.php index 501eae3..3018df2 100644 --- a/exam/api/Posts/Posts.php +++ b/exam/api/Posts/Posts.php @@ -11,8 +11,16 @@ use Khofmann\Models\User\User; use Khofmann\Request\Request; use Khofmann\Response\Response; +/** + * Posts route handlers + */ class Posts extends Api { + /** + * Posts GET handler + * + * Lists posts. Optional parameters are `l` (limit of returned list) and `p` (page, i.e. offset). + */ public function get() { // Fetch and constrain all parameters. @@ -24,6 +32,15 @@ class Posts extends Api Response::json(Post::list($page, $limit, $authed)); } + /** + * Posts POST handler + * + * Create a new posts. Required inputs are `content`. Optional parameter is `l` (limit of list for which the returned pages is calculated). + * + * Returns created post and resulting amount of pages for a given limit. + * + * @throws 400 Missing fields + */ public function post(): void { // Fetch all required inputs. @@ -47,6 +64,19 @@ class Posts extends Api } } + /** + * Posts PATCH handler + * + * Update a posts. + * + * Returns updated post. + * + * @param mixed $id ID of post to update + * + * @throws 401 Not authorized (trying to edit a different users post if not admin) + * @throws 404 Post not found + * @throws 500 Failed to update user + */ public function patch($id): void { // Fetch all inputs. @@ -75,9 +105,20 @@ class Posts extends Api } } + /** + * Posts DELETE handler + * + * Delete a post. Optional parameter is `l` (limit of list for which the returned pages is calculated). + * + * Returns deleted post and resulting amount of pages for a given limit. + * + * @param mixed $id ID of posts to delete + * + * @throws 404 Post not found + */ public function delete($id): void { - // Fetch ax(0, intval(Input::get("p", 0))); + // Fetch and constrain all parameters. $limit = constrain(0, 30, intval(Input::get("l", 10))); // Try delete, 404 if post was not found. try { diff --git a/exam/api/Refresh/Refresh.php b/exam/api/Refresh/Refresh.php index 1c25796..5e82b17 100644 --- a/exam/api/Refresh/Refresh.php +++ b/exam/api/Refresh/Refresh.php @@ -10,8 +10,22 @@ use Khofmann\Response\Response; use Khofmann\Models\User\User; use Khofmann\Request\Request; +/** + * Refresh route handlers + */ class Refresh extends Api { + /** + * Refresh POST handler + * + * Refresh a users session. User is retrieved using the authentication `token`. + * + * Returns user and tokens. + * + * @throws 401 Missing field + * @throws 404 User not found + * @throws 500 Failed to refresh tokens + */ public function post(): void { // Fetch all required inputs. diff --git a/exam/api/Register/Register.php b/exam/api/Register/Register.php index b57e544..469bd14 100644 --- a/exam/api/Register/Register.php +++ b/exam/api/Register/Register.php @@ -9,8 +9,22 @@ use Khofmann\Input\Input; use Khofmann\Response\Response; use Khofmann\Models\User\User; +/** + * Register route handlers + */ class Register extends Api { + /** + * Register POST handler + * + * Register a new user. Required inputs are `username`, `email`, and `password`. + * + * Returns user. + * + * @throws 400 Missing fields + * @throws 400 Duplicate + * @throws 404 Failure to create + */ public function post(): void { // Fetch all required inputs. @@ -39,6 +53,17 @@ class Register extends Api } } + /** + * Register PATCH handler + * + * Confirms a user. Required input is `code`. + * + * Returns user. + * + * @throws 400 Missing field + * @throws 404 User not found + * @throws 404 User already confirmed + */ public function patch(): void { // Fetch all required inputs. diff --git a/exam/api/Users/Image/Image.php b/exam/api/Users/Image/Image.php index de7bd6f..08365a0 100644 --- a/exam/api/Users/Image/Image.php +++ b/exam/api/Users/Image/Image.php @@ -10,8 +10,23 @@ use Khofmann\Response\Response; use Khofmann\ApiError\ApiError; use Khofmann\Request\Request; +/** + * User image route handlers + */ class Image extends Api { + /** + * Image POST handler + * + * Set a new user image. + * + * Returns updated user. + * + * @param mixed $id User ID + * + * @throws 404 User not found + * @throws 500 Failed to update user image + */ public function post($id): void { // Fetch all inputs. @@ -33,6 +48,16 @@ class Image extends Api } } + /** + * Image POST handler + * + * Set a new user image. User is retrieved using the authentication `token`. + * + * Returns updated user. + * + * @throws 404 User not found + * @throws 500 Failed to update user image + */ public function postSelf(): void { // Fetch all inputs. diff --git a/exam/api/Users/Posts/Posts.php b/exam/api/Users/Posts/Posts.php index 155c0de..601cce2 100644 --- a/exam/api/Users/Posts/Posts.php +++ b/exam/api/Users/Posts/Posts.php @@ -9,8 +9,20 @@ use Khofmann\Response\Response; use Khofmann\ApiError\ApiError; use Khofmann\Input\Input; +/** + * User posts route handlers + */ class Posts extends Api { + /** + * Posts GET handler + * + * Lists posts for a user. Optional parameters are `l` (limit of returned list), `p` (page, i.e. offset), `s` (sort order). + * + * Returns list of posts. + * + * @throws 404 User not found + */ public function get($id): void { // Fetch and constrain all parameters. diff --git a/exam/api/Users/Users.php b/exam/api/Users/Users.php index de61baa..55afc34 100644 --- a/exam/api/Users/Users.php +++ b/exam/api/Users/Users.php @@ -10,8 +10,18 @@ use Khofmann\Response\Response; use Khofmann\ApiError\ApiError; use Khofmann\Request\Request; +/** + * Users route handlers + */ class Users extends Api { + /** + * Users GET handler + * + * Lists users. Optional parameters are `l` (limit of returned list) and `p` (page, i.e. offset). + * + * Returns list of users. + */ public function list() { // Fetch and constrain all parameters. @@ -22,6 +32,17 @@ class Users extends Api Response::json(User::list($page, $limit)); } + /** + * User GET handler + * + * Get a single user. + * + * Returns user. + * + * @param mixed $id User ID + * + * @throws 404 User not found + */ public function get($id): void { // Try and get a user, 404 if not found. @@ -37,6 +58,18 @@ class Users extends Api } } + /** + * Users PATCH handler + * + * Update a user. + * + * Returns updated user. + * + * @param mixed $id User ID + * + * @throws 404 User not found + * @throws 500 Failed to update user + */ public function patch($id): void { // Fetch all inputs. @@ -59,6 +92,16 @@ class Users extends Api } } + /** + * Users PATCH handler + * + * Update a user. User is retrieved using the authentication `token`. + * + * Returns updated user. + * + * @throws 404 User not found + * @throws 500 Failed to update user + */ public function patchSelf(): void { // Fetch all inputs. @@ -82,11 +125,24 @@ class Users extends Api } } + /** + * Users DELETE handler + * + * Deletes a user. Optional parameter is `l` (limit of list for which the returned pages is calculated). + * + * Returns deleted user and resulting amount of pages for a given limit. + * + * @param mixed $id User ID + * + * @throws 404 User not found + */ public function delete($id): void { + // Fetch and constrain all parameters. + $limit = constrain(0, 30, intval(Input::get("l", 10))); // Try to delete user, 404 if not found. try { - Response::json(User::getByID($id)->delete()); + Response::json(User::getByID($id)->delete($limit)); } catch (Exception $err) { switch ($err->getMessage()) { case "NotFound": diff --git a/exam/classes/Api/Api.php b/exam/classes/Api/Api.php index 52ca669..26772ef 100644 --- a/exam/classes/Api/Api.php +++ b/exam/classes/Api/Api.php @@ -4,6 +4,11 @@ namespace Khofmann\Api; use Khofmann\Response\Response; +/** + * Base class for all API handler classes. + * + * Sets common headers + */ class Api { public function __construct() diff --git a/exam/classes/ApiError/ApiError.php b/exam/classes/ApiError/ApiError.php index 8efd913..c1060f0 100644 --- a/exam/classes/ApiError/ApiError.php +++ b/exam/classes/ApiError/ApiError.php @@ -4,6 +4,9 @@ namespace Khofmann\ApiError; use Exception; +/** + * Facade for common API errors + */ class ApiError extends Exception { private function __construct($message = "", $code = 0) @@ -11,6 +14,11 @@ class ApiError extends Exception parent::__construct($message, $code); } + /** + * Error for missing fields + * + * @param array $fields Array of strings denoting which fields were missing + */ public static function missingField(array $fields): ApiError { return new ApiError(json_encode([ @@ -19,6 +27,11 @@ class ApiError extends Exception ]), 400); } + /** + * Error for duplicates + * + * @param string entity Entity for which a duplicate exists + */ public static function duplicate(string $entity): ApiError { return new ApiError(json_encode([ @@ -27,6 +40,11 @@ class ApiError extends Exception ]), 400); } + /** + * Error for missing permissions + * + * @param string message Message specifics + */ public static function notAllowed(string $message) { return new ApiError(json_encode([ @@ -35,6 +53,11 @@ class ApiError extends Exception ]), 401); } + /** + * Error for missing authentication + * + * @param string $message Message specifics + */ public static function unauthorized(string $message) { return new ApiError(json_encode([ @@ -43,6 +66,11 @@ class ApiError extends Exception ]), 401); } + /** + * Error for not found + * + * @param string entity Entity for which a duplicate exists + */ public static function notFound(string $entity) { return new ApiError(json_encode([ @@ -51,6 +79,11 @@ class ApiError extends Exception ]), 404); } + /** + * Generic error + * + * @param string message Message specifics + */ public static function failed(string $message) { return new ApiError(json_encode([ @@ -59,6 +92,12 @@ class ApiError extends Exception ]), 500); } + /** + * Error for missing fields + * + * @param array $fields Array of strings denoting which fields failed + * @param array $fields Array of strings denoting why the fields failed + */ public static function failedUpdate(array $fields, array $reasons) { return new ApiError(json_encode([ diff --git a/exam/classes/Auth/AdminAuth.php b/exam/classes/Auth/AdminAuth.php index 9d5ac90..16a5902 100644 --- a/exam/classes/Auth/AdminAuth.php +++ b/exam/classes/Auth/AdminAuth.php @@ -8,8 +8,20 @@ use Pecee\Http\Request; use Khofmann\Models\User\User; use Khofmann\Response\Response; +/** + * Middleware for admin authenticated routes + */ class AdminAuth implements IMiddleware { + /** + * Request handler + * + * Returns 401 if `token`is missing, no user is found with the `token`or user is not admin. + * + * Keeps session fresh if request is authenticated. + * + * @param Request $request Incoming request + */ public function handle(Request $request): void { $token = $request->getHeader("token"); @@ -26,7 +38,9 @@ class AdminAuth implements IMiddleware } try { + // Get user $user = User::getByToken($token); + // Check if user is admin if (!$user->getIsAdmin()) { Response::response() ->header("Cache-control: no-cache") diff --git a/exam/classes/Auth/Auth.php b/exam/classes/Auth/Auth.php index 51586c8..8d97fdd 100644 --- a/exam/classes/Auth/Auth.php +++ b/exam/classes/Auth/Auth.php @@ -8,8 +8,20 @@ use Pecee\Http\Request; use Khofmann\Models\User\User; use Khofmann\Response\Response; +/** + * Middleware for authenticated routes + */ class Auth implements IMiddleware { + /** + * Request handler + * + * Returns 401 if `token`is missing, or no user is found with the `token`. + * + * Keeps session fresh if request is authenticated. + * + * @param Request $request Incoming request + */ public function handle(Request $request): void { $token = $request->getHeader("token"); @@ -26,6 +38,7 @@ class Auth implements IMiddleware } try { + // Get user $user = User::getByToken($token); // Keep fresh diff --git a/exam/classes/Auth/OptAuth.php b/exam/classes/Auth/OptAuth.php index c5c21dd..eeb9711 100644 --- a/exam/classes/Auth/OptAuth.php +++ b/exam/classes/Auth/OptAuth.php @@ -8,18 +8,31 @@ use Pecee\Http\Request; use Khofmann\Models\User\User; use Khofmann\Response\Response; +/** + * Middleware for optional authenticated routes + */ class OptAuth implements IMiddleware { + /** + * Request handler + * + * Returns 401 if no user is found with the `token`. + * + * Keeps session fresh if request is authenticated. + * + * @param Request $request Incoming request + */ public function handle(Request $request): void { $token = $request->getHeader("token"); - // No token + // No token, since authentication is optional, pass if ($token === null) { return; } try { + // Get user $user = User::getByToken($token); // Keep fresh diff --git a/exam/classes/Config/Config.php b/exam/classes/Config/Config.php index 9a436ed..45154b6 100644 --- a/exam/classes/Config/Config.php +++ b/exam/classes/Config/Config.php @@ -4,29 +4,50 @@ namespace Khofmann\Config; use Exception; +/** + * Facade for application configuration + */ class Config { + // Instances array to ensure singleton pattern private static array $instances = []; + // Configuration arrays private array $app; private array $database; + /** + * Loads configurations into arrays. + * + * Private since singleton pattern. + */ private function __construct() { $this->app = require_once __DIR__ . "/../../config/app.php"; $this->database = require_once __DIR__ . "/../../config/database.php"; } + /** + * Disallow clone + */ private function __clone() { throw new Exception("Cannot clone a singleton."); } + /** + * Disallow wakeup + */ private function __wakeup() { throw new Exception("Cannot unserialize a singleton."); } + /** + * Get the instance. Ensures singleton pattern. + * + * Private to ensure only facade methods can be used. + */ private static function getInstance(): Config { $cls = static::class; @@ -37,36 +58,57 @@ class Config return self::$instances[$cls]; } + /** + * @return string Application base URI path + */ public static function getBasePath(): string { return Config::getInstance()->app["basePath"]; } + /** + * @return string Application base file system path + */ public static function getBaseFSPath(): string { return Config::getInstance()->app["baseFSPath"]; } + /** + * @return string Application storage URI path + */ public static function getStoragePath(): string { return Config::getInstance()->app["storagePath"]; } + /** + * @return string Application storage file system path + */ public static function getStorageFSPath(): string { return Config::getInstance()->app["storageFSPath"]; } + /** + * @return string Application base URI path + */ public static function getDatabase(): array { return Config::getInstance()->database; } + /** + * @return string Application base URI path + */ public static function getTokenExpiry(): string { return Config::getInstance()->app["tokenExpiry"]; } + /** + * @return string Application base URI path + */ public static function getRefreshTokenExpiry(): string { return Config::getInstance()->app["refreshTokenExpiry"]; diff --git a/exam/classes/Database/Database.php b/exam/classes/Database/Database.php index 31b88c8..adc4bb3 100644 --- a/exam/classes/Database/Database.php +++ b/exam/classes/Database/Database.php @@ -6,25 +6,43 @@ use PDO; use Khofmann\Config\Config; use Exception; +/** + * Facade for database access + */ class Database extends PDO { + // Instances array to ensure singleton pattern private static array $instances = []; + /** + * Private since singleton pattern. + */ private function __construct(string $dsn, string $username = null, string $password = null, array $options = null) { parent::__construct($dsn, $username, $password, $options); } + /** + * Disallow clone + */ private function __clone() { throw new Exception("Cannot clone a singleton."); } + /** + * Disallow wakeup + */ private function __wakeup() { throw new Exception("Cannot unserialize a singleton."); } + /** + * Get the instance. Ensures singleton pattern. + * + * Loads configuration from `Config` facade + */ public static function getInstance(): Database { $cls = static::class; diff --git a/exam/classes/GUID/GUID.php b/exam/classes/GUID/GUID.php index 8d42287..1e27984 100644 --- a/exam/classes/GUID/GUID.php +++ b/exam/classes/GUID/GUID.php @@ -2,12 +2,25 @@ namespace Khofmann\GUID; +/** + * Facade for GUID generators + */ class GUID { + /** + * Private since facade. + */ private function __construct() { } + /** + * Generate a UUID v4. + * + * @param mixed $data Optional data + * + * @return string UUID v4 + */ public static function v4($data = null): string { // Generate 16 bytes (128 bits) of random data or use the data passed into the function. diff --git a/exam/classes/Input/Input.php b/exam/classes/Input/Input.php index 3ac34ab..5bdf0aa 100644 --- a/exam/classes/Input/Input.php +++ b/exam/classes/Input/Input.php @@ -4,30 +4,60 @@ namespace Khofmann\Input; use Khofmann\Request\Request; +/** + * Facade for Input (wraps SimpleRouter) + */ class Input { + /** + * Private since facade. + */ private function __construct() { } + /** + * Get POST parameter. + * + * @param string $index Parameter name + * @param mixed $defaultValue Default value if parameter was null + */ public static function post(string $index, $defaultValue = null) { $value = Request::request()->getInputHandler()->post($index, $defaultValue); return !is_object($value) ? $value : $value->getValue(); } + /** + * Get PATCH parameter. + * + * @param string $index Parameter name + * @param mixed $defaultValue Default value if parameter was null + */ public static function patch(string $index, $defaultValue = null) { $value = Request::request()->getInputHandler()->post($index, $defaultValue); return !is_object($value) ? $value : $value->getValue(); } + /** + * Get query string parameter. + * + * @param string $index Parameter name + * @param mixed $defaultValue Default value if parameter was null + */ public static function get(string $index, $defaultValue = null) { $value = Request::request()->getInputHandler()->get($index, $defaultValue); return !is_object($value) ? $value : $value->getValue(); } + /** + * Get FILE entry. + * + * @param string $index File name + * @param mixed $defaultValue Default value if parameter was null + */ public static function file(string $index, $defaultValue = null) { $value = Request::request()->getInputHandler()->file($index, $defaultValue); diff --git a/exam/classes/Models/Post/Post.php b/exam/classes/Models/Post/Post.php index 728f71e..8825e1f 100644 --- a/exam/classes/Models/Post/Post.php +++ b/exam/classes/Models/Post/Post.php @@ -2,7 +2,6 @@ namespace Khofmann\Models\Post; -use Api\User\User as UserUser; use DateTime; use Exception; use Khofmann\Models\User\User; @@ -12,6 +11,11 @@ use Khofmann\Config\Config; use Khofmann\Database\Database; use PDO; +/** + * Post database model + * + * Abstracts database access + */ class Post implements JsonSerializable { private int $id; @@ -37,6 +41,16 @@ class Post implements JsonSerializable * Statics */ + /** + * Get a post by ID. + * + * Also get creator (`User` object). + * + * @param int $id Post ID + * + * @throws NotFound Post not found + * @throws NotFound Creator of post was not found + */ public static function getByID(int $id): Post { $db = Database::getInstance(); @@ -59,6 +73,15 @@ class Post implements JsonSerializable return new Post($data["id"], $user, null, null, $data["beitrag"], $data["zeitstempel"]); } + /** + * Create a new post. + * + * @param User $user Creator of post + * @param string $content Post content + * @param int $limit Limit for posts list to calculate number of pages + * + * @return array Number of pages after creation (in accordance with `limit`) and created post + */ public static function create(User $user, string $content, int $limit): array { $content = substr(trim($content), 0, 250); @@ -91,6 +114,15 @@ class Post implements JsonSerializable ]; } + /** + * List of posts + * + * @param int $page Current page (offset) + * @param int $limit Posts per page + * @param bool $authed If `true`, include full `User` object. Defaults to `false` + * + * @return array Number of pages and posts of selected page + */ public static function list(int $page, int $limit, bool $authed = false): array { $db = Database::getInstance(); @@ -128,10 +160,20 @@ class Post implements JsonSerializable * Members */ + /** + * Update post + * + * Does nothing if new `content` is empty + * + * @param ?string $content New content + * + * @throws Failed Failed to update content + */ public function update(?string $content): Post { $db = Database::getInstance(); + // Make sure we do all changes or none $db->beginTransaction(); $failed = []; @@ -156,16 +198,25 @@ class Post implements JsonSerializable } } if (count($failed) > 0) { + // We failed, go back $db->rollBack(); throw ApiError::failedUpdate($failed, $reason); } + // Commit the changes $db->commit(); return Post::getByID($this->id); } + /** + * Delete post + * + * @param int $limit Limit of list for which the returned pages is calculated. + * + * @return array Returns deleted post and resulting amount of pages for a given limit. + */ public function delete(int $limit): array { $db = Database::getInstance(); @@ -189,21 +240,33 @@ class Post implements JsonSerializable * Getters */ + /** + * Get post ID + */ public function getId(): int { return $this->id; } + /** + * Get post creator + */ public function getUser(): User { return $this->user; } + /** + * Get post content + */ public function getContent(): string { return $this->content; } + /** + * Get time when post was created + */ public function getPostedAt(): DateTime { return $this->postedAt; diff --git a/exam/classes/Models/User/User.php b/exam/classes/Models/User/User.php index 6a97044..53f126c 100644 --- a/exam/classes/Models/User/User.php +++ b/exam/classes/Models/User/User.php @@ -11,8 +11,12 @@ use JsonSerializable; use Khofmann\ApiError\ApiError; use Khofmann\GUID\GUID; use Khofmann\Models\Post\Post; -use PDOException; +/** + * User database model + * + * Abstracts database access + */ class User implements JsonSerializable { private int $id; @@ -36,6 +40,13 @@ class User implements JsonSerializable $this->postCount = $postCount; } + /** + * Get a user by their confirmation code. + * + * @param string $code Confirmation code + * + * @throws NotFound User not found + */ private static function getByConfirmCode(string $code): User { $db = Database::getInstance(); @@ -62,6 +73,13 @@ class User implements JsonSerializable * Statics */ + /** + * Get a user by ID. + * + * @param int $id User ID + * + * @throws NotFound User not found + */ public static function getByID(int $id): User { $db = Database::getInstance(); @@ -83,6 +101,13 @@ class User implements JsonSerializable return new User($id, $data["benutzer"], $data["status"], $data["email"], $data["zeitstempel"], $data["image"], $data["isadmin"] === 1, $data["postCount"]); } + /** + * Get a user by their email. + * + * @param string $email User email + * + * @throws NotFound User not found + */ public static function getByEmail(string $email): User { $db = Database::getInstance(); @@ -104,6 +129,13 @@ class User implements JsonSerializable return new User($data["id"], $data["benutzer"], $data["status"], $email, $data["zeitstempel"], $data["image"], $data["isadmin"] === 1, $data["postCount"]); } + /** + * Get a user by their authentication token. + * + * @param string $token Authentication token + * + * @throws NotFound User not found + */ public static function getByToken(string $token): User { $db = Database::getInstance(); @@ -126,6 +158,18 @@ class User implements JsonSerializable return new User($data["id"], $data["benutzer"], $data["status"], $data["email"], $data["zeitstempel"], $data["image"], $data["isadmin"] === 1, $data["postCount"]); } + /** + * Log in a user + * + * @param string $email User email + * @param string $password USer password + * + * @return array Tokens and user + * + * @throws Failed Could not generate tokens + * @throws Invalid Password was wrong + * @throws NotFound User not found (email wrong) + */ public static function logIn(string $email, string $password): array { $db = Database::getInstance(); @@ -191,6 +235,15 @@ class User implements JsonSerializable } } + /** + * Create a user + * + * @param string $username Username + * @param string $email User email + * @param string $password User password + * + * @throws Duplicate User with this username or email already exists + */ public static function create(string $username, string $email, string $password): User { $db = Database::getInstance(); @@ -225,6 +278,13 @@ class User implements JsonSerializable } } + /** + * Confirm a user + * + * @param string $confirmCode Confirmation code + * + * @throws NotFound User not found (invalid code or already confirmed) + */ public static function confirm(string $confirmCode): User { $db = Database::getInstance(); @@ -243,6 +303,14 @@ class User implements JsonSerializable return User::getByID($user->getID()); } + /** + * List of users + * + * @param int $page Current page (offset) + * @param int $limit Users per page + * + * @return array Number of pages and posts of selected page + */ public static function list(int $page, int $limit) { $db = Database::getInstance(); @@ -274,6 +342,15 @@ class User implements JsonSerializable return ["pages" => intdiv($count, $limit + 1), "data" => $list]; } + /** + * Refresh a user session + * + * @param string $token Authentication token + * @param string $refreshToken Refresh token + * + * @throws NotFound User not found (tokens do not exist, refresh token expired) + * @throws Failed Could not regenerate tokens + */ public static function refresh(string $token, string $refreshToken) { $db = Database::getInstance(); @@ -347,10 +424,22 @@ class User implements JsonSerializable return $stmt->execute(); } + /** + * Update post + * + * Does nothing if new all fields are empty + * + * @param ?string $username New username + * @param ?string $password New password + * @param ?string $email New email + * + * @throws Failed At least one field failed to update + */ public function update(?string $username, ?string $password, ?string $email): User { $db = Database::getInstance(); + // Make sure we do all changes or none $db->beginTransaction(); $failed = []; @@ -413,20 +502,33 @@ class User implements JsonSerializable } if (count($failed) > 0) { + // We failed, go back $db->rollBack(); throw ApiError::failedUpdate($failed, $reasons); } + // Commit the changes $db->commit(); return User::getByID($this->id); } + /** + * Update post + * + * Does nothing if all fields are empty + * + * @param mixed $image New file upload + * @param ?string $predefined Predefined avatar + * + * @param Failed Image failed to update + */ public function updateImage($image, ?string $predefined): User { $db = Database::getInstance(); + // Make sure we do all changes or none $db->beginTransaction(); $failed = []; @@ -442,6 +544,7 @@ class User implements JsonSerializable } if (!empty($image)) { + // Move file and grab filename $destinationFilename = sprintf('%s.%s', uniqid(), $image->getExtension()); $image->move(Config::getStorageFSPath() . "profilbilder/$destinationFilename"); @@ -482,25 +585,52 @@ class User implements JsonSerializable } if (count($failed) > 0) { + // We failed, go back $db->rollBack(); throw ApiError::failedUpdate($failed, $reasons); } + // Commit the changes $db->commit(); return User::getByID($this->id); } - public function delete(): User + /** + * Delete user + * + * @param int $limit Limit of list for which the returned pages is calculated. + * + * @return array Returns deleted user and resulting amount of pages for a given limit. + */ + public function delete(int $limit): array { $db = Database::getInstance(); $stmt = $db->prepare("DELETE FROM egb_benutzer WHERE id = :ID"); $stmt->bindValue(":ID", $this->id); - return $this; + $stmt = $db->prepare( + "SELECT + COUNT(*) + FROM + egb_benutzer" + ); + $stmt->execute(); + $count = $stmt->fetch(PDO::FETCH_COLUMN, 0); + + return ["pages" => intdiv($count, $limit + 1) + 1, "data" => $this]; } + /** + * List of posts by user + * + * @param int $page Current page (offset) + * @param int $limit Users per page + * @param string $sort Sort direction + * + * @return array Number of pages and posts of selected page + */ public function posts(int $page, int $limit, string $sort) { $db = Database::getInstance(); @@ -543,6 +673,9 @@ class User implements JsonSerializable return ["pages" => intdiv($count, $limit + 1) + 1, "data" => $list]; } + /** + * Refresh user token expiry + */ public function keepFresh() { try { @@ -565,41 +698,68 @@ class User implements JsonSerializable * Getters */ + /** + * Get user ID + */ public function getID(): int { return $this->id; } + /** + * Get username + */ public function getUsername(): string { return $this->username; } + /** + * Get user status + * + * * 0: Not confirmed + * * 1: Confirmed + */ public function getStatus(): int { return $this->status; } + /** + * Get user email + */ public function getEmail(): string { return $this->email; } + /** + * Get user image path + */ public function getImage(): ?string { return $this->image; } + /** + * Get user admin status + */ public function getIsAdmin(): bool { return $this->isAdmin; } + /** + * Get time of user creation + */ public function getMemberSince(): DateTime { return $this->memberSince; } + /** + * Get count of posts by user + */ public function getPostCount(): int { return $this->postCount; diff --git a/exam/classes/Request/Request.php b/exam/classes/Request/Request.php index 78ccc0e..9b1ca66 100644 --- a/exam/classes/Request/Request.php +++ b/exam/classes/Request/Request.php @@ -5,22 +5,41 @@ namespace Khofmann\Request; use Pecee\Http\Request as PRequest; use Pecee\SimpleRouter\SimpleRouter; +/** + * Facade for Request access (wraps SimpleRouter) + */ class Request { + /** + * Private since facade. + */ private function __construct() { } + /** + * Get current request + */ public static function request(): PRequest { return SimpleRouter::request(); } + /** + * Get header value from current request + * + * @param string $name Field name + * @param mixed $defaultValue Default value if header field was null + * @param bool $tryParse When enabled the method will try to find the header from both from client (http) and server-side variants, if the header is not found. + */ public static function header(string $name, $defaultValue = null, bool $tryParse = true): ?string { return Request::request()->getHeader($name, $defaultValue, $tryParse); } + /** + * Get authentication header field + */ public static function token() { return Request::header("token"); diff --git a/exam/classes/Response/Response.php b/exam/classes/Response/Response.php index ec26bc8..4b714ee 100644 --- a/exam/classes/Response/Response.php +++ b/exam/classes/Response/Response.php @@ -5,17 +5,33 @@ namespace Khofmann\Response; use Pecee\SimpleRouter\SimpleRouter; use Pecee\Http\Response as PResponse; +/** + * Facade for Response creation (wraps SimpleRouter) + */ class Response { + /** + * Private since facade. + */ private function __construct() { } + /** + * Get current response + */ public static function response(): PResponse { return SimpleRouter::response(); } + /** + * Create JSON response + * + * @param mixed $value Body of response + * @param int $options Options passed to `json_encode` + * @param int $dept Depth passed to `json_encode` + */ public static function json($value, int $options = 0, int $dept = 512): void { if (is_bool($value)) { @@ -26,6 +42,12 @@ class Response SimpleRouter::response()->json($value, $options, $dept); } + /** + * Create API error response + * + * @param string $value Body of error + * @param int $code HTTP code of error + */ public static function apiError(string $value, int $code): void { Response::response()->header('Content-Type: application/json; charset=utf-8')->httpCode($code); @@ -33,6 +55,12 @@ class Response exit(0); } + /** + * Create redirect response + * + * @param string $url New target + * @param ?int $code HTTP code + */ public static function redirect(string $url, ?int $code = null): void { if ($code !== null) { diff --git a/exam/utils/helpers.php b/exam/utils/helpers.php index c98153d..23b4e30 100644 --- a/exam/utils/helpers.php +++ b/exam/utils/helpers.php @@ -1,5 +1,11 @@