From 62b7b6897656ebb4d45fde75060194c152f35e30 Mon Sep 17 00:00:00 2001 From: shr3dd3r Date: Thu, 7 Mar 2024 19:58:39 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A4=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B8=D1=81=D0=BA=D0=B0=20=D0=BF=D0=BE=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Мне это всё расписывать что-ли? Смотрите в содержание коммита, мне феерически индифферентно --- TODO.md | 13 +- api/_errorslist.php | 2 + api/_input_checks.php | 27 ++ api/_utils.php | 14 +- api/comments/index.php | 3 +- api/post/create.php | 15 +- api/post/find.php | 305 ++++++++++++++++++++ api/post/index.php | 6 +- api/user/index.php | 26 +- config.json.example | 3 +- front/notifications.php | 3 + front/pages/index/counter.php | 7 +- front/pages/index/random_meme.php | 2 +- front/pages/index/searchbox.php | 2 +- front/pages/main_nav.php | 2 +- front/pages/new_post/page.php | 3 +- front/pages/search_posts/gen_post_entry.php | 58 ++++ front/pages/search_posts/page.php | 180 ++++++++++++ front/pages/user_info/page.php | 39 +++ front/styles/base.css | 13 + front/styles/index.css | 9 + front/styles/main.css | 102 ++++++- index.php | 6 + test.png | Bin 254228 -> 251875 bytes 24 files changed, 804 insertions(+), 36 deletions(-) create mode 100644 api/_input_checks.php create mode 100644 api/post/find.php create mode 100644 front/pages/search_posts/gen_post_entry.php create mode 100644 front/pages/search_posts/page.php create mode 100644 front/pages/user_info/page.php diff --git a/TODO.md b/TODO.md index 8fd7672..61d6726 100644 --- a/TODO.md +++ b/TODO.md @@ -5,7 +5,7 @@ - Детальная стата по инстансу - Демонстрация наполнения и управление БД - "Большая Красная Кнопка" - - Общая статистика по инстансу + - ~~Общая статистика по инстансу~~ - Главная страница - Страница регистрации - Страница с отображением поста-картинки @@ -48,10 +48,10 @@ - Приглашения - Регистрация по приглашению автоматически даёт роль "проверенный" - Пост с картинкой - - Рекодирование пикчи в низкое разрешение для превью - - Описание - - Теги - - Добавление нового + - ~~Рекодирование пикчи в низкое разрешение для превью~~ + - ~~Описание~~ + - ~~Теги~~ + - ~~Добавление нового~~ - Редактирование тегов существующего - Удаление - Оценки @@ -59,7 +59,8 @@ - Статистика по всем картинкам - Комментарии - Теги - - Перечень одобренных + - ~~Перечень одобренных~~ + - Добавление, редактирование и удаление одобренных - Шаблонная разметка - Локализация - Кастомизация внешнего вида diff --git a/api/_errorslist.php b/api/_errorslist.php index f9eace9..639439c 100644 --- a/api/_errorslist.php +++ b/api/_errorslist.php @@ -30,6 +30,7 @@ const E_AUT_WRONGCREDS = 305; // User with that credentials does not exist const E_ACS_PERMDENIED = 401; // Permission to object denied const E_ACS_INSUFROLE = 402; // Insufficient role // Database-related errors +const E_DBE_UNKNOWN = 500; // Unknown error const E_DBE_INSERTFAIL = 501; // INSERT query failed const E_DBE_SELECTFAIL = 502; // SELECT query failed const E_DBE_DELETEFAIL = 503; // DELETE query failed @@ -64,6 +65,7 @@ $Errors_Enum = array( array("acs.permdenied", E_ACS_PERMDENIED, "permission denied"), array("acs.insufrole", E_ACS_INSUFROLE, "insufficient role"), // Database-related errors + array("dbe.unknown", E_DBE_UNKNOWN, "unknown database error"), array("dbe.insertfail", E_DBE_INSERTFAIL, "insert query failed"), array("dbe.selectfail", E_DBE_SELECTFAIL, "select query failed"), array("dbe.deletefail", E_DBE_DELETEFAIL, "delete query failed") diff --git a/api/_input_checks.php b/api/_input_checks.php new file mode 100644 index 0000000..679d646 --- /dev/null +++ b/api/_input_checks.php @@ -0,0 +1,27 @@ + 24) + return false; + if (!ctype_digit($value)) + return false; + $value = intval($value); + } + + if ($value > 0xffffffff || $value < -(0xffffffff)) + return false; + + return true; +} + + + +?> \ No newline at end of file diff --git a/api/_utils.php b/api/_utils.php index 81a19b0..7ef32c6 100644 --- a/api/_utils.php +++ b/api/_utils.php @@ -4,20 +4,19 @@ // Check if request was to specified file -function Utils_ThisFileIsRequested ($fullpath): bool { - return substr($fullpath, -strlen($_SERVER["SCRIPT_NAME"])) === $_SERVER["SCRIPT_NAME"]; +function Utils_ThisFileIsRequested (string $fullpath): bool { + return (substr($fullpath, -strlen($_SERVER["SCRIPT_NAME"])) === $_SERVER["SCRIPT_NAME"]) + || ($fullpath === $_SERVER["SCRIPT_NAME"]); // Old variant won't work on some configurations, as reported by doesnm } // Generate secure random string function Utils_GenerateRandomString (int $length, string $keyspace = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"): string { - if ($length < 1) { + if ($length < 1) die("cant generate random string of size less than 1"); - } $pieces = []; $max = mb_strlen($keyspace, "8bit") - 1; - for ($i = 0; $i < $length; ++$i) { + for ($i = 0; $i < $length; ++$i) $pieces []= $keyspace[random_int(0, $max)]; - } return implode("", $pieces); } @@ -32,7 +31,8 @@ function Utils_GetRatio ($x, $y) { function Utils_JoinPaths () { $paths = array(); foreach (func_get_args() as $arg) { - if ($arg !== '') { $paths[] = $arg; } + if ($arg !== "") + $paths[] = $arg; } return preg_replace('#/+#', '/', join('/', $paths)); } diff --git a/api/comments/index.php b/api/comments/index.php index 0e37b86..319c6a4 100644 --- a/api/comments/index.php +++ b/api/comments/index.php @@ -45,13 +45,14 @@ function Comments_GetSectionRange (int $sec_id, int $ts_from = 0, int $ts_to = 0 $result = array(); $s = $db->prepare("SELECT * FROM comments WHERE comment_section_id=? AND created_at>=? AND created_at<=? ORDER BY created_at"); - $s->bind_param("sss", $sec_id, date("Y-m-d H:i:s", $ts_from), date("Y-m-d H:i:s", $ts_to)); + $s->bind_param("iss", $sec_id, date("Y-m-d H:i:s", $ts_from), date("Y-m-d H:i:s", $ts_to)); $s->execute(); $d = $s->get_result(); if (!(bool)$d) return new ReturnT(data: $result); + // TODO: move this check to method $isAdmin = false; if ($LOGGED_IN && User_HasRole($THIS_USER, "admin")->GetData()) $isAdmin = true; diff --git a/api/post/create.php b/api/post/create.php index 1f1d0ec..0b7a5b4 100644 --- a/api/post/create.php +++ b/api/post/create.php @@ -45,7 +45,7 @@ function Post_ParseRawTagString (string $str): ReturnT { $currLen = 0; $currTag = ""; } else { - return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "syntax error while trying to parse tags"); + return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "syntax error at index $i while trying to parse tags"); } } elseif (!IntlChar::isspace($str[$i])) { $currTag .= $str[$i]; @@ -70,7 +70,7 @@ function Post_ParseRawTagString (string $str): ReturnT { * FUNCTION * Check if image size properties are valid */ -function Post_ImgResIsValid ($x, $y): bool { +function Post_ImgResIsValid (int $x, int $y): bool { global $Config; return ($x <= $Config["media"]["max_pic_res"]["x"]) @@ -179,10 +179,6 @@ function Post_Create ( $result = null; - // Author ID must exist - if (!User_IDExist($author_id)) - return new ReturnT(err_code: E_UIN_WRONGID, err_desc: "specified user id does not exist"); - // Performing SQL query $s = $db->prepare("INSERT INTO posts (author_id,tags,title,pic_path,preview_path,comments_enabled,edit_lock) VALUES (?,?,?,?,?,?,?)"); $s->bind_param("issssii", $author_id, $tags, $title, $pic_path, $prev_path, $comms_enabled, $edit_lock); @@ -202,6 +198,11 @@ function Post_Create ( /* * METHOD * Create single publication + * Request fields: + * tags - list of tags, should be delimited by comma + * title - optional title for post + * Files fields: + * pic - id of file object in $_FILES variable */ function Post_Create_Method (array $req, array $files): ReturnT { global $Config, $LOGGED_IN, $THIS_USER; @@ -257,7 +258,7 @@ function Post_Create_Method (array $req, array $files): ReturnT { $realTitleLen = strlen($req["title"]); if ($realTitleLen > $maxTitleLen) return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "title length exceeds maximum value"); - // Cleaning off all bad symbols (no script injection allowed here) + // Cleaning off all bad symbols (no script injection allowed here) TODO: move to function for ($i = 0; $i < $realTitleLen; ++$i) { switch ($req["title"][$i]) { case "<": diff --git a/api/post/find.php b/api/post/find.php new file mode 100644 index 0000000..4adcd43 --- /dev/null +++ b/api/post/find.php @@ -0,0 +1,305 @@ +prepare("SELECT * FROM posts LIMIT ?, ?"); + $statement->bind_param("ii", $offset, $amount); + $statement->execute(); + if (($queryResult = $statement->get_result()) === false) + return new ReturnT(err_code: E_DBE_UNKNOWN); + + $result = $queryResult->fetch_all(MYSQLI_ASSOC); + + return new ReturnT(data: $result); +} + +/* + * FUNCTION + * Get list of posts matching criteria. No additional checks of arguments are performed + * Arguments: + * tags - must be array of valid tags or null + * author_ids - must be array of valid author ids or null + * ts_after - valid starting timestamp for filtering by time, that less or equal to ts_before, or null + * ts_before - valid ending timestamp for filtering by time, that bigger or equal to ts_after, or null + */ +function Post_GetMatchingPosts ( + ?array $tags = null, + ?array $author_ids = null, + ?int $ts_after = null, + ?int $ts_before = null + ): ReturnT { + global $db; + + $result = array(); + + // Managing defaults + if (is_null($ts_after)) + $ts_after = 0; + if (is_null($ts_before)) + $ts_before = 0xffffffff; + + $dateFrom = date("Y-m-d H:i:s", $ts_after); + $dateTo = date("Y-m-d H:i:s", $ts_before); + + // Get posts from db in time range + $s = $db->prepare("SELECT * FROM posts WHERE created_at>=? AND created_at<=?"); + $s->bind_param("ss", $dateFrom, $dateTo); + $s->execute(); + $d = $s->get_result(); + + // Filter them out + // NOTICE: ~~skill~~ perf issue, will wildly affect response time and memory usage on big sets + + // Filter by author, if needed + $needToFilterByAuthor = !empty($author_ids); + $tempFilteredByAuthor = array(); + // If post author is any author from list - we take it + while ($row = $d->fetch_array()) { + if (!$needToFilterByAuthor || ($needToFilterByAuthor && in_array($row["author_id"], $author_ids))) + $tempFilteredByAuthor[] = array( // NOTICE: this should look better + "id" => $row["id"], + "author_id" => $row["author_id"], + "comment_section_id" => $row["comment_section_id"], + "created_at" => $row["created_at"], + "tags" => $row["tags"], + "title" => $row["title"], + "votes_up" => $row["votes_up"], + "votes_down" => $row["votes_down"], + "views" => $row["views"], + "pic_path" => $row["pic_path"], + "preview_path" => $row["preview_path"], + "comments_enabled" => $row["comments_enabled"], + "edit_lock" => $row["edit_lock"] + ); + } + if (!count($tempFilteredByAuthor)) + return new ReturnT(data: $result); + + // Filter by tags + // If post has all of the tags from list - we take it + foreach ($tempFilteredByAuthor as $post) { + $fitsFilter = true; + foreach ($tags as $singleTag) { + if (!str_contains($post["tags"], $singleTag)) { + $fitsFilter = false; + break; + } + } + if ($fitsFilter) + $result[] = $post; + } + + // Return result + return new ReturnT(data: $result); +} + +/* + * FUNCTION + * Parse raw query to list of tags and author IDs. Checks on encoding are not performed + * Arguments: + * query - ASCII query string + */ +function Post_ParseRawQuery (string $query): ReturnT { + global $Config; + + $result = array( + "tags" => array(), + "author_ids" => array() + ); + + $allowedTagSymbols = $Config["posting"]["tags"]["allowed_syms"]; + $badTagSymbolsPreg = "/[^" . $allowedTagSymbols . "]/"; + $allowedLoginSymbols = $Config["registration"]["allowed_syms"]; + $badLoginSymbolsPreg = "/[^" . $allowedLoginSymbols . "]/"; + + $maxTagLength = $Config["posting"]["tags"]["max_single_length"]; + $queryLength = strlen($query); + + $currWord = ""; + $currWordLen = 0; + $isAuthor = false; + + // Parse everything + for ($i = 0; $i <= $queryLength; ++$i) { + if ($i === $queryLength || $query[$i] === ",") { // If end of query or comma + // NOTICE: potential fix ` || (IntlChar::isspace($query[$i]) && $isAuthor)` + // NOTICE: currently, query tags are separated by comma, but may be i should make it by space + if ($currWordLen > 0) { // If we have some word + if ($isAuthor) { // If word is author meta-field + $isAuthor = false; + if (preg_match($badLoginSymbolsPreg, $currWord)) // Unallowed symbols in login are detected + return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "only allowed symbols in logins are \"$allowedLoginSymbols\""); + $userIDRet = User_GetIDByLogin($currWord); // Fetching user ID by login + if ($userIDRet->IsError()) + return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "user $currWord does not exist"); + else + $result["author_ids"][] = $userIDRet->GetData(); + } else { // If word is tag + $result["tags"][] = $currWord; + } + // Reset current word + $currWordLen = 0; + $currWord = ""; + } else { // If malformed query + return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "syntax error at index $i: starting/ending comma, sequence of commas or missing meta-field value"); + } + } elseif ($query[$i] === ":") { // Semicolon means this is meta-field + if (strtolower($currWord) === "author") { // If meta-field is author + $isAuthor = true; // Set author meta-field flag + // Reset word + $currWordLen = 0; + $currWord = ""; + } else { // Invalid metafield + return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "syntax error at index $i: invalid meta-field name \"$currWord\""); + } + } elseif (!preg_match($badTagSymbolsPreg, $query[$i]) || $isAuthor) { // If any valid non-special symbol OR we parsing login now + $currWord .= $query[$i]; + if (++$currWordLen > $maxTagLength) + return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "word too large: $currWord"); + } elseif (!IntlChar::isspace($query[$i])) { // If we have something that is not whitespace + return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "unexpected symbol at index $i: " . $query[$i]); + } + } + + return new ReturnT(data: $result); +} + + + +// Methods + +/* + * METHOD + * Returns list of posts from supplied range based on supplied raw filter parameters + * Request fields: + * query - raw query string + * offset - beginning of posts range + * amount - number of posts to get (optional) + */ +function Post_GetMatchingPosts_Method (array $req): ReturnT { + global $Config; + + $cfgMaxPostsPerRequest = $Config["max_posts_per_request"]; + + $reqQuery = null; + $reqOffset = null; + $reqAmount = null; + // TODO: filter by time range + + // Input sanity checks + + // Generic checks + if (isset($req["offset"])) { + $reqOffset = $req["offset"]; + if (!InpChk_IsValidInt32($reqOffset)) + return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "invalid offset value: $reqOffset"); + } else { + $reqOffset = 0; + } + if (isset($req["amount"])) { + $reqAmount = $req["amount"]; + if (!InpChk_IsValidInt32($reqAmount)) + return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "invalid amount value: $reqAmount"); + } else { + $reqAmount = $cfgMaxPostsPerRequest; // TODO: account defaults + } + + // Specific checks + if ($reqOffset < 0) + return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "offset must be zero or bigger"); + if ($reqAmount < 1) + return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "posts amount must be bigger than 1"); + if ($reqAmount > $cfgMaxPostsPerRequest) + $reqAmount = $cfgMaxPostsPerRequest; + + // Generic check again + if (!isset($req["query"])) { + $result = Post_GetPostsFromRange($reqOffset, $reqAmount); + if ($result->IsError()) + return $result; + $resData = $result->GetData(); + return new ReturnT(data: array( // Just return posts from range, without filtering + "data" => $resData, + "total_amount" => count($resData) + )); + } + $reqQuery = $req["query"]; + + // Check query and parse it to array + if (!Utils_IsAscii($reqQuery)) + return new ReturnT(err_code: E_UIN_BADARGS, err_desc: "query must be ASCII string"); + $qr = Post_ParseRawQuery($reqQuery); + if ($qr->IsError()) + return $qr; + $query = $qr->GetData(); + + // Actions + + // NOTICE: perf issue + $result = Post_GetMatchingPosts($query["tags"], $query["author_ids"]); + if ($result->IsError()) + return $result; + $resData = $result->GetData(); + return new ReturnT(data: array( + "data" => array_slice($resData, $reqOffset, $reqAmount), + "total_amount" => count($resData) // NOTICE: very shitty design + )); +} + + + +if (Utils_ThisFileIsRequested(__FILE__)) { + require_once("../_json.php"); + + $result = Post_GetMatchingPosts_Method($_REQUEST); + + if ($result->IsError()) + $result->ThrowJSONError(); + else + JSON_ReturnData($result->GetData()); +} + +?> \ No newline at end of file diff --git a/api/post/index.php b/api/post/index.php index 25b18ad..9621bce 100644 --- a/api/post/index.php +++ b/api/post/index.php @@ -4,7 +4,7 @@ // Includes -if ($IS_FRONTEND) { +if (isset($IS_FRONTEND) && $IS_FRONTEND) { require_once("api/_auth.php"); require_once("api/_utils.php"); require_once("api/_errorslist.php"); @@ -61,7 +61,7 @@ function Post_GetByID (int $id): ReturnT { $result = array(); $s = $db->prepare("SELECT * FROM posts WHERE id = ?"); - $s->bind_param("s", $id); + $s->bind_param("i", $id); $s->execute(); $d = $s->get_result()->fetch_assoc(); @@ -97,7 +97,7 @@ function Post_GetByID (int $id): ReturnT { * METHOD * Get post information by ID */ -function Post_GetByID_Method (array $req) { +function Post_GetByID_Method (array $req): ReturnT { // Input sanity checks $PostID = null; diff --git a/api/user/index.php b/api/user/index.php index 5aec564..d35c829 100644 --- a/api/user/index.php +++ b/api/user/index.php @@ -4,7 +4,7 @@ // Includes -if ($IS_FRONTEND) { +if (isset($IS_FRONTEND) && $IS_FRONTEND) { require_once("api/_auth.php"); require_once("api/_utils.php"); require_once("api/_errorslist.php"); @@ -109,7 +109,25 @@ function User_IsMod (int $id): ReturnT { /* * FUNCTION - * Get user information from DB + * Get user ID by login + */ +function User_GetIDByLogin (string $login): ReturnT { + global $db; + + $s = $db->prepare("SELECT * FROM users WHERE login = ?"); + $s->bind_param("s", $login); + $s->execute(); + $d = $s->get_result()->fetch_assoc(); + + if (!(bool)$d) + return new ReturnT(err_code: E_UIN_WRONGID, err_desc: "user not found in database"); + + return new ReturnT(data: $d["id"]); +} + +/* + * FUNCTION + * Get user information from DB by supplied ID */ function User_GetInfoByID (int $id): ReturnT { global $db, $THIS_USER, $LOGGED_IN; @@ -117,7 +135,7 @@ function User_GetInfoByID (int $id): ReturnT { $result = array(); $s = $db->prepare("SELECT * FROM users WHERE id = ?"); - $s->bind_param("s", $id); + $s->bind_param("i", $id); $s->execute(); $d = $s->get_result()->fetch_assoc(); @@ -151,6 +169,8 @@ function User_GetInfoByID (int $id): ReturnT { /* * METHOD * Get user information from DB + * Request fields: + * id - user id */ function User_GetInfoByID_Method (array $req): ReturnT { global $THIS_USER, $LOGGED_IN; diff --git a/config.json.example b/config.json.example index be9a16c..cbcdb6e 100644 --- a/config.json.example +++ b/config.json.example @@ -46,5 +46,6 @@ "title": { "max_length": 4096 } - } + }, + "max_posts_per_request": 120 } \ No newline at end of file diff --git a/front/notifications.php b/front/notifications.php index 85daf48..a1e2e68 100644 --- a/front/notifications.php +++ b/front/notifications.php @@ -15,6 +15,9 @@ function NTFY_AddNotice (string $text, string $type = "fail") { case "fail": $NTFY_NoticesQueue[] = "

$text

"; break; + case "warning": + $NTFY_NoticesQueue[] = "

$text

"; + break; case "success": $NTFY_NoticesQueue[] = "

$text

"; break; diff --git a/front/pages/index/counter.php b/front/pages/index/counter.php index 2767a50..e617018 100644 --- a/front/pages/index/counter.php +++ b/front/pages/index/counter.php @@ -17,16 +17,17 @@ require_once("api/post/index.php"); $totalPostsAmount = Post_GetPostsAmount(); $totalPostsAmount = strval($totalPostsAmount); +$totalPostsAmountLen = strlen($totalPostsAmount); ?>
"; - while (count($allNumbers) < 7) + while (count($allNumbers) < (7 - $totalPostsAmountLen)) $allNumbers[] = ""; + for ($i = 0; $i < $totalPostsAmountLen; ++$i) + $allNumbers[] = ""; foreach ($allNumbers as $numberImg) echo $numberImg; ?> diff --git a/front/pages/index/random_meme.php b/front/pages/index/random_meme.php index ad8e045..882d8cd 100644 --- a/front/pages/index/random_meme.php +++ b/front/pages/index/random_meme.php @@ -2,5 +2,5 @@ // TODO: picking random meme ?>
- +
diff --git a/front/pages/index/searchbox.php b/front/pages/index/searchbox.php index 5e6296a..96fd214 100644 --- a/front/pages/index/searchbox.php +++ b/front/pages/index/searchbox.php @@ -47,7 +47,7 @@ require_once("api/user/index.php");
-
+