Функция поиска по постам

Мне это всё расписывать что-ли? Смотрите в содержание коммита, мне феерически индифферентно
This commit is contained in:
2024-03-07 19:58:39 +03:00
parent 8700a544b9
commit 62b7b68976
24 changed files with 804 additions and 36 deletions

View File

@@ -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")

27
api/_input_checks.php Normal file
View File

@@ -0,0 +1,27 @@
<?php
// Functions for common input checks
// Check 32 bit integer
function InpChk_IsValidInt32 (&$value): bool {
if (is_null($value))
return false;
if (is_string($value)) {
if (strlen($value) > 24)
return false;
if (!ctype_digit($value))
return false;
$value = intval($value);
}
if ($value > 0xffffffff || $value < -(0xffffffff))
return false;
return true;
}
?>

View File

@@ -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));
}

View File

@@ -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;

View File

@@ -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 "<":

305
api/post/find.php Normal file
View File

@@ -0,0 +1,305 @@
<?php
// Search posts
// Includes
if (isset($IS_FRONTEND) && $IS_FRONTEND) {
require_once("api/_auth.php");
require_once("api/_utils.php");
require_once("api/_input_checks.php");
require_once("api/_errorslist.php");
require_once("api/_types.php");
require_once("api/user/index.php");
require_once("api/post/index.php");
} else {
require_once("../_auth.php");
require_once("../_utils.php");
require_once("../_input_checks.php");
require_once("../_errorslist.php");
require_once("../_types.php");
require_once("../user/index.php");
require_once("./index.php");
}
// Functions
/*
* FUNCTION
* Get list of posts from range. Almost no checks of arguments are performed, so beware
* Arguments:
* offset - offset from start
* amount - amount of posts to return
*/
function Post_GetPostsFromRange (?int $offset = null, ?int $amount = null): ReturnT {
global $db, $Config;
$result = array();
// Managing defaults
if (is_null($offset))
$offset = 0;
if (empty($amount))
$amount = $Config["max_posts_per_request"];
// Get posts from db in range
$statement = $db->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());
}
?>

View File

@@ -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;

View File

@@ -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;