'array', 'sanitize_callback' => [self::class, 'sanitize'], 'default' => Settings::defaults(), ]); } public static function sanitize(array $input): array { $old = Plugin::settings(); $settings = Settings::defaults(); $settings['gitlab_base_url'] = esc_url_raw(GitLabClient::normalizeBaseUrl((string) ($input['gitlab_base_url'] ?? ''))); $settings['gitlab_token'] = trim((string) ($input['gitlab_token'] ?? '')) ?: (string) $old['gitlab_token']; $settings['gitlab_group'] = sanitize_text_field((string) ($input['gitlab_group'] ?? 'knowledgebase')); $settings['branch_pattern'] = sanitize_text_field((string) ($input['branch_pattern'] ?? '^v.*')); $settings['docs_base_slug'] = sanitize_title((string) ($input['docs_base_slug'] ?? 'docs')) ?: 'docs'; $settings['image_lightbox'] = ! empty($input['image_lightbox']) ? '1' : '0'; $settings['public_docs'] = ! empty($input['public_docs']) ? '1' : '0'; $settings['allow_svg'] = ! empty($input['allow_svg']) ? '1' : '0'; $settings['cron_interval'] = in_array(($input['cron_interval'] ?? 'disabled'), ['disabled', 'hourly', 'daily', 'weekly'], true) ? (string) $input['cron_interval'] : 'disabled'; $settings['design_theme'] = in_array(($input['design_theme'] ?? 'obyte'), ['obyte', 'inherit'], true) ? (string) $input['design_theme'] : 'obyte'; $settings['design_primary_color'] = self::sanitizeHexColor((string) ($input['design_primary_color'] ?? '#00A7E6'), '#00A7E6'); $settings['design_accent_color'] = self::sanitizeHexColor((string) ($input['design_accent_color'] ?? '#F59C00'), '#F59C00'); $settings['design_radius'] = (string) max(0, min(32, (int) ($input['design_radius'] ?? 14))); $settings['custom_theme_css_url'] = esc_url_raw((string) ($input['custom_theme_css_url'] ?? '')); $settings['docs_home_intro_title'] = sanitize_text_field((string) ($input['docs_home_intro_title'] ?? $settings['docs_home_intro_title'])); $settings['docs_home_intro_content'] = wp_kses_post((string) ($input['docs_home_intro_content'] ?? $settings['docs_home_intro_content'])); $settings['product_updates_source'] = in_array(($input['product_updates_source'] ?? 'rss'), ['rss', 'rest'], true) ? (string) $input['product_updates_source'] : 'rss'; $settings['product_updates_feed_url'] = esc_url_raw((string) ($input['product_updates_feed_url'] ?? '')); $settings['product_updates_feed_limit'] = (string) max(1, min(20, (int) ($input['product_updates_feed_limit'] ?? 5))); $settings['product_updates_feed_item_path'] = self::sanitizeXmlPath((string) ($input['product_updates_feed_item_path'] ?? 'channel/item'), 'channel/item'); $settings['product_updates_feed_product_field'] = self::sanitizeXmlPath((string) ($input['product_updates_feed_product_field'] ?? 'title'), 'title'); $settings['product_updates_feed_version_field'] = self::sanitizeXmlPath((string) ($input['product_updates_feed_version_field'] ?? 'category'), 'category'); $settings['product_updates_feed_date_field'] = self::sanitizeXmlPath((string) ($input['product_updates_feed_date_field'] ?? 'pubDate'), 'pubDate'); $settings['product_updates_feed_changelog_field'] = self::sanitizeXmlPath((string) ($input['product_updates_feed_changelog_field'] ?? 'description'), 'description'); $settings['product_updates_rest_url'] = esc_url_raw((string) ($input['product_updates_rest_url'] ?? '')); $settings['product_updates_rest_list_path'] = self::sanitizePathList((string) ($input['product_updates_rest_list_path'] ?? 'content,data,items'), 'content,data,items'); $settings['product_updates_rest_product_field'] = self::sanitizePathList((string) ($input['product_updates_rest_product_field'] ?? 'product.name,productName,name'), 'product.name,productName,name'); $settings['product_updates_rest_version_field'] = self::sanitizePathList((string) ($input['product_updates_rest_version_field'] ?? 'version,versionName,name'), 'version,versionName,name'); $settings['product_updates_rest_date_field'] = self::sanitizePathList((string) ($input['product_updates_rest_date_field'] ?? 'releaseDate,date,updatedAt,createdAt'), 'releaseDate,date,updatedAt,createdAt'); $settings['product_updates_rest_changelog_field'] = self::sanitizePathList((string) ($input['product_updates_rest_changelog_field'] ?? 'changelog,changeLog,description,changes'), 'changelog,changeLog,description,changes'); Plugin::syncCronSchedule($settings); if (($old['docs_base_slug'] ?? 'docs') !== $settings['docs_base_slug']) { flush_rewrite_rules(false); } return $settings; } public static function render(): void { if (! current_user_can('manage_kb_docs')) { wp_die(esc_html__('Insufficient permissions.', 'kb-markdown-importer')); } if (isset($_POST['kb_markdown_test_connection']) && check_admin_referer('kb_markdown_test_connection')) { self::handleConnectionTest(); } $updatesTest = null; if (isset($_POST['kb_markdown_test_product_updates']) && check_admin_referer('kb_markdown_test_product_updates')) { $updatesTest = self::handleProductUpdatesTest(); } $settings = Plugin::settings(); ?>

getGroup(Plugin::settings()['gitlab_group']); if (is_wp_error($result)) { $message = self::formatConnectionError($result); ImportLogger::error('GitLab connection failed: ' . $message); add_settings_error('kb_markdown_importer', 'connection_failed', esc_html($message), 'error'); settings_errors('kb_markdown_importer'); return; } ImportLogger::info('GitLab connection successful.'); add_settings_error('kb_markdown_importer', 'connection_ok', esc_html__('GitLab connection successful.', 'kb-markdown-importer'), 'success'); settings_errors('kb_markdown_importer'); } private static function handleProductUpdatesTest(): array { $settings = Plugin::settings(); $source = (string) ($settings['product_updates_source'] ?? 'rss'); $url = esc_url_raw((string) ('rest' === $source ? ($settings['product_updates_rest_url'] ?? '') : ($settings['product_updates_feed_url'] ?? ''))); if ('' === $url) { return [ 'ok' => false, 'title' => __('Keine Produktupdate-Quelle konfiguriert.', 'kb-markdown-importer'), 'message' => __('Bitte zuerst eine RSS/XML- oder REST/JSON-URL speichern.', 'kb-markdown-importer'), 'body' => '', ]; } $response = wp_remote_get($url, [ 'timeout' => 12, 'redirection' => 3, 'user-agent' => 'KB Markdown Importer/' . KB_MARKDOWN_IMPORTER_VERSION, ]); if (is_wp_error($response)) { return [ 'ok' => false, 'title' => __('Produktupdate-Quelle nicht erreichbar.', 'kb-markdown-importer'), 'message' => $response->get_error_message(), 'body' => '', ]; } $status = (int) wp_remote_retrieve_response_code($response); $contentType = (string) wp_remote_retrieve_header($response, 'content-type'); $body = (string) wp_remote_retrieve_body($response); $excerpt = substr($body, 0, 12000); $validPayload = true; $payloadNote = ''; if ('rest' === $source) { json_decode($body, true); $validPayload = JSON_ERROR_NONE === json_last_error(); if (! $validPayload) { $payloadNote = ' ' . sprintf( /* translators: %s: JSON parser error message. */ __('Die Antwort ist kein gültiges JSON: %s', 'kb-markdown-importer'), json_last_error_msg() ); } } $message = sprintf( /* translators: 1: source type, 2: HTTP status code, 3: content type. */ __('Quelle: %1$s | HTTP-Status: %2$d | Content-Type: %3$s', 'kb-markdown-importer'), 'rest' === $source ? 'REST/JSON' : 'RSS/XML', $status, $contentType ?: '-' ); $message .= $payloadNote; if (strlen($body) > strlen($excerpt)) { $message .= ' ' . __('Die Antwort wurde auf 12000 Zeichen gekürzt.', 'kb-markdown-importer'); } $ok = $status >= 200 && $status < 300 && $validPayload; return [ 'ok' => $ok, 'title' => $ok ? __('Produktupdate-Quelle erreichbar.', 'kb-markdown-importer') : __('Produktupdate-Quelle nicht nutzbar.', 'kb-markdown-importer'), 'message' => $message, 'body' => $excerpt, ]; } private static function formatConnectionError(\WP_Error $error): string { $message = $error->get_error_message(); $data = $error->get_error_data(); if (! is_array($data)) { return $message; } if (! empty($data['url'])) { $message .= ' Target: ' . $data['url']; } if (! empty($data['retry_after'])) { $message .= ' Retry-After: ' . $data['retry_after']; } if (! empty($data['response_excerpt'])) { $message .= ' Response: ' . $data['response_excerpt']; } return $message; } private static function sanitizeHexColor(string $value, string $fallback): string { $value = trim($value); return preg_match('/^#[0-9a-fA-F]{6}$/', $value) ? strtoupper($value) : $fallback; } private static function sanitizeXmlPath(string $value, string $fallback): string { $value = trim($value); $value = preg_replace('/[^A-Za-z0-9_:@.\/-]/', '', $value) ?: ''; return '' !== $value ? $value : $fallback; } private static function sanitizePathList(string $value, string $fallback): string { $value = trim($value); $value = preg_replace('/[^A-Za-z0-9_:@.\/,-]/', '', $value) ?: ''; return '' !== $value ? $value : $fallback; } }