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()); } ?>