165 lines
5.2 KiB
PHP
165 lines
5.2 KiB
PHP
<?php
|
|
|
|
require_once __DIR__.'/../bootstrap.php';
|
|
|
|
use App\Classes\BaseHandler;
|
|
|
|
class SearchHandler extends BaseHandler
|
|
{
|
|
protected int $cacheTTL = 300;
|
|
|
|
public function __construct()
|
|
{
|
|
parent::__construct();
|
|
$this->initializeCache();
|
|
}
|
|
|
|
public function handleRequest(): void
|
|
{
|
|
try {
|
|
$query = $this->validateAndSanitizeQuery($_GET['q'] ?? null);
|
|
$sections = $this->validateAndSanitizeSections($_GET['section'] ?? '');
|
|
$page = isset($_GET['page']) ? intval($_GET['page']) : 1;
|
|
$pageSize = isset($_GET['pageSize']) ? intval($_GET['pageSize']) : 10;
|
|
$offset = ($page - 1) * $pageSize;
|
|
$cacheKey = $this->generateCacheKey($query, $sections, $page, $pageSize);
|
|
$results = [];
|
|
$results = $this->getCachedResults($cacheKey) ?? $this->fetchSearchResults($query, $sections, $pageSize, $offset);
|
|
|
|
if (empty($results) || empty($results['data'])) {
|
|
$this->sendResponse(['results' => [], 'total' => 0, 'page' => $page, 'pageSize' => $pageSize], 200);
|
|
|
|
return;
|
|
}
|
|
|
|
$this->cacheResults($cacheKey, $results);
|
|
$this->sendResponse(
|
|
[
|
|
'results' => $results['data'],
|
|
'total' => $results['total'],
|
|
'page' => $page,
|
|
'pageSize' => $pageSize,
|
|
],
|
|
200
|
|
);
|
|
} catch (\Exception $e) {
|
|
error_log('Search API Error: '.$e->getMessage());
|
|
$this->sendErrorResponse('Invalid request. Please check your query and try again.', 400);
|
|
}
|
|
}
|
|
|
|
private function validateAndSanitizeQuery(?string $query): string
|
|
{
|
|
if (empty($query) || ! is_string($query)) {
|
|
throw new \Exception("Invalid 'q' parameter. Must be a non-empty string.");
|
|
}
|
|
|
|
$query = trim($query);
|
|
|
|
if (strlen($query) > 255) {
|
|
throw new \Exception("Invalid 'q' parameter. Exceeds maximum length of 255 characters.");
|
|
}
|
|
if (! preg_match('/^[a-zA-Z0-9\s\-_\'"]+$/', $query)) {
|
|
throw new \Exception("Invalid 'q' parameter. Contains unsupported characters.");
|
|
}
|
|
|
|
$query = preg_replace("/\s+/", ' ', $query);
|
|
|
|
return $query;
|
|
}
|
|
|
|
private function validateAndSanitizeSections(string $rawSections): ?array
|
|
{
|
|
$allowedSections = ['post', 'artist', 'genre', 'book', 'movie', 'show'];
|
|
|
|
if (empty($rawSections)) {
|
|
return null;
|
|
}
|
|
|
|
$sections = array_map(
|
|
fn ($section) => strtolower(
|
|
trim(htmlspecialchars($section, ENT_QUOTES, 'UTF-8'))
|
|
),
|
|
explode(',', $rawSections)
|
|
);
|
|
$invalidSections = array_diff($sections, $allowedSections);
|
|
|
|
if (! empty($invalidSections)) {
|
|
throw new Exception("Invalid 'section' parameter. Unsupported sections: ".implode(', ', $invalidSections));
|
|
}
|
|
|
|
return $sections;
|
|
}
|
|
|
|
private function fetchSearchResults(
|
|
string $query,
|
|
?array $sections,
|
|
int $pageSize,
|
|
int $offset
|
|
): array {
|
|
$sectionsParam = $sections && count($sections) > 0 ? '%7B'.implode(',', $sections).'%7D' : '';
|
|
$endpoint = 'rpc/search_optimized_index';
|
|
$queryString =
|
|
'search_query='.
|
|
urlencode($query).
|
|
"&page_size={$pageSize}&page_offset={$offset}".
|
|
($sectionsParam ? "§ions={$sectionsParam}" : '');
|
|
$data = $this->makeRequest('GET', "{$endpoint}?{$queryString}");
|
|
$total = count($data) > 0 ? $data[0]['total_count'] : 0;
|
|
$results = array_map(function ($item) {
|
|
unset($item['total_count']);
|
|
|
|
if (! empty($item['description'])) {
|
|
$item['description'] = truncateText(strip_tags(parseMarkdown($item['description'])), 225);
|
|
}
|
|
|
|
return $item;
|
|
|
|
}, $data);
|
|
|
|
return ['data' => $results, 'total' => $total];
|
|
}
|
|
|
|
private function generateCacheKey(
|
|
string $query,
|
|
?array $sections,
|
|
int $page,
|
|
int $pageSize
|
|
): string {
|
|
$sectionsKey = $sections ? implode(',', $sections) : 'all';
|
|
|
|
return sprintf(
|
|
'search:%s:sections:%s:page:%d:pageSize:%d',
|
|
md5($query),
|
|
$sectionsKey,
|
|
$page,
|
|
$pageSize
|
|
);
|
|
}
|
|
|
|
private function getCachedResults(string $cacheKey): ?array
|
|
{
|
|
if ($this->cache instanceof \Redis) {
|
|
$cachedData = $this->cache->get($cacheKey);
|
|
|
|
return $cachedData ? json_decode($cachedData, true) : null;
|
|
} elseif (is_array($this->cache)) {
|
|
return $this->cache[$cacheKey] ?? null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function cacheResults(string $cacheKey, array $results): void
|
|
{
|
|
if ($this->cache instanceof \Redis) {
|
|
$this->cache->set($cacheKey, json_encode($results));
|
|
$this->cache->expire($cacheKey, $this->cacheTTL);
|
|
} elseif (is_array($this->cache)) {
|
|
$this->cache[$cacheKey] = $results;
|
|
}
|
|
}
|
|
}
|
|
|
|
$handler = new SearchHandler();
|
|
$handler->handleRequest();
|