307 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			307 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
// Search posts
 | 
						|
 | 
						|
 | 
						|
 | 
						|
// Includes
 | 
						|
if (isset($IS_FRONTEND) && $IS_FRONTEND) {
 | 
						|
	require_once("api/_config.php");
 | 
						|
	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("../_config.php");
 | 
						|
	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());
 | 
						|
}
 | 
						|
 | 
						|
?>
 |