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