>|null */ private ?array $loaded = null; public function __construct(WidgetRegistry $widgets, Settings $settings) { $this->widgets = $widgets; $this->settings = $settings; } public static function bundled_path(): string { return (defined('ZADEX_PATH') ? ZADEX_PATH : dirname(__DIR__, 2) . '/') . 'library/kits.json'; } public static function library_url(): string { return (defined('ZADEX_URL') ? ZADEX_URL : '') . 'library'; } /** The configured remote index URL ('' = bundled file). */ public function remote_url(): string { $url = (string) $this->settings->get('library_url', ''); $url = (string) apply_filters('zadex/library/remote_url', $url); return $url !== '' && preg_match('#^https?://#i', $url) ? $url : ''; } /** Kit summaries (no element trees) for the library grid. */ public function index(): array { $out = []; foreach ($this->kits() as $kit) { $out[] = [ 'slug' => $kit['slug'], 'title' => $kit['title'], 'type' => $kit['type'], 'tags' => $kit['tags'], 'thumbnail' => $kit['thumbnail'], ]; } return $out; } /** One kit with its sanitized element tree, or null. */ public function get(string $slug): ?array { foreach ($this->kits() as $kit) { if ($kit['slug'] !== $slug) { continue; } $sanitizer = new Sanitizer($this->widgets); $kit['elements'] = $sanitizer->sanitize_elements($kit['elements']); $kit['pageSettings'] = $kit['pageSettings'] ? $sanitizer->sanitize_settings_generic($kit['pageSettings']) : (object) []; return $kit; } return null; } /** @return array> validated kits */ public function kits(): array { if ($this->loaded !== null) { return $this->loaded; } $data = null; $url = $this->remote_url(); if ($url !== '') { $data = $this->fetch_remote($url); } if (!is_array($data)) { $data = $this->read_bundled(); } $this->loaded = self::validate($data); return $this->loaded; } public function flush_cache(): void { $url = $this->remote_url(); if ($url !== '') { delete_transient(self::TRANSIENT_PREFIX . md5($url)); } $this->loaded = null; } private function read_bundled(): ?array { $path = self::bundled_path(); if (!is_file($path) || (int) filesize($path) > self::MAX_BYTES) { return null; } $data = wp_json_file_decode($path, ['associative' => true]); return is_array($data) ? self::with_library_url($data) : null; } /** Replaces the `{{library}}` placeholder in every string of a decoded kit list (like decode() does on raw JSON). */ public static function with_library_url(array $data): array { $url = self::library_url(); array_walk_recursive($data, static function (&$value) use ($url): void { if (is_string($value)) { $value = str_replace('{{library}}', $url, $value); } }); return $data; } private function fetch_remote(string $url): ?array { $key = self::TRANSIENT_PREFIX . md5($url); $cached = get_transient($key); if (is_array($cached)) { return $cached; } $response = wp_remote_get($url, ['timeout' => 8, 'redirection' => 3, 'limit_response_size' => self::MAX_BYTES]); if (is_wp_error($response) || (int) wp_remote_retrieve_response_code($response) !== 200) { return null; } $data = self::decode((string) wp_remote_retrieve_body($response)); if (!is_array($data) || !self::validate($data)) { return null; } set_transient($key, $data, self::CACHE_TTL); return $data; } public static function decode(string $json): ?array { if ($json === '' || strlen($json) > self::MAX_BYTES) { return null; } $json = str_replace('{{library}}', self::library_url(), $json); $data = json_decode($json, true); return is_array($data) ? $data : null; } /** * Keeps well-formed kits only. Thumbnails are inline SVG, cleaned through the SVG sanitizer. * * @return array> */ public static function validate(?array $data): array { $list = is_array($data['kits'] ?? null) ? $data['kits'] : []; $out = []; $seen = []; foreach ($list as $kit) { if (!is_array($kit)) { continue; } $slug = isset($kit['slug']) && is_string($kit['slug']) ? $kit['slug'] : ''; if (!preg_match('/^[a-z0-9][a-z0-9\-]{0,63}$/', $slug) || isset($seen[$slug])) { continue; } if (!is_array($kit['elements'] ?? null) || !$kit['elements']) { continue; } $seen[$slug] = true; $thumb = isset($kit['thumbnail']) && is_string($kit['thumbnail']) ? $kit['thumbnail'] : ''; $out[] = [ 'slug' => $slug, 'title' => isset($kit['title']) && is_string($kit['title']) ? sanitize_text_field($kit['title']) : $slug, 'type' => Store::sanitize_type($kit['type'] ?? 'section'), 'tags' => Store::sanitize_tags($kit['tags'] ?? []), 'thumbnail' => $thumb !== '' ? Sanitizer::clean_svg($thumb) : '', 'elements' => array_values($kit['elements']), 'pageSettings' => is_array($kit['pageSettings'] ?? null) ? $kit['pageSettings'] : [], ]; } return $out; } }