398 lines
13 KiB
PHP
398 lines
13 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace KbMarkdownImporter\Olm;
|
|
|
|
use KbMarkdownImporter\Import\ImportLogger;
|
|
use KbMarkdownImporter\Plugin;
|
|
|
|
final class ChangelogSync
|
|
{
|
|
private const OPTION_ITEMS = 'kb_markdown_importer_product_updates';
|
|
private const OPTION_LAST_SYNC = 'kb_markdown_importer_changelog_last_sync';
|
|
|
|
private array $settings;
|
|
private string $baseUrl;
|
|
private array $headers = [];
|
|
|
|
public function __construct(?array $settings = null)
|
|
{
|
|
$this->settings = $settings ?: Plugin::settings();
|
|
$this->baseUrl = self::normalizeBaseUrl((string) ($this->settings['olm_base_url'] ?? ''));
|
|
}
|
|
|
|
public static function items(): array
|
|
{
|
|
$items = get_option(self::OPTION_ITEMS, []);
|
|
|
|
return is_array($items) ? $items : [];
|
|
}
|
|
|
|
public static function lastSync(): string
|
|
{
|
|
return (string) get_option(self::OPTION_LAST_SYNC, '');
|
|
}
|
|
|
|
public static function normalizeBaseUrl(string $url): string
|
|
{
|
|
$url = trim($url);
|
|
|
|
if ('' === $url) {
|
|
return '';
|
|
}
|
|
|
|
if (! preg_match('#^https?://#i', $url)) {
|
|
$url = 'https://' . $url;
|
|
}
|
|
|
|
return rtrim($url, '/');
|
|
}
|
|
|
|
public function sync(): \WP_REST_Response
|
|
{
|
|
ImportLogger::info('OLM changelog synchronization started.');
|
|
|
|
$token = $this->login();
|
|
if (is_wp_error($token)) {
|
|
ImportLogger::error('OLM changelog login failed: ' . $token->get_error_message());
|
|
return new \WP_REST_Response(['success' => false, 'message' => $token->get_error_message()], 500);
|
|
}
|
|
|
|
$this->headers = [
|
|
'Content-Type' => 'application/json',
|
|
'Accept' => 'application/json',
|
|
'Authorization' => 'Bearer ' . $token,
|
|
];
|
|
|
|
$downloadIds = $this->productDownloadIds();
|
|
if (is_wp_error($downloadIds)) {
|
|
ImportLogger::error('OLM product lookup failed: ' . $downloadIds->get_error_message());
|
|
return new \WP_REST_Response(['success' => false, 'message' => $downloadIds->get_error_message()], 500);
|
|
}
|
|
|
|
$items = [];
|
|
|
|
foreach ($downloadIds as $downloadId) {
|
|
$versions = $this->downloadFieldVersions($downloadId);
|
|
|
|
if (is_wp_error($versions)) {
|
|
ImportLogger::warning('OLM download field lookup failed for ' . $downloadId . ': ' . $versions->get_error_message());
|
|
continue;
|
|
}
|
|
|
|
foreach ($versions as $version) {
|
|
$item = $this->normalizeVersion($version);
|
|
|
|
if (null !== $item) {
|
|
$items[] = $item;
|
|
}
|
|
}
|
|
}
|
|
|
|
usort($items, static fn (array $a, array $b): int => ($b['_timestamp'] ?? 0) <=> ($a['_timestamp'] ?? 0));
|
|
$items = array_filter($items, fn (array $item): bool => $this->isInDateWindow($item));
|
|
$items = array_map(static function (array $item): array {
|
|
unset($item['_timestamp']);
|
|
return $item;
|
|
}, array_values($items));
|
|
|
|
update_option(self::OPTION_ITEMS, $items, false);
|
|
update_option(self::OPTION_LAST_SYNC, current_time('mysql'), false);
|
|
ImportLogger::info('OLM changelog synchronization completed. Entries: ' . count($items));
|
|
|
|
return new \WP_REST_Response([
|
|
'success' => true,
|
|
'stats' => [
|
|
'downloads' => count($downloadIds),
|
|
'updates' => count($items),
|
|
],
|
|
]);
|
|
}
|
|
|
|
private function login(): string|\WP_Error
|
|
{
|
|
if ('' === $this->baseUrl) {
|
|
return new \WP_Error('kb_olm_missing_base_url', __('OLM base URL missing.', 'kb-markdown-importer'));
|
|
}
|
|
|
|
$username = trim((string) ($this->settings['olm_username'] ?? ''));
|
|
$password = (string) ($this->settings['olm_password'] ?? '');
|
|
|
|
if ('' === $username || '' === $password) {
|
|
return new \WP_Error('kb_olm_missing_credentials', __('OLM credentials missing.', 'kb-markdown-importer'));
|
|
}
|
|
|
|
$response = wp_remote_post($this->baseUrl . '/login', [
|
|
'timeout' => 12,
|
|
'redirection' => 3,
|
|
'headers' => [
|
|
'Content-Type' => 'application/json',
|
|
'Accept' => 'application/json',
|
|
],
|
|
'body' => wp_json_encode([
|
|
'username' => $username,
|
|
'password' => $password,
|
|
]),
|
|
]);
|
|
|
|
if (is_wp_error($response)) {
|
|
return $response;
|
|
}
|
|
|
|
$status = (int) wp_remote_retrieve_response_code($response);
|
|
$body = (string) wp_remote_retrieve_body($response);
|
|
$data = json_decode($body, true);
|
|
|
|
if ($status < 200 || $status >= 300 || ! is_array($data) || empty($data['bearerToken'])) {
|
|
return new \WP_Error(
|
|
'kb_olm_login_failed',
|
|
__('OLM login failed.', 'kb-markdown-importer'),
|
|
[
|
|
'status' => $status,
|
|
'response_excerpt' => substr(wp_strip_all_tags($body), 0, 1000),
|
|
]
|
|
);
|
|
}
|
|
|
|
return (string) $data['bearerToken'];
|
|
}
|
|
|
|
private function productDownloadIds(): array|\WP_Error
|
|
{
|
|
$ids = [];
|
|
$page = 1;
|
|
|
|
while (true) {
|
|
$data = $this->getJson($this->baseUrl . '/api/rest/v1/product?page=' . $page . '&size=1');
|
|
|
|
if (is_wp_error($data)) {
|
|
return $data;
|
|
}
|
|
|
|
$products = is_array($data['content'] ?? null) ? $data['content'] : [];
|
|
if (! $products) {
|
|
break;
|
|
}
|
|
|
|
foreach ($products as $product) {
|
|
if (! is_array($product)) {
|
|
continue;
|
|
}
|
|
|
|
foreach ((array) ($product['downloads'] ?? []) as $download) {
|
|
$id = is_array($download) ? (string) ($download['id'] ?? '') : '';
|
|
|
|
if ('' !== $id) {
|
|
$ids[$id] = $id;
|
|
}
|
|
}
|
|
}
|
|
|
|
$page++;
|
|
}
|
|
|
|
return array_values($ids);
|
|
}
|
|
|
|
private function downloadFieldVersions(string $downloadId): array|\WP_Error
|
|
{
|
|
$versions = [];
|
|
$page = 1;
|
|
|
|
while (true) {
|
|
$data = $this->getJson($this->baseUrl . '/api/rest/v1/download/field/' . rawurlencode($downloadId) . '?page=' . $page . '&size=1');
|
|
|
|
if (is_wp_error($data)) {
|
|
return $data;
|
|
}
|
|
|
|
$content = is_array($data['content'] ?? null) ? $data['content'] : [];
|
|
if (! $content) {
|
|
break;
|
|
}
|
|
|
|
foreach ($content as $version) {
|
|
if (is_array($version)) {
|
|
$versions[] = $version;
|
|
}
|
|
}
|
|
|
|
$page++;
|
|
}
|
|
|
|
return $versions;
|
|
}
|
|
|
|
private function getJson(string $url): array|\WP_Error
|
|
{
|
|
$response = wp_remote_get($url, [
|
|
'timeout' => 12,
|
|
'redirection' => 3,
|
|
'headers' => $this->headers,
|
|
'user-agent' => 'KB Markdown Importer/' . KB_MARKDOWN_IMPORTER_VERSION,
|
|
]);
|
|
|
|
if (is_wp_error($response)) {
|
|
return $response;
|
|
}
|
|
|
|
$status = (int) wp_remote_retrieve_response_code($response);
|
|
$body = (string) wp_remote_retrieve_body($response);
|
|
$data = json_decode($body, true);
|
|
|
|
if ($status < 200 || $status >= 300 || ! is_array($data)) {
|
|
return new \WP_Error(
|
|
'kb_olm_request_failed',
|
|
__('OLM request failed.', 'kb-markdown-importer'),
|
|
[
|
|
'status' => $status,
|
|
'url' => $url,
|
|
'response_excerpt' => substr(wp_strip_all_tags($body), 0, 1000),
|
|
]
|
|
);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
private function normalizeVersion(array $version): ?array
|
|
{
|
|
$productVersion = is_array($version['productVersion'] ?? null) ? $version['productVersion'] : [];
|
|
$product = is_array($productVersion['product'] ?? null) ? $productVersion['product'] : [];
|
|
$downloadField = is_array($version['downloadField'] ?? null) ? $version['downloadField'] : [];
|
|
$productNo = strtolower((string) ($product['productNo'] ?? ''));
|
|
$publishedAt = (string) ($version['publishedAt'] ?? '');
|
|
|
|
if (true !== ($version['qa'] ?? false) || true !== ($product['published'] ?? false) || '' === $publishedAt) {
|
|
return null;
|
|
}
|
|
|
|
if (in_array($productNo, $this->ignoredOlmNumbers(), true)) {
|
|
return null;
|
|
}
|
|
|
|
$timestamp = $this->dateTimestamp($publishedAt);
|
|
$productName = $this->cleanText((string) ($product['name'] ?? ''));
|
|
$downloadName = $this->cleanText((string) ($downloadField['name'] ?? ''));
|
|
|
|
if (true !== ($downloadField['starfaceModule'] ?? false) && '' !== $downloadName) {
|
|
$productName = trim($productName . ' - ' . $downloadName);
|
|
}
|
|
|
|
$changelogLines = $this->changelogLines((string) ($version['changelog'] ?? ''));
|
|
|
|
return [
|
|
'product' => $productName,
|
|
'version' => $this->moduleVersion($productVersion, $version),
|
|
'date' => $this->formatDate($publishedAt),
|
|
'month_label' => $timestamp > 0 ? wp_date('F Y', $timestamp) : '',
|
|
'changelog' => implode(' ', $changelogLines),
|
|
'changelog_lines' => $changelogLines,
|
|
'starface_min' => $this->starfaceVersion(is_array($productVersion['minStarfaceVersion'] ?? null) ? $productVersion['minStarfaceVersion'] : []),
|
|
'starface_max' => $this->starfaceVersion(is_array($productVersion['maxStarfaceVersion'] ?? null) ? $productVersion['maxStarfaceVersion'] : []),
|
|
'link' => $this->validUrl((string) ($product['productPageURI'] ?? '')),
|
|
'download_link' => '' !== $productNo ? 'https://get.o-byte.com?olm=' . rawurlencode($productNo) : '',
|
|
'product_no' => $productNo,
|
|
'published_at' => $timestamp > 0 ? wp_date('Y-m-d', $timestamp) : '',
|
|
'_timestamp' => $timestamp,
|
|
];
|
|
}
|
|
|
|
private function isInDateWindow(array $item): bool
|
|
{
|
|
$timestamp = (int) ($item['_timestamp'] ?? 0);
|
|
if ($timestamp <= 0) {
|
|
return false;
|
|
}
|
|
|
|
$months = max(1, min(24, (int) ($this->settings['product_updates_olm_months'] ?? 4)));
|
|
$timezone = wp_timezone();
|
|
$date = (new \DateTimeImmutable('@' . $timestamp))->setTimezone($timezone);
|
|
$now = new \DateTimeImmutable('now', $timezone);
|
|
$start = $now->modify('first day of this month')->modify('-' . $months . ' months')->setTime(0, 0, 0);
|
|
|
|
return $date >= $start && $date <= $now;
|
|
}
|
|
|
|
private function ignoredOlmNumbers(): array
|
|
{
|
|
$value = strtolower((string) ($this->settings['product_updates_olm_ignore_numbers'] ?? ''));
|
|
|
|
return array_values(array_filter(array_map('trim', explode(',', $value)), static fn (string $item): bool => '' !== $item));
|
|
}
|
|
|
|
private function moduleVersion(array $productVersion, array $downloadVersion): string
|
|
{
|
|
return implode('.', [
|
|
(string) ($productVersion['major'] ?? ''),
|
|
(string) ($productVersion['minor'] ?? ''),
|
|
(string) ($downloadVersion['bugfixVersion'] ?? ''),
|
|
]);
|
|
}
|
|
|
|
private function starfaceVersion(array $version): string
|
|
{
|
|
return implode('.', [
|
|
(string) ($version['major'] ?? ''),
|
|
(string) ($version['minor'] ?? ''),
|
|
(string) ($version['build'] ?? ''),
|
|
(string) ($version['revision'] ?? ''),
|
|
]);
|
|
}
|
|
|
|
private function changelogLines(string $value): array
|
|
{
|
|
$value = str_replace(["\r\n", "\r"], "\n", $value);
|
|
$lines = [];
|
|
|
|
foreach (explode("\n", $value) as $line) {
|
|
$line = $this->cleanText($line);
|
|
|
|
if ('' !== $line) {
|
|
$lines[] = $line;
|
|
}
|
|
}
|
|
|
|
return $lines;
|
|
}
|
|
|
|
private function cleanText(string $value): string
|
|
{
|
|
$value = html_entity_decode($value, ENT_QUOTES | ENT_HTML5, get_bloginfo('charset'));
|
|
$value = preg_replace('/<br\s*\/?>/i', ' ', $value) ?? $value;
|
|
$value = wp_strip_all_tags($value);
|
|
$value = preg_replace('/\s+/', ' ', $value) ?? $value;
|
|
|
|
return trim($value);
|
|
}
|
|
|
|
private function validUrl(string $value): string
|
|
{
|
|
$value = trim($value);
|
|
|
|
if ('' === $value || in_array($value, ['.', '-'], true)) {
|
|
return '';
|
|
}
|
|
|
|
return filter_var($value, FILTER_VALIDATE_URL) ? $value : '';
|
|
}
|
|
|
|
private function dateTimestamp(string $date): int
|
|
{
|
|
if ('' === $date) {
|
|
return 0;
|
|
}
|
|
|
|
$timestamp = strtotime($date);
|
|
|
|
return $timestamp ?: 0;
|
|
}
|
|
|
|
private function formatDate(string $date): string
|
|
{
|
|
$timestamp = $this->dateTimestamp($date);
|
|
|
|
return $timestamp > 0 ? wp_date((string) get_option('date_format'), $timestamp) : $date;
|
|
}
|
|
}
|