> */ public static function items(bool $force = false): array { if (!$force) { $cached = get_site_transient(self::CACHE_KEY); if (is_array($cached)) return $cached; } $response = wp_safe_remote_get(self::ENDPOINT, ['timeout' => 15, 'redirection' => 3, 'headers' => ['Accept' => 'application/json']]); if (is_wp_error($response) || 200 !== (int) wp_remote_retrieve_response_code($response)) return is_array(get_site_transient(self::CACHE_KEY)) ? get_site_transient(self::CACHE_KEY) : []; $decoded = json_decode((string) wp_remote_retrieve_body($response), true); if (!is_array($decoded)) return []; $items = []; foreach ($decoded as $record) { if (!is_array($record)) continue; // Only published catalog entries may be exposed, installed or offered as updates. if ('publish' !== (string) ($record['status'] ?? '')) continue; $item = self::normalize($record); if ('' !== $item['name']) $items[] = $item; } set_site_transient(self::CACHE_KEY, $items, HOUR_IN_SECONDS); return $items; } /** @return array|null */ public static function find(string $slug): ?array { foreach (self::items() as $item) if ($item['slug'] === $slug) return $item; return null; } public static function clear_cache(): void { delete_site_transient(self::CACHE_KEY); } /** @param array $record @return array */ private static function normalize(array $record): array { $acf = isset($record['acf']) && is_array($record['acf']) ? $record['acf'] : []; $title = isset($record['title']['rendered']) ? wp_strip_all_tags((string) $record['title']['rendered']) : ''; // The WPresstech catalog has a fixed ACF schema. Use it directly instead of guessing field names. $name = trim((string) ($acf['np_nazev_pluginu'] ?? $title)); $slug = sanitize_key((string) ($record['slug'] ?? '')); $version = trim((string) ($acf['np_verze_pluginu'] ?? '')); $description = trim(wp_strip_all_tags((string) ($acf['np_kratky_popis'] ?? ''))); $downloadable = true === ($acf['np_stahnutelny'] ?? false) || 1 === ($acf['np_stahnutelny'] ?? null) || '1' === ($acf['np_stahnutelny'] ?? null); $download = self::attachment_url($acf['np_stahnout'] ?? ''); $icon = self::attachment_url($acf['np_ikona'] ?? ''); $banner = self::attachment_url($acf['np_nahledovy_obrazek'] ?? ($record['featured_media'] ?? '')); $pluginFile = ''; return compact('slug', 'name', 'version', 'description', 'download', 'downloadable', 'icon', 'banner', 'pluginFile'); } private static function attachment_url(mixed $value): string { if (is_array($value)) { foreach (['url', 'source_url', 'download_url'] as $key) { if (!empty($value[$key]) && is_string($value[$key])) return esc_url_raw($value[$key]); } if (isset($value['ID'])) $value = $value['ID']; elseif (isset($value['id'])) $value = $value['id']; } if (is_string($value) && preg_match('~^https?://~i', trim($value))) return esc_url_raw(trim($value)); $id = absint($value); if (!$id) return ''; $cacheKey = 'ds82_3dps_media_' . $id; $cached = get_site_transient($cacheKey); if (is_string($cached) && '' !== $cached) return $cached; $response = wp_safe_remote_get('https://api.wpresstech.cz/wp-json/wp/v2/media/' . $id, [ 'timeout' => 10, 'redirection' => 3, 'headers' => ['Accept' => 'application/json'], ]); if (is_wp_error($response) || 200 !== (int) wp_remote_retrieve_response_code($response)) return ''; $media = json_decode((string) wp_remote_retrieve_body($response), true); $url = is_array($media) ? esc_url_raw((string) ($media['source_url'] ?? '')) : ''; if ('' !== $url) set_site_transient($cacheKey, $url, DAY_IN_SECONDS); return $url; } /** @param array $data @param array $keys */ private static function raw(array $data, array $keys): mixed { foreach ($keys as $key) if (array_key_exists($key, $data)) return $data[$key]; return null; } /** Find a value by exact aliases first, then recursively by key hints. */ private static function raw_fuzzy(array $data, array $keys, array $hints): mixed { $exact = self::raw($data, $keys); if (null !== $exact && '' !== $exact) return $exact; foreach ($data as $key => $value) { $normalized = strtolower(remove_accents((string)$key)); foreach ($hints as $hint) { if (str_contains($normalized, strtolower(remove_accents((string)$hint)))) return $value; } } foreach ($data as $value) { if (is_array($value)) { $found = self::raw_fuzzy($value, [], $hints); if (null !== $found && '' !== $found) return $found; } } return null; } private static function url_value(mixed $value): string { if (is_string($value)) { $value = trim($value); if (preg_match('~https?://[^\s\"\'<>]+~i', $value, $m)) return html_entity_decode($m[0], ENT_QUOTES); return $value; } if (is_array($value)) { foreach (['url','source_url','download_url','link','href','value'] as $key) { if (isset($value[$key])) { $url = self::url_value($value[$key]); if ('' !== $url) return $url; } } foreach ($value as $nested) { if (is_array($nested)) { $url = self::url_value($nested); if ('' !== $url) return $url; } } } return ''; } /** @param array $data @param array $keys */ private static function value(array $data, array $keys, string $default = ''): string { $v = self::raw($data, $keys); if (is_string($v) || is_numeric($v)) return trim((string)$v); if (is_array($v)) { foreach (['url','value','label'] as $k) if (isset($v[$k]) && is_scalar($v[$k])) return trim((string)$v[$k]); } return $default; } /** Find a scalar value by exact aliases first, then recursively by a key containing one of the hints. */ private static function value_fuzzy(array $data, array $keys, array $hints, string $default = ''): string { $exact = self::value($data, $keys, ''); if ('' !== $exact) return $exact; $found = self::find_recursive_scalar($data, $hints); return null === $found ? $default : trim((string)$found); } private static function find_recursive_scalar(array $data, array $hints): string|int|float|null { foreach ($data as $key => $value) { $normalized = strtolower(remove_accents((string)$key)); foreach ($hints as $hint) { if (str_contains($normalized, strtolower(remove_accents((string)$hint))) && (is_string($value) || is_numeric($value))) { $candidate = trim((string)$value); if ('' !== $candidate && preg_match('/^v?\d+(?:\.\d+){1,3}(?:[-+][0-9A-Za-z.-]+)?$/', $candidate)) return $candidate; } } if (is_array($value)) { $nested = self::find_recursive_scalar($value, $hints); if (null !== $nested) return $nested; } } return null; } private static function truthy(mixed $value): bool { if (true === $value || 1 === $value) return true; if (is_array($value)) { foreach (['value','checked','enabled'] as $key) if (array_key_exists($key, $value)) return self::truthy($value[$key]); } if (is_string($value)) return in_array(strtolower(trim($value)), ['1','true','yes','ano','on','enabled','publish','published','povoleno','aktivni','aktivnĂ­'], true); return false; } /** @return array{file:string,version:string,active:bool} */ public static function installed(array $item): array { if (!function_exists('get_plugins')) require_once ABSPATH . 'wp-admin/includes/plugin.php'; $plugins = get_plugins(); $wanted = (string)($item['pluginFile'] ?? ''); foreach ($plugins as $file => $data) { if (($wanted && $file === $wanted) || (($item['slug'] ?? '') && (str_starts_with($file, $item['slug'].'/') || dirname($file) === $item['slug'])) || (($item['name'] ?? '') && 0 === strcasecmp((string)($data['Name'] ?? ''), (string)$item['name']))) { return ['file'=>$file,'version'=>(string)($data['Version'] ?? ''),'active'=>is_plugin_active($file)]; } } return ['file'=>'','version'=>'','active'=>false]; } public static function inject_updates(mixed $transient): mixed { if (!is_object($transient)) return $transient; foreach (self::items() as $item) { if (!$item['downloadable'] || '' === $item['download'] || '' === $item['version']) continue; $installed = self::installed($item); if ('' === $installed['file'] || '' === $installed['version'] || !version_compare($item['version'], $installed['version'], '>')) continue; $transient->response[$installed['file']] = (object)[ 'id' => 'wpresstech-' . $item['slug'], 'slug' => $item['slug'], 'plugin' => $installed['file'], 'new_version' => $item['version'], 'url' => '', 'package' => $item['download'], ]; } return $transient; } public static function plugin_information(mixed $result, string $action, object $args): mixed { if ('plugin_information' !== $action || empty($args->slug)) return $result; $item = self::find(sanitize_key((string)$args->slug)); if (!$item) return $result; return (object)['name'=>$item['name'],'slug'=>$item['slug'],'version'=>$item['version'],'sections'=>['description'=>$item['description']],'download_link'=>$item['downloadable'] ? $item['download'] : '']; } }