. Topic names are restricted to lowercase slugs, * so no path outside the directory can ever be read; filter entries with another name or * a non-string content are ignored. Filtered content is site-owned and returned as-is. */ final class GuideRepository { private const TOPIC_PATTERN = '/^[a-z0-9]+(?:-[a-z0-9]+)*$/D'; /** @var array|null Filtered topics, read once per instance (= request). */ private ?array $filtered = null; public function __construct(private string $directory) { } /** @return list */ public function topics(): array { $topics = array_unique(array_merge($this->fileTopics(), array_keys($this->filtered()))); sort($topics); return array_values($topics); } public function get(string $topic): ?string { if (! self::isTopic($topic)) { return null; } $filtered = $this->filtered(); if (array_key_exists($topic, $filtered)) { return $filtered[$topic]; } if (! in_array($topic, $this->fileTopics(), true)) { return null; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $content = file_get_contents($this->directory . '/' . $topic . '.md'); return $content === false ? null : $content; } /** @return list */ private function fileTopics(): array { $files = glob($this->directory . '/*.md'); if ($files === false) { return []; } $topics = []; foreach ($files as $file) { $topic = basename($file, '.md'); if (self::isTopic($topic)) { $topics[] = $topic; } } return $topics; } /** @return array */ private function filtered(): array { if ($this->filtered !== null) { return $this->filtered; } $guides = apply_filters('lit_mcp_guides', []); $this->filtered = []; foreach (is_array($guides) ? $guides : [] as $topic => $markdown) { if (is_string($topic) && self::isTopic($topic) && is_string($markdown)) { $this->filtered[$topic] = $markdown; } } return $this->filtered; } private static function isTopic(string $topic): bool { return preg_match(self::TOPIC_PATTERN, $topic) === 1; } }