8, 'redirection' => 3, 'user-agent' => 'KB Markdown Importer/' . KB_MARKDOWN_IMPORTER_VERSION, ]); if (is_wp_error($response) || 200 !== (int) wp_remote_retrieve_response_code($response)) { return []; } $body = (string) wp_remote_retrieve_body($response); $items = 'rest' === $source ? self::parseJson($body, $settings) : self::parseXml($body, $settings); set_transient($cacheKey, $items, 15 * MINUTE_IN_SECONDS); return $items; } private static function parseXml(string $xml, array $settings): array { if ('' === trim($xml) || ! class_exists(\DOMDocument::class)) { return []; } $previous = libxml_use_internal_errors(true); $document = new \DOMDocument(); $loaded = $document->loadXML($xml, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING); libxml_clear_errors(); libxml_use_internal_errors($previous); if (! $loaded) { return []; } $xpath = new \DOMXPath($document); $itemNodes = $xpath->query(self::itemPath((string) ($settings['product_updates_feed_item_path'] ?? 'channel/item'))); if (! $itemNodes) { return []; } $limit = max(1, min(20, (int) ($settings['product_updates_feed_limit'] ?? 5))); $items = []; foreach ($itemNodes as $itemNode) { if (count($items) >= $limit) { break; } $date = self::fieldValue($xpath, $itemNode, (string) ($settings['product_updates_feed_date_field'] ?? 'pubDate')); $items[] = [ 'product' => self::fieldValue($xpath, $itemNode, (string) ($settings['product_updates_feed_product_field'] ?? 'title')), 'version' => self::fieldValue($xpath, $itemNode, (string) ($settings['product_updates_feed_version_field'] ?? 'category')), 'date' => self::formatDate($date), 'changelog' => self::fieldValue($xpath, $itemNode, (string) ($settings['product_updates_feed_changelog_field'] ?? 'description')), ]; } return $items; } private static function parseJson(string $json, array $settings): array { $data = json_decode($json, true); if (! is_array($data)) { return []; } $nodes = self::jsonList($data, (string) ($settings['product_updates_rest_list_path'] ?? 'content,data,items')); if (! $nodes) { return []; } $limit = max(1, min(20, (int) ($settings['product_updates_feed_limit'] ?? 5))); $items = []; foreach ($nodes as $node) { if (count($items) >= $limit || ! is_array($node)) { break; } $date = self::jsonField($node, (string) ($settings['product_updates_rest_date_field'] ?? 'releaseDate,date,updatedAt,createdAt')); $items[] = [ 'product' => self::jsonField($node, (string) ($settings['product_updates_rest_product_field'] ?? 'product.name,productName,name')), 'version' => self::jsonField($node, (string) ($settings['product_updates_rest_version_field'] ?? 'version,versionName,name')), 'date' => self::formatDate($date), 'changelog' => self::jsonField($node, (string) ($settings['product_updates_rest_changelog_field'] ?? 'changelog,changeLog,description,changes')), ]; } return $items; } private static function jsonList(array $data, string $paths): array { foreach (self::pathAlternatives($paths) as $path) { $value = '' === $path ? $data : self::jsonValue($data, $path); if (is_array($value) && array_is_list($value)) { return $value; } } return array_is_list($data) ? $data : []; } private static function jsonField(array $data, string $paths): string { foreach (self::pathAlternatives($paths) as $path) { $value = self::jsonValue($data, $path); if (is_scalar($value)) { return trim(wp_strip_all_tags((string) $value)); } if (is_array($value)) { $text = implode(', ', array_filter(array_map(static fn ($item): string => is_scalar($item) ? (string) $item : '', $value))); if ('' !== $text) { return trim(wp_strip_all_tags($text)); } } } return ''; } private static function jsonValue(array $data, string $path): mixed { $value = $data; foreach (self::pathSegments($path) as $segment) { if (! is_array($value) || ! array_key_exists($segment, $value)) { return null; } $value = $value[$segment]; } return $value; } private static function pathAlternatives(string $paths): array { return array_values(array_map('trim', explode(',', $paths))); } private static function itemPath(string $path): string { $segments = self::pathSegments($path ?: 'channel/item'); $first = (string) array_shift($segments); $query = '//*[local-name()="' . self::localName($first ?: 'item') . '"]'; foreach ($segments as $segment) { $query .= '/*[local-name()="' . self::localName((string) $segment) . '"]'; } return $query; } private static function fieldValue(\DOMXPath $xpath, \DOMNode $node, string $path): string { $segments = self::pathSegments($path); if (! $segments) { return ''; } $attribute = null; $last = (string) end($segments); if (str_starts_with($last, '@')) { $attribute = substr($last, 1); array_pop($segments); } $query = '.'; foreach ($segments as $segment) { $query .= '/*[local-name()="' . self::localName((string) $segment) . '"]'; } $result = $xpath->query($query, $node); $target = $result && $result->length > 0 ? $result->item(0) : null; if (! $target) { return ''; } if ($attribute && $target instanceof \DOMElement) { return trim(wp_strip_all_tags(html_entity_decode($target->getAttribute($attribute), ENT_QUOTES | ENT_XML1, get_bloginfo('charset')))); } return trim(wp_strip_all_tags(html_entity_decode($target->textContent, ENT_QUOTES | ENT_XML1, get_bloginfo('charset')))); } private static function pathSegments(string $path): array { $path = trim(str_replace('.', '/', $path), '/ '); if ('' === $path) { return []; } return array_values(array_filter(array_map('trim', explode('/', $path)), static fn (string $segment): bool => '' !== $segment)); } private static function localName(string $segment): string { $segment = trim($segment); $parts = explode(':', $segment); return preg_replace('/[^A-Za-z0-9_-]/', '', (string) end($parts)) ?: ''; } private static function formatDate(string $date): string { if ('' === $date) { return ''; } $timestamp = strtotime($date); if (! $timestamp) { return $date; } return wp_date((string) get_option('date_format'), $timestamp); } }